From a4675be2b771ea6addf6c020f50a678b9e9dfb2d Mon Sep 17 00:00:00 2001 From: Quentin Ambard Date: Fri, 26 Jun 2026 13:06:04 +0200 Subject: [PATCH] feat(appkit): resolve schema-relative typegen queries via catalog/schema context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The type-generator DESCRIBE pass ran `DESCRIBE QUERY ` with only a warehouse id and no session catalog/schema. Schema-relative queries — unqualified table names like `FROM orders`, which resolve fine at runtime when the app passes catalog/schema as statement context — therefore failed at typegen time with TABLE_OR_VIEW_NOT_FOUND, and the generated types silently degraded to `result: unknown`. Thread an optional catalog/schema through the typegen path so DESCRIBE runs with the same session context the runtime uses: - vite-plugin reads DATABRICKS_CATALOG / DATABRICKS_SCHEMA from env (next to the existing DATABRICKS_WAREHOUSE_ID) and forwards them. - generateFromEntryPoint -> generateQueriesFromDescribe -> describeAdaptive carry the optional context down to executeStatement, which honors top-level catalog/schema as the equivalent of USE CATALOG / USE SCHEMA for that one statement. Fully backward compatible: when the env vars are unset, no catalog/schema keys are added to the request and behavior is unchanged. Adds describeAdaptive tests covering both the forwarded-context and omitted-context (back-compat) paths. Signed-off-by: Quentin Ambard --- packages/appkit/src/type-generator/index.ts | 7 ++++ .../src/type-generator/query-registry.ts | 11 ++++++ .../src/type-generator/statement-result.ts | 8 ++++ .../tests/statement-result.test.ts | 39 ++++++++++++++++++- .../appkit/src/type-generator/vite-plugin.ts | 8 ++++ 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/appkit/src/type-generator/index.ts b/packages/appkit/src/type-generator/index.ts index 90dcaae7d..0b0a788b9 100644 --- a/packages/appkit/src/type-generator/index.ts +++ b/packages/appkit/src/type-generator/index.ts @@ -298,6 +298,9 @@ export async function generateFromEntryPoint(options: { mvOutFile?: string; mvMetadataOutFile?: string; metricFetcher?: DescribeFetcher; + /** Session catalog/schema for DESCRIBE — see generateQueriesFromDescribe. */ + catalog?: string; + schema?: string; }) { const { outFile, @@ -308,6 +311,8 @@ export async function generateFromEntryPoint(options: { mvOutFile, mvMetadataOutFile, metricFetcher, + catalog, + schema, } = options; const projectRoot = resolveProjectRoot(outFile); @@ -320,6 +325,8 @@ export async function generateFromEntryPoint(options: { const result = await generateQueriesFromDescribe(queryFolder, warehouseId, { noCache, mode, + catalog, + schema, }); queryRegistry = result.schemas; syntaxErrors = result.syntaxErrors ?? []; diff --git a/packages/appkit/src/type-generator/query-registry.ts b/packages/appkit/src/type-generator/query-registry.ts index 1844720f3..765201d48 100644 --- a/packages/appkit/src/type-generator/query-registry.ts +++ b/packages/appkit/src/type-generator/query-registry.ts @@ -487,12 +487,22 @@ export async function generateQueriesFromDescribe( noCache?: boolean; concurrency?: number; mode?: PreflightMode; + /** + * Session catalog/schema for the DESCRIBE statements. Lets schema-relative + * SQL (unqualified table names) resolve at typegen time the same way it + * does at runtime. Optional — when omitted, queries must be fully + * qualified to describe successfully. + */ + catalog?: string; + schema?: string; } = {}, ): Promise { const { noCache = false, concurrency: rawConcurrency = 10, mode = "non-blocking", + catalog, + schema, } = options; const concurrency = typeof rawConcurrency === "number" && Number.isFinite(rawConcurrency) @@ -740,6 +750,7 @@ export async function generateQueriesFromDescribe( `DESCRIBE QUERY ${cleanedSql}`, warehouseId, describeFormat, + { catalog, schema }, ); completed++; diff --git a/packages/appkit/src/type-generator/statement-result.ts b/packages/appkit/src/type-generator/statement-result.ts index 0c0c5d201..581b76f3f 100644 --- a/packages/appkit/src/type-generator/statement-result.ts +++ b/packages/appkit/src/type-generator/statement-result.ts @@ -167,6 +167,7 @@ export async function describeAdaptive( statement: string, warehouseId: string, memo: DescribeFormatMemo, + context?: { catalog?: string; schema?: string }, ): Promise { const formats: DescribeFormat[] = memo.format ? [memo.format] @@ -178,6 +179,13 @@ export async function describeAdaptive( const response = (await client.statementExecution.executeStatement({ statement, warehouse_id: warehouseId, + // Session context for the statement. The Statement Execution API honors + // top-level catalog/schema as the equivalent of USE CATALOG / USE SCHEMA + // for that one statement — so schema-relative SQL (e.g. `FROM orders`, + // no catalog.schema qualifier) resolves during DESCRIBE. Omitted keys + // are simply absent, so callers that pass no context are unaffected. + ...(context?.catalog ? { catalog: context.catalog } : {}), + ...(context?.schema ? { schema: context.schema } : {}), // Synchronous wait: without it the call can return PENDING/RUNNING with // no rows, which downstream misreads as a no-result degrade. wait_timeout: "30s", diff --git a/packages/appkit/src/type-generator/tests/statement-result.test.ts b/packages/appkit/src/type-generator/tests/statement-result.test.ts index 9712d9448..6b148be66 100644 --- a/packages/appkit/src/type-generator/tests/statement-result.test.ts +++ b/packages/appkit/src/type-generator/tests/statement-result.test.ts @@ -272,15 +272,17 @@ describe("describeAdaptive", () => { // each executeStatement to behavior(format), which may resolve or throw. function stubClient(behavior: StubBehavior) { const formats: string[] = []; + const requests: Record[] = []; const client = { statementExecution: { executeStatement: async (req: { format: string }) => { formats.push(req.format); + requests.push(req); return behavior(req.format); }, }, } as unknown as WorkspaceClient; - return { client, formats }; + return { client, formats, requests }; } const rows = ( @@ -484,4 +486,39 @@ describe("describeAdaptive", () => { expect(memo.format).toBeUndefined(); expect(formats).toEqual(["JSON_ARRAY"]); }); + + test("session context: forwards catalog/schema to executeStatement", async () => { + const memo: DescribeFormatMemo = {}; + const { client, requests } = stubClient((format) => { + if (format === "JSON_ARRAY") return rows([["schema"]]); + throw new Error("ARROW should not be tried"); + }); + + await describeAdaptive(client, "DESCRIBE QUERY x", "wh", memo, { + catalog: "my_catalog", + schema: "my_schema", + }); + + // The catalog/schema reach executeStatement as top-level session context, + // so schema-relative SQL (unqualified table names) resolves at DESCRIBE. + expect(requests[0]).toMatchObject({ + catalog: "my_catalog", + schema: "my_schema", + }); + }); + + test("session context: omitted keys are absent (back-compat)", async () => { + const memo: DescribeFormatMemo = {}; + const { client, requests } = stubClient((format) => { + if (format === "JSON_ARRAY") return rows([["schema"]]); + throw new Error("ARROW should not be tried"); + }); + + // No context arg at all → the request carries no catalog/schema, exactly + // as before this parameter existed. + await describeAdaptive(client, "DESCRIBE QUERY x", "wh", memo); + + expect(requests[0]).not.toHaveProperty("catalog"); + expect(requests[0]).not.toHaveProperty("schema"); + }); }); diff --git a/packages/appkit/src/type-generator/vite-plugin.ts b/packages/appkit/src/type-generator/vite-plugin.ts index 635a530f6..85d9dad76 100644 --- a/packages/appkit/src/type-generator/vite-plugin.ts +++ b/packages/appkit/src/type-generator/vite-plugin.ts @@ -102,6 +102,12 @@ export function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin { return; } + // Optional session context so schema-relative queries (unqualified table + // names) resolve during DESCRIBE. Read from env alongside the warehouse + // id; unset → DESCRIBE runs without a default catalog/schema as before. + const catalog = process.env.DATABRICKS_CATALOG || undefined; + const schema = process.env.DATABRICKS_SCHEMA || undefined; + await generateFromEntryPoint({ outFile, queryFolder: watchFolders[0], @@ -110,6 +116,8 @@ export function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin { mode, mvOutFile, mvMetadataOutFile, + catalog, + schema, }); } catch (error) { // TypegenSyntaxError / TypegenFatalError carry a complete, actionable