Skip to content

Commit f05f7b9

Browse files
authored
Fix SQL schema selector execution
Fix schema-aware SQL execution, linting, and Studio navigation; includes a minor changeset for @prisma/studio-core.
1 parent 6ac923f commit f05f7b9

27 files changed

Lines changed: 746 additions & 85 deletions

.changeset/silver-schema-sql.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@prisma/studio-core": minor
3+
---
4+
5+
Fix schema-aware SQL execution, linting, and Studio navigation so SQL queries and diagnostics resolve unqualified identifiers against the selected schema instead of always relying on the adapter default schema.

Architecture/navigation-url-state.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ Use two patterns only:
112112
- Imperative updates: `setViewParam`, `setSchemaParam`, `setTableParam`, `setStreamParam`, etc.
113113

114114
On schema switch, code MUST also resolve and set a valid table for that schema (current behavior in `Navigation.SchemaSelector`).
115+
Database view links in the Studio sidebar (`schema`, `queries`, `console`, and
116+
`sql`) MUST preserve the active `schema` URL param so switching views does not
117+
silently fall back to the adapter default schema.
115118

116119
## Context Boundary
117120

Architecture/sql-editor-intelligence.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ This architecture governs:
5151
- stale responses MUST be discarded via request id check
5252
- Diagnostics MUST be clamped to document bounds before returning to CodeMirror.
5353
- Empty SQL MUST short-circuit with no lint request.
54+
- SQL editor language configuration and lint requests MUST use the active URL
55+
schema as the default schema so completions and diagnostics match the schema
56+
selector.
5457
- Table-level SQL filter pills MAY reuse the same adapter `sqlLint` transport, but they MUST wrap raw `WHERE` fragments in a full `SELECT ... WHERE ...` statement before linting.
5558
- SQL filter-pill linting MUST run asynchronously in the background and MUST NOT block the initial filter apply interaction.
5659
- Table-level SQL filter-pill lint diagnostics are advisory UI state only and MUST NOT mutate the already-applied URL filter after the pill has been saved.
@@ -61,6 +64,8 @@ This architecture governs:
6164
- existing `customHeaders`
6265
- existing `customPayload`
6366
- Procedure name for linting is `sql-lint`.
67+
- Lint request payloads MAY include `schema`; when present, the backend MUST
68+
plan unqualified identifiers against that schema.
6469
- New SQL-editor surfaces MUST NOT bypass BFF auth propagation.
6570
- Executor contract:
6671
- `Executor` MAY expose `lintSql(details, options)` as an optimized lint transport.
@@ -79,6 +84,7 @@ This architecture governs:
7984
- `statement_timeout = 1000ms`
8085
- `lock_timeout = 100ms`
8186
- `idle_in_transaction_session_timeout = 1000ms`
87+
- transaction-local `search_path` when a selected schema is provided
8288

8389
On Postgres errors, diagnostics MUST include mapped position and SQLSTATE when available. Timeout diagnostics (`57014`) MUST be rewritten to a user-facing lint-timeout message.
8490
For multi-statement SQL text, diagnostics MUST map statement-relative positions back to full-editor offsets.

Architecture/sql-view.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ SQL result visualization is governed by:
5353
- SQL execution target is cursor-aware:
5454
- single-statement editor text: execute that statement
5555
- multi-statement editor text: execute the top-level statement containing the cursor
56+
- SQL execution MUST pass the active URL schema to `adapter.raw(...)` so
57+
unqualified identifiers resolve against the schema selected in Studio.
58+
- PostgreSQL raw execution MUST apply the active schema through a
59+
transaction-local `search_path` and MUST NOT leak schema changes across
60+
requests or connections.
5661
- The grid renders all rows returned by that execution.
5762
- "row(s) returned in Xms" MUST report client-observed request duration from
5863
the SQL request transport (BFF request timing) when available; it MUST NOT
@@ -94,6 +99,7 @@ SQL result visualization is governed by:
9499
Changes to SQL view MUST include tests for:
95100

96101
- query execution success and cancellation behavior
102+
- query execution with an active non-public schema
97103
- read-only grid rendering (pin control present, sorting controls disabled)
98104
- absence of history UI
99105
- absence of pagination controls in SQL result grid

FEATURES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ Matching substrings are highlighted in the grid, and timeout errors explain that
208208

209209
The SQL view uses a full CodeMirror editor with dialect-aware syntax highlighting and schema-aware autocomplete for schemas, tables, and columns.
210210
Autocomplete is built from live introspection metadata, so suggestions track the current database structure without manual refresh workflows.
211+
The active schema selector is also used as the default SQL namespace, so unqualified queries like `select * from order_items` run and lint against the selected schema instead of always resolving through `public`.
211212
PostgreSQL, MySQL, and SQLite linting runs asynchronously through guarded parse/plan `EXPLAIN` paths and shows inline diagnostics while preserving the normal run/cancel query flow.
212213
The same lint transport also validates saved table-level SQL filter pills in the background, so Studio reuses one dialect-aware SQL validation path for both the SQL editor and advanced inline table filters.
213214
Keyboard execution supports `Cmd/Ctrl+Enter`, and in multi-statement scripts it runs only the top-level statement at the current cursor.

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ type StudioBFFRequest =
363363
| {
364364
procedure: "query";
365365
query: Query;
366+
schema?: string;
366367
customPayload?: Record<string, unknown>;
367368
}
368369
| {
@@ -378,6 +379,7 @@ type StudioBFFRequest =
378379
| {
379380
procedure: "sql-lint";
380381
sql: string;
382+
schema?: string;
381383
schemaVersion?: string;
382384
customPayload?: Record<string, unknown>;
383385
}
@@ -419,10 +421,10 @@ type QueryInsightsResponse =
419421

420422
### Procedure Semantics
421423

422-
- `query`: execute one SQL statement. This is required for every Studio adapter.
424+
- `query`: execute one SQL statement. This is required for every Studio adapter. Requests may include `schema`; when present, use it as the default namespace for unqualified identifiers.
423425
- `sequence`: execute exactly two queries in order. This is used by MySQL write flows that update first and refetch second.
424426
- `transaction`: execute an ordered list of queries inside one database transaction. This is the contract addition that enables atomic staged multi-row saves from the table editor.
425-
- `sql-lint`: return parse/plan diagnostics for the SQL editor and SQL-backed filter pills.
427+
- `sql-lint`: return parse/plan diagnostics for the SQL editor and SQL-backed filter pills. Requests may include `schema`; diagnostics should plan unqualified identifiers against that selected schema.
426428
- `query-insights`: return a live query snapshot for the optional `Queries` view.
427429

428430
For `sequence`, the second query should only run if the first one succeeds. For `transaction`, the response result array must stay in the same order as `body.queries`.
@@ -469,7 +471,9 @@ export async function handleStudioBff(request: Request): Promise<Response> {
469471
}
470472

471473
if (payload.procedure === "query") {
472-
const [error, result] = await executor.execute(payload.query);
474+
const [error, result] = await executor.execute(payload.query, {
475+
schema: payload.schema,
476+
});
473477
return Response.json([error ? serializeError(error) : null, result]);
474478
}
475479

@@ -513,6 +517,7 @@ export async function handleStudioBff(request: Request): Promise<Response> {
513517
}
514518

515519
const [error, result] = await executor.lintSql({
520+
schema: payload.schema,
516521
schemaVersion: payload.schemaVersion,
517522
sql: payload.sql,
518523
});

data/adapter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,10 @@ export interface AdapterQueryResult {
380380
}
381381

382382
export interface AdapterRawDetails {
383+
/**
384+
* Schema to use as the default namespace for unqualified identifiers.
385+
*/
386+
schema?: string;
383387
sql: string;
384388
}
385389

@@ -400,6 +404,10 @@ export interface AdapterSqlSchemaResult {
400404
}
401405

402406
export interface AdapterSqlLintDetails {
407+
/**
408+
* Schema to use as the default namespace for unqualified identifiers.
409+
*/
410+
schema?: string;
403411
schemaVersion?: string;
404412
sql: string;
405413
}

data/bff/bff-client.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,27 @@ describe("bff/bff-client", () => {
7474
expect(response).toStrictEqual([null, results]);
7575
});
7676

77+
it("should send selected schema context when provided", async () => {
78+
fetchFn.mockResolvedValueOnce(
79+
new Response(JSON.stringify([null, results])),
80+
);
81+
82+
const response = await client.execute(query, {
83+
schema: "test_app",
84+
});
85+
const latestFetchCall = fetchFn.mock.calls.at(-1);
86+
87+
expect(latestFetchCall?.[0]).toBe(url);
88+
expect(JSON.parse(latestFetchCall?.[1]?.body as string)).toStrictEqual({
89+
customPayload,
90+
procedure: "query",
91+
query,
92+
schema: "test_app",
93+
});
94+
95+
expect(response).toStrictEqual([null, results]);
96+
});
97+
7798
it("should return (not throw) an error if the request fails", async () => {
7899
const error = "Internal server error";
79100
fetchFn.mockResolvedValueOnce(new Response(error, { status: 500 }));
@@ -564,6 +585,7 @@ describe("bff/bff-client", () => {
564585

565586
describe("lintSql", () => {
566587
const details = {
588+
schema: "test_app",
567589
schemaVersion: "schema-abc123",
568590
sql: "select * from users",
569591
};
@@ -605,6 +627,7 @@ describe("bff/bff-client", () => {
605627
expect(JSON.parse(value as string)).toStrictEqual({
606628
customPayload,
607629
procedure: "sql-lint",
630+
schema: "test_app",
608631
schemaVersion: "schema-abc123",
609632
sql: "select * from users",
610633
});

data/bff/bff-client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export interface StudioBFFQueryRequest {
281281
customPayload?: Record<string, unknown>;
282282
procedure: "query";
283283
query: Query<unknown>;
284+
schema?: string;
284285
}
285286

286287
export interface StudioBFFSequenceRequest {
@@ -296,6 +297,7 @@ export interface StudioBFFTransactionRequest {
296297
}
297298

298299
export interface StudioBFFSqlLintDetails {
300+
schema?: string;
299301
schemaVersion?: string;
300302
sql: string;
301303
}
@@ -308,6 +310,7 @@ export interface StudioBFFSqlLintResult {
308310
export interface StudioBFFSqlLintRequest {
309311
customPayload?: Record<string, unknown>;
310312
procedure: "sql-lint";
313+
schema?: string;
311314
schemaVersion?: string;
312315
sql: string;
313316
}
@@ -335,6 +338,7 @@ export function createStudioBFFClient(
335338
customPayload,
336339
procedure: "query",
337340
query,
341+
schema: options?.schema,
338342
} satisfies StudioBFFQueryRequest),
339343
headers: {
340344
Accept: "application/json",
@@ -516,6 +520,7 @@ export function createStudioBFFClient(
516520
body: JSON.stringify({
517521
customPayload,
518522
procedure: "sql-lint",
523+
schema: details.schema,
519524
schemaVersion: details.schemaVersion,
520525
sql: details.sql,
521526
} satisfies StudioBFFSqlLintRequest),

data/executor.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,17 @@ export interface SequenceExecutor extends Executor {
3636

3737
export interface ExecuteOptions {
3838
abortSignal?: AbortSignal;
39+
/**
40+
* Schema to use as the default namespace for unqualified identifiers.
41+
*/
42+
schema?: string;
3943
}
4044

4145
export interface SqlLintDetails {
46+
/**
47+
* Schema to use as the default namespace for unqualified identifiers.
48+
*/
49+
schema?: string;
4250
schemaVersion?: string;
4351
sql: string;
4452
}

0 commit comments

Comments
 (0)