@databricks/appkit 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -0
- package/dist/agents/databricks.d.ts +2 -3
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +5 -4
- package/dist/agents/databricks.js.map +1 -1
- package/dist/agents/supervisor-api.d.ts.map +1 -1
- package/dist/agents/supervisor-api.js +3 -1
- package/dist/agents/supervisor-api.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +4 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/connectors/files/client.js +2 -1
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/genie/client.d.ts +2 -4
- package/dist/connectors/genie/client.js +2 -3
- package/dist/connectors/genie/client.js.map +1 -1
- package/dist/connectors/jobs/client.d.ts +2 -2
- package/dist/connectors/jobs/client.js +2 -1
- package/dist/connectors/jobs/client.js.map +1 -1
- package/dist/connectors/serving/client.d.ts +1 -1
- package/dist/connectors/serving/client.js +2 -1
- package/dist/connectors/serving/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js +2 -1
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/defaults.js.map +1 -1
- package/dist/connectors/sql-warehouse/warehouse-status-emitter.js.map +1 -1
- package/dist/connectors/vector-search/client.js.map +1 -1
- package/dist/context/client-options.js.map +1 -1
- package/dist/context/execution-context.d.ts +1 -1
- package/dist/context/service-context.d.ts +2 -1
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +8 -5
- package/dist/context/service-context.js.map +1 -1
- package/dist/core/appkit.d.ts +2 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -1
- package/dist/internal-telemetry/reporter.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/query.js.map +1 -1
- package/dist/plugins/analytics/result-delivery.js.map +1 -1
- package/dist/plugins/files/plugin.js +4 -3
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/files/types.d.ts +2 -1
- package/dist/plugins/files/types.d.ts.map +1 -1
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/jobs/types.d.ts +2 -1
- package/dist/plugins/jobs/types.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +5 -4
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/stream/arrow-stream-processor.js.map +1 -1
- package/dist/type-generator/index.js +3 -2
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/mv-registry/describe.js.map +1 -1
- package/dist/type-generator/query-registry.js +3 -2
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/serving/fetcher.js +2 -1
- package/dist/type-generator/serving/fetcher.js.map +1 -1
- package/dist/type-generator/serving/generator.js +3 -2
- package/dist/type-generator/serving/generator.js.map +1 -1
- package/dist/type-generator/statement-result.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +3 -2
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/type-generator/warehouse-status.js.map +1 -1
- package/dist/workspace-client/client.js +57 -0
- package/dist/workspace-client/client.js.map +1 -0
- package/dist/workspace-client/errors.d.ts +2 -0
- package/dist/workspace-client/errors.js +3 -0
- package/dist/workspace-client/factory.d.ts +19 -0
- package/dist/workspace-client/factory.d.ts.map +1 -0
- package/dist/workspace-client/factory.js +24 -0
- package/dist/workspace-client/factory.js.map +1 -0
- package/dist/workspace-client/index.d.ts +4 -0
- package/dist/workspace-client/index.js +5 -0
- package/dist/workspace-client/legacy.d.ts +29 -0
- package/dist/workspace-client/legacy.d.ts.map +1 -0
- package/dist/workspace-client/legacy.js +23 -0
- package/dist/workspace-client/legacy.js.map +1 -0
- package/dist/workspace-client/types.d.ts +49 -0
- package/dist/workspace-client/types.d.ts.map +1 -0
- package/docs/api/appkit/Class.DatabricksAdapter.md +2 -3
- package/docs/api/appkit/Function.createApp.md +9 -9
- package/docs/api/appkit/Function.createWorkspaceClient.md +28 -0
- package/docs/api/appkit/Interface.WorkspaceClient.md +119 -0
- package/docs/api/appkit/Interface.WorkspaceClientOptions.md +47 -0
- package/docs/api/appkit.md +3 -0
- package/llms.txt +3 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe.js","names":[],"sources":["../../../src/type-generator/mv-registry/describe.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\n// Grammar + SQL-quoting for metric-view FQNs live together in the shared,\n// zod-free leaf so the type-generator and the analytics runtime validate and\n// escape against one source of truth (see the module doc in metric-fqn.ts).\nimport {\n isValidFqn,\n quoteFqnForSql,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport { type DescribeFormatMemo, describeAdaptive } from \"../statement-result\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport type { DescribeFetcher, MetricColumnMetadata } from \"./types\";\n\n/**\n * Parse the JSON payload returned by DESCRIBE TABLE EXTENDED ... AS JSON.\n *\n * The Statement Execution API returns a single string cell — this normalizer\n * unwraps it. Handles both the production (real warehouse) shape and the\n * shape produced by mocked test responses.\n */\nexport function parseDescribeTableExtendedJson(\n response: DatabricksStatementExecutionResponse,\n): unknown {\n if (response.status?.state === \"FAILED\") {\n const msg = response.status.error?.message ?? \"DESCRIBE failed\";\n throw new Error(`DESCRIBE TABLE EXTENDED failed: ${msg}`);\n }\n\n const rows = response.result?.data_array ?? [];\n if (rows.length === 0) {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED returned no rows. Verify the FQN points to a metric view.\",\n );\n }\n\n const cell = rows[0]?.[0];\n if (typeof cell !== \"string\") {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED first cell was not a JSON string. Confirm the AS JSON suffix is supported.\",\n );\n }\n\n try {\n return JSON.parse(cell);\n } catch (err) {\n throw new Error(\n `Failed to parse DESCRIBE TABLE EXTENDED JSON: ${(err as Error).message}`,\n );\n }\n}\n\n/**\n * Pure function: turn the parsed DESCRIBE JSON into structured column metadata.\n *\n * Tolerant of multiple JSON shapes (the field may be `columns` or\n * `schema.fields`, type may be a string or `{ name }` object, the measure\n * marker may be `is_measure` or under `metadata.is_measure`).\n */\nexport function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] {\n if (!parsed || typeof parsed !== \"object\") {\n return [];\n }\n\n const root = parsed as Record<string, unknown>;\n const columnsCandidate = (root.columns ??\n (root.schema && typeof root.schema === \"object\"\n ? (root.schema as Record<string, unknown>).fields\n : undefined)) as unknown;\n\n if (!Array.isArray(columnsCandidate)) {\n return [];\n }\n\n const columns: MetricColumnMetadata[] = [];\n for (const raw of columnsCandidate) {\n if (!raw || typeof raw !== \"object\") continue;\n const obj = raw as Record<string, unknown>;\n const name =\n typeof obj.name === \"string\"\n ? obj.name\n : typeof obj.column_name === \"string\"\n ? obj.column_name\n : undefined;\n if (!name) continue;\n\n const typeRaw = obj.type ?? obj.data_type ?? obj.type_name;\n let type = \"STRING\";\n if (typeof typeRaw === \"string\") {\n type = typeRaw;\n } else if (typeRaw && typeof typeRaw === \"object\") {\n const inner = (typeRaw as Record<string, unknown>).name;\n if (typeof inner === \"string\") type = inner;\n }\n\n let isMeasure = false;\n if (typeof obj.is_measure === \"boolean\") {\n isMeasure = obj.is_measure;\n } else if (\n obj.metadata &&\n typeof obj.metadata === \"object\" &&\n typeof (obj.metadata as Record<string, unknown>).is_measure === \"boolean\"\n ) {\n isMeasure = (obj.metadata as Record<string, unknown>)\n .is_measure as boolean;\n } else if (obj.kind === \"measure\" || obj.role === \"measure\") {\n isMeasure = true;\n }\n\n const description =\n typeof obj.comment === \"string\"\n ? obj.comment\n : typeof obj.description === \"string\"\n ? obj.description\n : undefined;\n\n const displayName = extractStringFromAny(obj, [\n \"display_name\",\n \"displayName\",\n ]);\n const format = extractFormatString(obj);\n\n // Time-grain inference is type-driven, not YAML-attribute-driven.\n const timeGrains = isMeasure ? undefined : inferTimeGrains(type);\n\n columns.push({\n name,\n type,\n isMeasure,\n description,\n ...(displayName ? { displayName } : {}),\n ...(format ? { format } : {}),\n ...(timeGrains ? { timeGrains } : {}),\n });\n }\n\n return columns;\n}\n\n/**\n * Read a non-empty string attribute from a DESCRIBE column entry, tolerating\n * the multiple shapes UC has shipped for this metadata over time.\n */\nfunction extractStringFromAny(\n obj: Record<string, unknown>,\n candidates: readonly string[],\n): string | undefined {\n for (const key of candidates) {\n const direct = obj[key];\n if (typeof direct === \"string\" && direct.trim().length > 0) {\n return direct;\n }\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n if (typeof nested === \"string\" && nested.trim().length > 0) {\n return nested;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Read the column's `format` attribute from a DESCRIBE entry and return a\n * printf-like format string suitable for `formatValue` and `toD3Format`.\n */\nfunction extractFormatString(obj: Record<string, unknown>): string | undefined {\n for (const key of [\"format\", \"format_spec\"]) {\n const direct = obj[key];\n const fromDirect = formatStringFromValue(direct);\n if (fromDirect) return fromDirect;\n\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n const fromMeta = formatStringFromValue(nested);\n if (fromMeta) return fromMeta;\n }\n }\n return undefined;\n}\n\nfunction formatStringFromValue(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return translateStructuredFormat(value as Record<string, unknown>);\n }\n return undefined;\n}\n\n/**\n * Translate the structured `format` object emitted by DESCRIBE TABLE EXTENDED\n * AS JSON into a printf-like format string.\n */\nfunction translateStructuredFormat(\n spec: Record<string, unknown>,\n): string | undefined {\n if (spec.currency && typeof spec.currency === \"object\") {\n return currencyFormatString(spec.currency as Record<string, unknown>);\n }\n if (spec.percent && typeof spec.percent === \"object\") {\n return percentFormatString(spec.percent as Record<string, unknown>);\n }\n if (spec.number && typeof spec.number === \"object\") {\n return numberFormatString(spec.number as Record<string, unknown>);\n }\n return undefined;\n}\n\nfunction currencyFormatString(c: Record<string, unknown>): string {\n const places = readDecimalPlaces(c) ?? 2;\n const codeRaw = c.currency_code;\n const code =\n typeof codeRaw === \"string\" && codeRaw.trim().length > 0\n ? codeRaw.toUpperCase()\n : \"USD\";\n const symbol = currencySymbol(code);\n return `${symbol}#,##0${fractionalSuffix(places)}`;\n}\n\nfunction percentFormatString(p: Record<string, unknown>): string {\n const places = readDecimalPlaces(p) ?? 0;\n return `0${fractionalSuffix(places)}%`;\n}\n\nfunction numberFormatString(n: Record<string, unknown>): string {\n const places = readDecimalPlaces(n) ?? 0;\n return `#,##0${fractionalSuffix(places)}`;\n}\n\nfunction fractionalSuffix(places: number): string {\n return places > 0 ? `.${\"0\".repeat(places)}` : \"\";\n}\n\n/**\n * Maximum decimal places honored from a format spec.\n */\nconst MAX_DECIMAL_PLACES = 100;\n\nfunction readDecimalPlaces(obj: Record<string, unknown>): number | undefined {\n const dp = obj.decimal_places;\n if (typeof dp === \"number\" && Number.isFinite(dp) && dp >= 0) {\n return Math.min(Math.floor(dp), MAX_DECIMAL_PLACES);\n }\n if (dp && typeof dp === \"object\" && !Array.isArray(dp)) {\n const places = (dp as Record<string, unknown>).places;\n if (typeof places === \"number\" && Number.isFinite(places) && places >= 0) {\n return Math.min(Math.floor(places), MAX_DECIMAL_PLACES);\n }\n }\n return undefined;\n}\n\n/**\n * Map ISO currency codes to their conventional prefix symbol. Unknown codes\n * fall back to the literal code + space.\n */\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n USD: \"$\",\n EUR: \"€\",\n GBP: \"£\",\n JPY: \"¥\",\n CNY: \"¥\",\n INR: \"₹\",\n BRL: \"R$\",\n};\n\nfunction currencySymbol(code: string): string {\n return CURRENCY_SYMBOLS[code] ?? `${code} `;\n}\n\n/**\n * Infer the standard set of valid time grains for a dimension based on its\n * SQL data type.\n */\nfunction inferTimeGrains(type: string): string[] | undefined {\n const normalized = type\n .toLowerCase()\n .replace(/\\(.*\\)$/, \"\")\n .trim();\n if (\n normalized === \"timestamp\" ||\n normalized === \"timestamp_ltz\" ||\n normalized === \"timestamp_ntz\"\n ) {\n return [\"day\", \"hour\", \"minute\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n if (normalized === \"date\") {\n return [\"day\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n return undefined;\n}\n\n/**\n * Build a DescribeFetcher from a real WorkspaceClient + warehouseId.\n */\nexport function createWorkspaceDescribeFetcher(\n client: WorkspaceClient,\n warehouseId: string,\n): DescribeFetcher {\n // One format probe per fetcher (= per typegen run): the first DESCRIBE\n // discovers the warehouse's working format, every later one reuses it.\n const describeFormat: DescribeFormatMemo = {};\n return async (fqn: string) => {\n /**\n * Defense-in-depth: every caller passes a source that already cleared\n * {@link resolveMetricConfig}, but this fetcher is an exported seam — re-check\n * before interpolating into SQL.\n */\n if (!isValidFqn(fqn)) {\n throw new Error(\n `Invalid metric source \"${fqn}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view>.`,\n );\n }\n // Escape + quote every segment before interpolation. isValidFqn already\n // rejects backticks/control chars for metric sources, so this is\n // belt-and-suspenders for the SQL-injection seam — and keeps the quoting\n // independent of the naming rule.\n const quotedFqn = quoteFqnForSql(fqn);\n return describeAdaptive(\n client,\n `DESCRIBE TABLE EXTENDED ${quotedFqn} AS JSON`,\n warehouseId,\n describeFormat,\n );\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"describe.js","names":[],"sources":["../../../src/type-generator/mv-registry/describe.ts"],"sourcesContent":["// Grammar + SQL-quoting for metric-view FQNs live together in the shared,\n// zod-free leaf so the type-generator and the analytics runtime validate and\n// escape against one source of truth (see the module doc in metric-fqn.ts).\nimport {\n isValidFqn,\n quoteFqnForSql,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\nimport { type DescribeFormatMemo, describeAdaptive } from \"../statement-result\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport type { DescribeFetcher, MetricColumnMetadata } from \"./types\";\n\n/**\n * Parse the JSON payload returned by DESCRIBE TABLE EXTENDED ... AS JSON.\n *\n * The Statement Execution API returns a single string cell — this normalizer\n * unwraps it. Handles both the production (real warehouse) shape and the\n * shape produced by mocked test responses.\n */\nexport function parseDescribeTableExtendedJson(\n response: DatabricksStatementExecutionResponse,\n): unknown {\n if (response.status?.state === \"FAILED\") {\n const msg = response.status.error?.message ?? \"DESCRIBE failed\";\n throw new Error(`DESCRIBE TABLE EXTENDED failed: ${msg}`);\n }\n\n const rows = response.result?.data_array ?? [];\n if (rows.length === 0) {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED returned no rows. Verify the FQN points to a metric view.\",\n );\n }\n\n const cell = rows[0]?.[0];\n if (typeof cell !== \"string\") {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED first cell was not a JSON string. Confirm the AS JSON suffix is supported.\",\n );\n }\n\n try {\n return JSON.parse(cell);\n } catch (err) {\n throw new Error(\n `Failed to parse DESCRIBE TABLE EXTENDED JSON: ${(err as Error).message}`,\n );\n }\n}\n\n/**\n * Pure function: turn the parsed DESCRIBE JSON into structured column metadata.\n *\n * Tolerant of multiple JSON shapes (the field may be `columns` or\n * `schema.fields`, type may be a string or `{ name }` object, the measure\n * marker may be `is_measure` or under `metadata.is_measure`).\n */\nexport function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] {\n if (!parsed || typeof parsed !== \"object\") {\n return [];\n }\n\n const root = parsed as Record<string, unknown>;\n const columnsCandidate = (root.columns ??\n (root.schema && typeof root.schema === \"object\"\n ? (root.schema as Record<string, unknown>).fields\n : undefined)) as unknown;\n\n if (!Array.isArray(columnsCandidate)) {\n return [];\n }\n\n const columns: MetricColumnMetadata[] = [];\n for (const raw of columnsCandidate) {\n if (!raw || typeof raw !== \"object\") continue;\n const obj = raw as Record<string, unknown>;\n const name =\n typeof obj.name === \"string\"\n ? obj.name\n : typeof obj.column_name === \"string\"\n ? obj.column_name\n : undefined;\n if (!name) continue;\n\n const typeRaw = obj.type ?? obj.data_type ?? obj.type_name;\n let type = \"STRING\";\n if (typeof typeRaw === \"string\") {\n type = typeRaw;\n } else if (typeRaw && typeof typeRaw === \"object\") {\n const inner = (typeRaw as Record<string, unknown>).name;\n if (typeof inner === \"string\") type = inner;\n }\n\n let isMeasure = false;\n if (typeof obj.is_measure === \"boolean\") {\n isMeasure = obj.is_measure;\n } else if (\n obj.metadata &&\n typeof obj.metadata === \"object\" &&\n typeof (obj.metadata as Record<string, unknown>).is_measure === \"boolean\"\n ) {\n isMeasure = (obj.metadata as Record<string, unknown>)\n .is_measure as boolean;\n } else if (obj.kind === \"measure\" || obj.role === \"measure\") {\n isMeasure = true;\n }\n\n const description =\n typeof obj.comment === \"string\"\n ? obj.comment\n : typeof obj.description === \"string\"\n ? obj.description\n : undefined;\n\n const displayName = extractStringFromAny(obj, [\n \"display_name\",\n \"displayName\",\n ]);\n const format = extractFormatString(obj);\n\n // Time-grain inference is type-driven, not YAML-attribute-driven.\n const timeGrains = isMeasure ? undefined : inferTimeGrains(type);\n\n columns.push({\n name,\n type,\n isMeasure,\n description,\n ...(displayName ? { displayName } : {}),\n ...(format ? { format } : {}),\n ...(timeGrains ? { timeGrains } : {}),\n });\n }\n\n return columns;\n}\n\n/**\n * Read a non-empty string attribute from a DESCRIBE column entry, tolerating\n * the multiple shapes UC has shipped for this metadata over time.\n */\nfunction extractStringFromAny(\n obj: Record<string, unknown>,\n candidates: readonly string[],\n): string | undefined {\n for (const key of candidates) {\n const direct = obj[key];\n if (typeof direct === \"string\" && direct.trim().length > 0) {\n return direct;\n }\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n if (typeof nested === \"string\" && nested.trim().length > 0) {\n return nested;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Read the column's `format` attribute from a DESCRIBE entry and return a\n * printf-like format string suitable for `formatValue` and `toD3Format`.\n */\nfunction extractFormatString(obj: Record<string, unknown>): string | undefined {\n for (const key of [\"format\", \"format_spec\"]) {\n const direct = obj[key];\n const fromDirect = formatStringFromValue(direct);\n if (fromDirect) return fromDirect;\n\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n const fromMeta = formatStringFromValue(nested);\n if (fromMeta) return fromMeta;\n }\n }\n return undefined;\n}\n\nfunction formatStringFromValue(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return translateStructuredFormat(value as Record<string, unknown>);\n }\n return undefined;\n}\n\n/**\n * Translate the structured `format` object emitted by DESCRIBE TABLE EXTENDED\n * AS JSON into a printf-like format string.\n */\nfunction translateStructuredFormat(\n spec: Record<string, unknown>,\n): string | undefined {\n if (spec.currency && typeof spec.currency === \"object\") {\n return currencyFormatString(spec.currency as Record<string, unknown>);\n }\n if (spec.percent && typeof spec.percent === \"object\") {\n return percentFormatString(spec.percent as Record<string, unknown>);\n }\n if (spec.number && typeof spec.number === \"object\") {\n return numberFormatString(spec.number as Record<string, unknown>);\n }\n return undefined;\n}\n\nfunction currencyFormatString(c: Record<string, unknown>): string {\n const places = readDecimalPlaces(c) ?? 2;\n const codeRaw = c.currency_code;\n const code =\n typeof codeRaw === \"string\" && codeRaw.trim().length > 0\n ? codeRaw.toUpperCase()\n : \"USD\";\n const symbol = currencySymbol(code);\n return `${symbol}#,##0${fractionalSuffix(places)}`;\n}\n\nfunction percentFormatString(p: Record<string, unknown>): string {\n const places = readDecimalPlaces(p) ?? 0;\n return `0${fractionalSuffix(places)}%`;\n}\n\nfunction numberFormatString(n: Record<string, unknown>): string {\n const places = readDecimalPlaces(n) ?? 0;\n return `#,##0${fractionalSuffix(places)}`;\n}\n\nfunction fractionalSuffix(places: number): string {\n return places > 0 ? `.${\"0\".repeat(places)}` : \"\";\n}\n\n/**\n * Maximum decimal places honored from a format spec.\n */\nconst MAX_DECIMAL_PLACES = 100;\n\nfunction readDecimalPlaces(obj: Record<string, unknown>): number | undefined {\n const dp = obj.decimal_places;\n if (typeof dp === \"number\" && Number.isFinite(dp) && dp >= 0) {\n return Math.min(Math.floor(dp), MAX_DECIMAL_PLACES);\n }\n if (dp && typeof dp === \"object\" && !Array.isArray(dp)) {\n const places = (dp as Record<string, unknown>).places;\n if (typeof places === \"number\" && Number.isFinite(places) && places >= 0) {\n return Math.min(Math.floor(places), MAX_DECIMAL_PLACES);\n }\n }\n return undefined;\n}\n\n/**\n * Map ISO currency codes to their conventional prefix symbol. Unknown codes\n * fall back to the literal code + space.\n */\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n USD: \"$\",\n EUR: \"€\",\n GBP: \"£\",\n JPY: \"¥\",\n CNY: \"¥\",\n INR: \"₹\",\n BRL: \"R$\",\n};\n\nfunction currencySymbol(code: string): string {\n return CURRENCY_SYMBOLS[code] ?? `${code} `;\n}\n\n/**\n * Infer the standard set of valid time grains for a dimension based on its\n * SQL data type.\n */\nfunction inferTimeGrains(type: string): string[] | undefined {\n const normalized = type\n .toLowerCase()\n .replace(/\\(.*\\)$/, \"\")\n .trim();\n if (\n normalized === \"timestamp\" ||\n normalized === \"timestamp_ltz\" ||\n normalized === \"timestamp_ntz\"\n ) {\n return [\"day\", \"hour\", \"minute\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n if (normalized === \"date\") {\n return [\"day\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n return undefined;\n}\n\n/**\n * Build a DescribeFetcher from a real WorkspaceClient + warehouseId.\n */\nexport function createWorkspaceDescribeFetcher(\n client: WorkspaceClient,\n warehouseId: string,\n): DescribeFetcher {\n // One format probe per fetcher (= per typegen run): the first DESCRIBE\n // discovers the warehouse's working format, every later one reuses it.\n const describeFormat: DescribeFormatMemo = {};\n return async (fqn: string) => {\n /**\n * Defense-in-depth: every caller passes a source that already cleared\n * {@link resolveMetricConfig}, but this fetcher is an exported seam — re-check\n * before interpolating into SQL.\n */\n if (!isValidFqn(fqn)) {\n throw new Error(\n `Invalid metric source \"${fqn}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view>.`,\n );\n }\n // Escape + quote every segment before interpolation. isValidFqn already\n // rejects backticks/control chars for metric sources, so this is\n // belt-and-suspenders for the SQL-injection seam — and keeps the quoting\n // independent of the naming rule.\n const quotedFqn = quoteFqnForSql(fqn);\n return describeAdaptive(\n client,\n `DESCRIBE TABLE EXTENDED ${quotedFqn} AS JSON`,\n warehouseId,\n describeFormat,\n );\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createLogger } from "../logging/logger.js";
|
|
2
|
+
import { createWorkspaceClient } from "../workspace-client/factory.js";
|
|
3
|
+
import "../workspace-client/index.js";
|
|
2
4
|
import { CACHE_VERSION, hashSQL, loadCache, saveCache } from "./cache.js";
|
|
3
5
|
import { getErrorDiagnostic, isConnectivityError } from "./errors.js";
|
|
4
6
|
import { describeAdaptive } from "./statement-result.js";
|
|
@@ -6,7 +8,6 @@ import { decidePreflight } from "./preflight.js";
|
|
|
6
8
|
import { Spinner } from "./spinner.js";
|
|
7
9
|
import { sqlTypeToHelper, sqlTypeToMarker } from "./types.js";
|
|
8
10
|
import { getWarehouseState, startWarehouse, waitUntilRunning } from "./warehouse-status.js";
|
|
9
|
-
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
10
11
|
import pc from "picocolors";
|
|
11
12
|
import fs from "node:fs/promises";
|
|
12
13
|
import path from "node:path";
|
|
@@ -349,7 +350,7 @@ async function generateQueriesFromDescribe(queryFolder, warehouseId, options = {
|
|
|
349
350
|
} : loadCache()]);
|
|
350
351
|
const queryFiles = allFiles.filter((file) => file.endsWith(".sql"));
|
|
351
352
|
logger.debug("Found %d SQL queries", queryFiles.length);
|
|
352
|
-
const client =
|
|
353
|
+
const client = createWorkspaceClient();
|
|
353
354
|
const spinner = new Spinner();
|
|
354
355
|
const sqlContents = await Promise.all(queryFiles.map((file) => fs.readFile(path.join(queryFolder, file), "utf8")));
|
|
355
356
|
const startTime = performance.now();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-registry.js","names":[],"sources":["../../src/type-generator/query-registry.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport { tableFromIPC } from \"apache-arrow\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport { CACHE_VERSION, hashSQL, loadCache, saveCache } from \"./cache\";\nimport { getErrorDiagnostic, isConnectivityError } from \"./errors\";\nimport { decidePreflight, type PreflightMode } from \"./preflight\";\nimport { Spinner } from \"./spinner\";\nimport { type DescribeFormatMemo, describeAdaptive } from \"./statement-result\";\nimport {\n type DatabricksStatementExecutionResponse,\n type QueryFatalError,\n type QueryGenerationResult,\n type QuerySchema,\n type QuerySyntaxError,\n sqlTypeToHelper,\n sqlTypeToMarker,\n} from \"./types\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:query-registry\");\n\n/**\n * Upper bound on how long a `blocking`-mode preflight will wait for a starting\n * warehouse to reach RUNNING before giving up (~5 min). Generous enough to ride\n * out a cold start without hanging an interactive CLI invocation indefinitely.\n */\nconst PREFLIGHT_WAIT_MAX_MS = 300_000;\n\n/**\n * Regex breakdown:\n * '(?:[^']|'')*' — matches a SQL string literal, including escaped '' pairs\n * | — alternation: whichever branch matches first at a position wins\n * --[^\\n]* — matches a single-line SQL comment\n *\n * Because the regex engine scans left-to-right, a `'` is consumed as a string\n * literal before any `--` inside it could match as a comment — giving us\n * correct single-pass ordering without a manual state machine.\n *\n * V1: no block-comment support (deferred to next PR).\n */\nconst PROTECTED_RANGE_RE = /'(?:[^']|'')*'|--[^\\n]*/g;\n\n/**\n * Numeric-context patterns for positional type inference.\n * Hoisted to module scope — safe because matchAll() clones the regex internally.\n */\nconst NUMERIC_PATTERNS: RegExp[] = [\n /\\bLIMIT\\s+:([a-zA-Z_]\\w*)/gi,\n /\\bOFFSET\\s+:([a-zA-Z_]\\w*)/gi,\n /\\bTOP\\s+:([a-zA-Z_]\\w*)/gi,\n /\\bFETCH\\s+FIRST\\s+:([a-zA-Z_]\\w*)\\s+ROWS/gi,\n // V1 limitation: arithmetic operators may false-positive for date\n // expressions like `:start_date - INTERVAL '1 day'`. A smarter\n // heuristic (e.g. look-ahead for INTERVAL) is deferred to a future PR.\n /[+\\-*/]\\s*:([a-zA-Z_]\\w*)/g,\n /:([a-zA-Z_]\\w*)\\s*[+\\-*/]/g,\n];\n\nexport function getProtectedRanges(sql: string): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n for (const m of sql.matchAll(PROTECTED_RANGE_RE)) {\n ranges.push([m.index, m.index + m[0].length]);\n }\n return ranges;\n}\n\nfunction isInsideProtectedRange(\n offset: number,\n ranges: Array<[number, number]>,\n): boolean {\n return ranges.some(([start, end]) => offset >= start && offset < end);\n}\n\n/**\n * Parse a raw API/SDK error into a structured code + message.\n * Handles Databricks-style JSON bodies embedded in the message string,\n * e.g. `Response from server (Bad Request) {\"error_code\":\"...\",\"message\":\"...\"}`.\n */\nfunction parseError(raw: string): { code?: string; message: string } {\n const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n try {\n const parsed = JSON.parse(jsonMatch[0]);\n if (parsed.error_code || parsed.message) {\n return {\n code: parsed.error_code,\n message: parsed.message || raw,\n };\n }\n } catch {\n // not valid JSON, fall through\n }\n }\n return { message: raw };\n}\n\n/**\n * Extract parameters from a SQL query\n * @param sql - the SQL query to extract parameters from\n * @returns an array of parameter names\n */\nexport function extractParameters(\n sql: string,\n ranges?: Array<[number, number]>,\n): string[] {\n const protectedRanges = ranges ?? getProtectedRanges(sql);\n const matches = sql.matchAll(/(?<!:):([a-zA-Z_]\\w*)/g);\n const params = new Set<string>();\n for (const match of matches) {\n if (!isInsideProtectedRange(match.index, protectedRanges)) {\n params.add(match[1]);\n }\n }\n return Array.from(params);\n}\n\n// parameters that are injected by the server\nexport const SERVER_INJECTED_PARAMS = [\"workspaceId\"];\n\n/**\n * Generates the TypeScript type literal for query parameters from SQL.\n * Shared by both the success and failure paths.\n */\nfunction formatParametersType(sql: string): string {\n const params = extractParameters(sql).filter(\n (p) => !SERVER_INJECTED_PARAMS.includes(p),\n );\n const paramTypes = extractParameterTypes(sql);\n\n return params.length > 0\n ? `{\\n ${params\n .map((p) => {\n const sqlType = paramTypes[p];\n const markerType = sqlType\n ? sqlTypeToMarker[sqlType]\n : \"SQLTypeMarker\";\n const helper = sqlType ? sqlTypeToHelper[sqlType] : \"sql.*()\";\n return `/** ${sqlType || \"any\"} - use ${helper} */\\n ${p}: ${markerType}`;\n })\n .join(\";\\n \")};\\n }`\n : \"Record<string, never>\";\n}\n\n/**\n * Decode a base64 Arrow IPC attachment from a DESCRIBE QUERY response and\n * extract column metadata. Returns the same shape as rows parsed from the\n * legacy data_array path.\n *\n * IMPORTANT: a DESCRIBE QUERY response is itself a result *table* with rows\n * shaped like `(col_name, data_type, comment)` describing the user query's\n * output schema. We must read those rows — NOT `table.schema.fields`, which\n * would describe DESCRIBE QUERY's own output (`col_name`, `data_type`,\n * `comment`) and yield bogus types for every query.\n */\nfunction columnsFromArrowAttachment(\n attachment: string,\n): Array<{ name: string; type_name: string; comment: string | undefined }> {\n const buf = Buffer.from(attachment, \"base64\");\n const table = tableFromIPC(buf);\n return table.toArray().map((row) => {\n const obj = row.toJSON() as {\n col_name?: unknown;\n data_type?: unknown;\n comment?: unknown;\n };\n return {\n name: typeof obj.col_name === \"string\" ? obj.col_name : \"\",\n type_name:\n typeof obj.data_type === \"string\"\n ? obj.data_type.toUpperCase()\n : \"STRING\",\n comment:\n typeof obj.comment === \"string\" && obj.comment !== \"\"\n ? obj.comment\n : undefined,\n };\n });\n}\n\nexport function convertToQueryType(\n result: DatabricksStatementExecutionResponse,\n sql: string,\n queryName: string,\n): { type: string; hasResults: boolean } {\n const dataRows = result.result?.data_array || [];\n let columns = dataRows.map((row) => ({\n name: row[0] || \"\",\n type_name: row[1]?.toUpperCase() || \"STRING\",\n comment: row[2] || undefined,\n }));\n\n // Fallback: serverless warehouses return ARROW_STREAM format with an inline\n // base64 attachment instead of data_array. Decode the Arrow IPC rows (the\n // DESCRIBE QUERY result table) to extract column names and types.\n if (columns.length === 0 && result.result?.attachment) {\n logger.debug(\"data_array empty, decoding Arrow IPC attachment for schema\");\n try {\n columns = columnsFromArrowAttachment(result.result.attachment);\n } catch (err) {\n logger.warn(\n \"Failed to decode Arrow IPC attachment: %s\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n const paramsType = formatParametersType(sql);\n\n // generate result fields with JSDoc\n const resultFields = columns.map((column) => {\n const normalizedType = normalizeTypeName(column.type_name);\n const mappedType = typeMap[normalizedType] || \"unknown\";\n // validate column name is a valid identifier\n const name = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(column.name)\n ? column.name\n : `\"${column.name}\"`;\n\n // generate comment for column\n const comment = column.comment\n ? `/** ${column.comment.replace(/\\*\\//g, \"* /\")} */\\n `\n : `/** @sqlType ${column.type_name.replace(/\\*\\//g, \"* /\")} */\\n `;\n\n return `${comment}${name}: ${mappedType}`;\n });\n\n const hasResults = resultFields.length > 0;\n\n const type = `{\n name: \"${queryName}\";\n parameters: ${paramsType};\n result: ${\n hasResults\n ? `Array<{\n ${resultFields.join(\";\\n \")};\n }>`\n : \"unknown\"\n };\n }`;\n\n return { type, hasResults };\n}\n\n/**\n * Used when DESCRIBE QUERY fails so the query still appears in QueryRegistry.\n * Generates a type with unknown result from SQL alone (no warehouse call).\n */\nfunction generateUnknownResultQuery(sql: string, queryName: string): string {\n const paramsType = formatParametersType(sql);\n\n return `{\n name: \"${queryName}\";\n parameters: ${paramsType};\n result: unknown;\n }`;\n}\n\n/**\n * Degrade gracefully when DESCRIBE can't produce a fresh schema (transient\n * connectivity outage, or a warehouse that's reachable but not ready). Reuse\n * the last-good cached type when the SQL hash is unchanged, otherwise emit\n * `unknown` from SQL alone. Never persists `result: unknown`.\n */\nfunction degradedType(\n cache: Awaited<ReturnType<typeof loadCache>>,\n queryName: string,\n sql: string,\n sqlHash: string,\n): string {\n const prior = cache.queries[queryName];\n const canReusePrior = prior?.hash === sqlHash && !prior.retry;\n return canReusePrior\n ? prior.type\n : generateUnknownResultQuery(sql, queryName);\n}\n\n// Single source of truth for the `@param` type alternation, shared by\n// extractParameterTypes and extractParameterDefaults so the two can't drift.\n// Alternation order matters: TIMESTAMP_NTZ must precede TIMESTAMP so the regex\n// engine doesn't greedy-match TIMESTAMP and leave `_NTZ` unconsumed.\nconst PARAM_TYPE_ALTERNATION =\n \"STRING|NUMERIC|DECIMAL|BIGINT|TINYINT|SMALLINT|INT|FLOAT|DOUBLE|BOOLEAN|DATE|TIMESTAMP_NTZ|TIMESTAMP|BINARY\";\n\nexport function extractParameterTypes(sql: string): Record<string, string> {\n const paramTypes: Record<string, string> = {};\n const regex = new RegExp(\n `--\\\\s*@param\\\\s+(\\\\w+)\\\\s+(${PARAM_TYPE_ALTERNATION})\\\\b`,\n \"gi\",\n );\n const matches = sql.matchAll(regex);\n for (const match of matches) {\n const [, paramName, paramType] = match;\n paramTypes[paramName] = paramType.toUpperCase();\n }\n\n return paramTypes;\n}\n\nexport function defaultForType(sqlType: string | undefined): string {\n switch (sqlType?.toUpperCase()) {\n case \"NUMERIC\":\n case \"DECIMAL\":\n case \"BIGINT\":\n case \"TINYINT\":\n case \"SMALLINT\":\n case \"INT\":\n return \"0\";\n case \"FLOAT\":\n case \"DOUBLE\":\n return \"0.0\";\n case \"STRING\":\n return \"''\";\n case \"BOOLEAN\":\n return \"true\";\n case \"DATE\":\n return \"'2000-01-01'\";\n case \"TIMESTAMP\":\n return \"'2000-01-01T00:00:00Z'\";\n case \"TIMESTAMP_NTZ\":\n return \"'2000-01-01T00:00:00'\";\n case \"BINARY\":\n return \"X'00'\";\n default:\n return \"''\";\n }\n}\n\n/**\n * True when `raw` is already a single, well-formed SQL single-quoted string\n * literal — i.e. it opens and closes with `'` and every interior quote is part\n * of an escaped `''` pair. `'2024-01-01'` and `'O''Brien'` qualify;\n * `'a' OR 1=1 OR 'b'` does not (it has lone interior quotes), so it is treated\n * as raw content and re-escaped rather than trusted. A backslash also\n * disqualifies it: Databricks/Spark treats `\\` as an escape inside literals (so\n * `'a\\'` is unterminated), and we never trust such input — it is re-escaped.\n */\nfunction isWellFormedStringLiteral(raw: string): boolean {\n if (raw.length < 2 || !raw.startsWith(\"'\") || !raw.endsWith(\"'\")) {\n return false;\n }\n const inner = raw.slice(1, -1);\n if (inner.includes(\"\\\\\")) return false;\n return !inner.replace(/''/g, \"\").includes(\"'\");\n}\n\n/**\n * Format a user-supplied sample value as a SQL literal for substitution into\n * the build-time DESCRIBE statement. Returns `null` when the value isn't valid\n * for its type, so the caller falls back to the safe type-based placeholder\n * instead of substituting attacker-controllable text.\n *\n * The value comes from a `.sql` file that may be shared via a template or\n * dependency, so it must not be able to inject SQL into `DESCRIBE QUERY`:\n * - string-like types are always emitted as one well-formed, fully-escaped\n * single-quoted literal (a pre-quoted literal is kept as-is, anything else is\n * quoted with both `\\` and `'` doubled so neither can terminate the literal),\n * so the value can never break out of the string;\n * - numeric / boolean / binary values must match a strict literal shape and are\n * rejected (`null`) otherwise, rather than being passed through verbatim.\n */\nfunction formatSampleValue(\n sqlType: string | undefined,\n raw: string,\n): string | null {\n switch (sqlType?.toUpperCase()) {\n case \"STRING\":\n case \"DATE\":\n case \"TIMESTAMP\":\n case \"TIMESTAMP_NTZ\":\n return isWellFormedStringLiteral(raw)\n ? raw\n : `'${raw.replace(/\\\\/g, \"\\\\\\\\\").replace(/'/g, \"''\")}'`;\n case \"NUMERIC\":\n case \"DECIMAL\":\n case \"BIGINT\":\n case \"TINYINT\":\n case \"SMALLINT\":\n case \"INT\":\n case \"FLOAT\":\n case \"DOUBLE\":\n return /^[+-]?\\d+(\\.\\d+)?$/.test(raw) ? raw : null;\n case \"BOOLEAN\":\n return /^(?:true|false)$/i.test(raw) ? raw.toLowerCase() : null;\n case \"BINARY\":\n return /^X'[0-9a-fA-F]*'$/i.test(raw) ? raw : null;\n default:\n return null;\n }\n}\n\n/**\n * Parse optional describe-time sample values from `@param` annotations, e.g.\n * `-- @param target_catalog STRING = main`. The value is substituted into the\n * SQL **only during DESCRIBE QUERY** so type generation can resolve queries\n * whose shape depends on a parameter value — most notably dynamic table names\n * via `IDENTIFIER(:target_catalog || '.schema.table')`, where the empty-string\n * default would otherwise produce malformed SQL. Runtime binding is unaffected:\n * the analytics plugin still binds the real parameter at execution time, so the\n * query stays portable across environments.\n *\n * Returns a map of parameter name to the formatted SQL literal to substitute.\n */\nexport function extractParameterDefaults(sql: string): Record<string, string> {\n const defaults: Record<string, string> = {};\n // Reuses PARAM_TYPE_ALTERNATION, then requires `= <value>` on the same line.\n // All inter-token whitespace is horizontal-only (`[^\\S\\r\\n]`, not `\\s`): `\\s`\n // matches newlines, so a value-less line like `-- @param x STRING =` would let\n // `\\s*=\\s*(.+?)` swallow the *next* line as the sample value. Restricting to\n // same-line whitespace makes such a line simply not match, so it correctly\n // falls back to the type placeholder.\n const regex = new RegExp(\n `--[^\\\\S\\\\r\\\\n]*@param[^\\\\S\\\\r\\\\n]+(\\\\w+)[^\\\\S\\\\r\\\\n]+(${PARAM_TYPE_ALTERNATION})[^\\\\S\\\\r\\\\n]*=[^\\\\S\\\\r\\\\n]*(.+?)[^\\\\S\\\\r\\\\n]*$`,\n \"gim\",\n );\n for (const match of sql.matchAll(regex)) {\n const [, paramName, paramType, rawValue] = match;\n const formatted = formatSampleValue(paramType, rawValue);\n // A value that fails type validation is dropped, not substituted: the param\n // then falls back to the safe type-based placeholder during DESCRIBE.\n if (formatted !== null) {\n defaults[paramName] = formatted;\n }\n }\n return defaults;\n}\n\n/**\n * Replace `:param` placeholders with describe-time literals so `DESCRIBE QUERY`\n * can run without bound parameters. Resolution order per parameter:\n * 1. An explicit `-- @param name TYPE = value` sample value (wins), which lets\n * dynamic table names via `IDENTIFIER(...)` resolve to a real table.\n * 2. Otherwise a placeholder default derived from the annotated/inferred type.\n * Placeholders inside string literals or comments are left untouched.\n */\nexport function substituteParametersForDescribe(sql: string): string {\n const protectedRanges = getProtectedRanges(sql);\n const annotatedTypes = extractParameterTypes(sql);\n const inferredTypes = inferParameterTypes(sql, protectedRanges);\n const parameterTypes = { ...inferredTypes, ...annotatedTypes };\n const parameterDefaults = extractParameterDefaults(sql);\n return sql.replace(\n /(?<!:):([a-zA-Z_]\\w*)/g,\n (original, paramName, offset) => {\n if (isInsideProtectedRange(offset, protectedRanges)) {\n return original;\n }\n const sampleValue = parameterDefaults[paramName];\n if (sampleValue !== undefined) {\n return sampleValue;\n }\n return defaultForType(parameterTypes[paramName]);\n },\n );\n}\n\n/**\n * Append a remediation hint when a DESCRIBE failure looks like a dynamic\n * identifier that couldn't be resolved: the query calls `IDENTIFIER(...)` and\n * has at least one parameter without a describe-time sample value. These fail\n * because typegen substitutes a placeholder default (e.g. `''`) that yields a\n * malformed or non-existent table name. Steering the user to the `= value`\n * annotation turns the fatal error into a one-line fix.\n */\nfunction withIdentifierHint(\n error: { code?: string; message: string },\n sql: string,\n): { code?: string; message: string } {\n if (!/\\bIDENTIFIER\\s*\\(/i.test(sql)) {\n return error;\n }\n const protectedRanges = getProtectedRanges(sql);\n const params = extractParameters(sql, protectedRanges);\n const defaults = extractParameterDefaults(sql);\n const unresolved = params.filter(\n (p) => !SERVER_INJECTED_PARAMS.includes(p) && defaults[p] === undefined,\n );\n if (unresolved.length === 0) {\n return error;\n }\n const example = unresolved[0];\n return {\n ...error,\n message: `${error.message}\\n Hint: this query uses IDENTIFIER() with parameter(s) ${unresolved\n .map((p) => `:${p}`)\n .join(\n \", \",\n )}. Give type generation a sample value so it can resolve the table, e.g. \\`-- @param ${example} STRING = my_catalog\\`. The runtime query still binds the real parameter.`,\n };\n}\n\n/**\n * Infer parameter types from positional context in SQL.\n * V1 only infers NUMERIC from patterns like LIMIT, OFFSET, TOP,\n * FETCH FIRST ... ROWS, and arithmetic operators.\n * Parameters inside string literals or SQL comments are ignored.\n */\nexport function inferParameterTypes(\n sql: string,\n ranges?: Array<[number, number]>,\n): Record<string, string> {\n const inferred: Record<string, string> = {};\n const protectedRanges = ranges ?? getProtectedRanges(sql);\n\n for (const pattern of NUMERIC_PATTERNS) {\n for (const match of sql.matchAll(pattern)) {\n if (!isInsideProtectedRange(match.index, protectedRanges)) {\n inferred[match[1]] = \"NUMERIC\";\n }\n }\n }\n\n return inferred;\n}\n\n/**\n * Generate query schemas from a folder of SQL files\n * It uses DESCRIBE QUERY to get the schema without executing the query\n * @param queryFolder - the folder containing the SQL files\n * @param warehouseId - the warehouse id to use for schema analysis\n * @param options - options for the query generation\n * @param options.noCache - if true, skip the cache and regenerate all types\n * @param options.mode - preflight policy: \"non-blocking\" never probes the\n * warehouse and never describes (emits cached/`unknown` types and returns\n * immediately), \"blocking\" waits for a starting warehouse and starts (then\n * waits for) a stopped one, treating only a deleted/deleting warehouse as\n * fatal. Defaults to \"non-blocking\".\n * @returns an array of query schemas\n */\nexport async function generateQueriesFromDescribe(\n queryFolder: string,\n warehouseId: string,\n options: {\n noCache?: boolean;\n concurrency?: number;\n mode?: PreflightMode;\n } = {},\n): Promise<QueryGenerationResult> {\n const {\n noCache = false,\n concurrency: rawConcurrency = 10,\n mode = \"non-blocking\",\n } = options;\n const concurrency =\n typeof rawConcurrency === \"number\" && Number.isFinite(rawConcurrency)\n ? Math.max(1, Math.floor(rawConcurrency))\n : 10;\n\n // read all query files and cache in parallel\n const [allFiles, cache] = await Promise.all([\n fs.readdir(queryFolder),\n noCache\n ? ({ version: CACHE_VERSION, queries: {} } as Awaited<\n ReturnType<typeof loadCache>\n >)\n : loadCache(),\n ]);\n\n const queryFiles = allFiles.filter((file) => file.endsWith(\".sql\"));\n logger.debug(\"Found %d SQL queries\", queryFiles.length);\n\n const client = new WorkspaceClient({});\n const spinner = new Spinner();\n\n // Read all SQL files in parallel\n const sqlContents = await Promise.all(\n queryFiles.map((file) => fs.readFile(path.join(queryFolder, file), \"utf8\")),\n );\n\n const startTime = performance.now();\n\n // Phase 1: Check cache, separate cached vs uncached\n const cachedResults: Array<{ index: number; schema: QuerySchema }> = [];\n const uncachedQueries: Array<{\n index: number;\n queryName: string;\n sql: string;\n sqlHash: string;\n cleanedSql: string;\n }> = [];\n const logEntries: Array<{\n queryName: string;\n status: \"HIT\" | \"MISS\";\n // Absent for clean hits/misses. \"syntax\" = bad SQL on a reachable warehouse;\n // \"connectivity\" = warehouse unreachable; \"empty\" = described but no columns;\n // \"fatal\" = non-SQL setup/request failure surfaced after .d.ts emission.\n kind?: \"syntax\" | \"connectivity\" | \"empty\" | \"fatal\";\n error?: { code?: string; message: string };\n }> = [];\n\n for (let i = 0; i < queryFiles.length; i++) {\n const file = queryFiles[i];\n const rawName = path.basename(file, \".sql\");\n const queryName = normalizeQueryName(rawName);\n\n const sql = sqlContents[i];\n const sqlHash = hashSQL(sql);\n\n const cached = cache.queries[queryName];\n if (cached && cached.hash === sqlHash && !cached.retry) {\n cachedResults.push({\n index: i,\n schema: { name: queryName, type: cached.type },\n });\n logEntries.push({ queryName, status: \"HIT\" });\n } else {\n const protectedRanges = getProtectedRanges(sql);\n const annotatedTypes = extractParameterTypes(sql);\n const inferredTypes = inferParameterTypes(sql, protectedRanges);\n const parameterTypes = { ...inferredTypes, ...annotatedTypes };\n // substituteParametersForDescribe applies `-- @param name TYPE = value`\n // sample values (e.g. for IDENTIFIER table names) ahead of type defaults;\n // parameterDefaults is recomputed here only to skip them in the warn loop.\n const parameterDefaults = extractParameterDefaults(sql);\n const sqlWithDefaults = substituteParametersForDescribe(sql);\n\n // Warn about unresolved parameters\n const allParams = extractParameters(sql, protectedRanges);\n for (const param of allParams) {\n if (SERVER_INJECTED_PARAMS.includes(param)) continue;\n if (parameterDefaults[param]) continue;\n if (parameterTypes[param]) continue;\n logger.warn(\n '%s: parameter \":%s\" has no type annotation or inference. Add %s to the query file.',\n queryFiles[i],\n param,\n `-- @param ${param} <TYPE>`,\n );\n }\n\n const cleanedSql = sqlWithDefaults.trim().replace(/;\\s*$/, \"\");\n uncachedQueries.push({ index: i, queryName, sql, sqlHash, cleanedSql });\n }\n }\n\n // Phase 2: Execute all uncached DESCRIBE calls in parallel\n type DescribeResult =\n | {\n // Described successfully with a result schema — the only case we cache.\n status: \"ok\";\n index: number;\n schema: QuerySchema;\n cacheEntry: { hash: string; type: string; retry: boolean };\n }\n | {\n // Reachable warehouse ran DESCRIBE and rejected the statement — a\n // genuine SQL error. Eligible to fail the build; never cached.\n status: \"syntax\";\n index: number;\n schema: QuerySchema;\n error: { code?: string; message: string };\n }\n | {\n // DESCRIBE succeeded but returned no columns — soft `unknown`. Not a\n // failure, not cached, retried next run.\n status: \"empty\";\n index: number;\n schema: QuerySchema;\n }\n | {\n // Warehouse reachable but returned a non-terminal state (PENDING/\n // RUNNING) with no rows — stopped/cold-starting/busy. Degrade like a\n // transient outage (reuse cache or `unknown`); not cached, not empty.\n status: \"unavailable\";\n index: number;\n schema: QuerySchema;\n };\n\n const freshResults: Array<{ index: number; schema: QuerySchema }> = [];\n // Genuine SQL errors (reachable warehouse). Connectivity failures are NOT\n // recorded here — they degrade silently so a transient outage isn't fatal.\n const syntaxErrors: QuerySyntaxError[] = [];\n const fatalErrors: QueryFatalError[] = [];\n\n if (uncachedQueries.length > 0) {\n // One-time warehouse preflight (before issuing any DESCRIBE). A single\n // warehouses.get classifies the warehouse so we can skip the whole describe\n // batch when it can't serve this run, instead of letting every query fail\n // (and re-fail next run). Reuses this file's degrade/classify helpers so a\n // not-ready warehouse degrades exactly like a per-query outage.\n let decision: ReturnType<typeof decidePreflight> = \"proceed\";\n let fatalMessage = \"\";\n if (mode === \"non-blocking\") {\n // `non-blocking` never describes and must make ZERO warehouse round-trips:\n // skip the probe entirely (no getWarehouseState) and go straight to\n // degradeAll. A foreground/one-shot run can't describe in the background,\n // so it emits best-available types (reused cache or `unknown`) and returns\n // now.\n decision = \"degradeAll\";\n } else {\n try {\n const state = await getWarehouseState(client, warehouseId);\n decision = decidePreflight(state, mode);\n if (decision === \"fatal\") {\n fatalMessage = `warehouse ${warehouseId} is ${state}`;\n }\n if (decision === \"startWaitProceed\") {\n // Stopped/stopping warehouse: nudge it out of the stopped state, then\n // poll to RUNNING. treatStoppedAsTransient rides out the stale\n // pre-start STOPPED/STOPPING reading the start hasn't propagated past\n // yet — only DELETED/DELETING (or the deadline) ends the wait early.\n await startWarehouse(client, warehouseId);\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: PREFLIGHT_WAIT_MAX_MS,\n treatStoppedAsTransient: true,\n });\n if (final === \"RUNNING\") {\n decision = \"proceed\";\n } else {\n decision = \"fatal\";\n fatalMessage = `warehouse ${warehouseId} did not reach RUNNING (now ${final})`;\n }\n }\n if (decision === \"waitThenProceed\") {\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: PREFLIGHT_WAIT_MAX_MS,\n });\n if (final === \"RUNNING\") {\n decision = \"proceed\";\n } else {\n decision = \"fatal\";\n fatalMessage = `warehouse ${warehouseId} did not reach RUNNING (now ${final})`;\n }\n }\n } catch (err) {\n if (isConnectivityError(err)) {\n // Warehouse unreachable (transient outage): degrade silently like a\n // per-query connectivity failure — never fail a build on a blip.\n decision = \"degradeAll\";\n } else {\n // Auth, bad warehouse id, malformed config, or a timed-out wait: fatal.\n decision = \"fatal\";\n fatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n }\n }\n\n if (decision !== \"proceed\") {\n // degradeAll or fatal: skip DESCRIBE entirely. Every uncached query gets a\n // degraded schema (reused cache or `unknown`); fatal additionally records\n // a fatalError per query so the caller fails the build after writing.\n const kind = decision === \"fatal\" ? \"fatal\" : \"connectivity\";\n for (const { index, queryName, sql, sqlHash } of uncachedQueries) {\n freshResults.push({\n index,\n schema: {\n name: queryName,\n type: degradedType(cache, queryName, sql, sqlHash),\n },\n });\n if (decision === \"fatal\") {\n fatalErrors.push({ name: queryName, message: fatalMessage });\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind,\n error: { message: fatalMessage },\n });\n } else {\n logEntries.push({ queryName, status: \"MISS\", kind });\n }\n }\n } else {\n let completed = 0;\n const total = uncachedQueries.length;\n spinner.start(\n `Describing ${total} ${total === 1 ? \"query\" : \"queries\"} (0/${total})`,\n );\n\n // Shared across this run's DESCRIBE QUERY calls: discover the warehouse's\n // result format once, then reuse it.\n const describeFormat: DescribeFormatMemo = {};\n\n const describeOne = async ({\n index,\n queryName,\n sql,\n sqlHash,\n cleanedSql,\n }: (typeof uncachedQueries)[number]): Promise<DescribeResult> => {\n // describeAdaptive negotiates the result format with the warehouse\n // (JSON_ARRAY for standard DBSQL, ARROW_STREAM for Reyden) and returns\n // an already-normalized response, so the state classification below\n // reads it directly.\n const result = await describeAdaptive(\n client,\n `DESCRIBE QUERY ${cleanedSql}`,\n warehouseId,\n describeFormat,\n );\n\n completed++;\n spinner.update(\n `Describing ${total} ${total === 1 ? \"query\" : \"queries\"} (${completed}/${total})`,\n );\n\n logger.debug(\n \"DESCRIBE result for %s: state=%s, rows=%d, hasAttachment=%s\",\n queryName,\n result.status.state,\n result.result?.data_array?.length ?? 0,\n !!result.result?.attachment,\n );\n\n if (result.status.state === \"FAILED\") {\n // The warehouse was reachable and ran DESCRIBE, but the statement\n // failed — a genuine SQL error (bad table, syntax, incompatible type).\n const sqlError =\n result.status.error?.message || \"Query execution failed\";\n // The failure is surfaced once, formatted, by the aggregated\n // TypegenSyntaxError (and the summary table) — don't also log the raw\n // message here or every SQL error prints twice in dev.\n const type = generateUnknownResultQuery(sql, queryName);\n return {\n status: \"syntax\",\n index,\n schema: { name: queryName, type },\n error: withIdentifierHint(parseError(sqlError), sql),\n };\n }\n\n if (result.status.state !== \"SUCCEEDED\") {\n // Non-terminal state (PENDING/RUNNING) with no result rows: the\n // warehouse is reachable but not ready (stopped, cold-starting, or\n // busy). Degrade like a transient outage — reuse the last-good cached\n // type when the SQL is unchanged, else emit `unknown`. Never \"empty\":\n // treating this as empty would emit `result: unknown` AND discard the\n // good cached type, silently throwing away working types.\n return {\n status: \"unavailable\",\n index,\n schema: {\n name: queryName,\n type: degradedType(cache, queryName, sql, sqlHash),\n },\n };\n }\n\n const { type, hasResults } = convertToQueryType(result, sql, queryName);\n if (!hasResults) {\n // Described, but no result columns. Emit `unknown` and retry next run;\n // do not cache (we never persist `result: unknown`).\n return { status: \"empty\", index, schema: { name: queryName, type } };\n }\n return {\n status: \"ok\",\n index,\n schema: { name: queryName, type },\n cacheEntry: { hash: sqlHash, type, retry: false },\n };\n };\n\n // Process in chunks, saving cache after each chunk\n const processBatchResults = (\n settled: PromiseSettledResult<DescribeResult>[],\n batchOffset: number,\n ) => {\n for (let i = 0; i < settled.length; i++) {\n const entry = settled[i];\n const { queryName } = uncachedQueries[batchOffset + i];\n\n if (entry.status === \"fulfilled\") {\n const res = entry.value;\n freshResults.push({ index: res.index, schema: res.schema });\n\n if (res.status === \"ok\") {\n // Only a successful describe with a result schema is cached.\n cache.queries[queryName] = res.cacheEntry;\n logEntries.push({ queryName, status: \"MISS\" });\n } else if (res.status === \"syntax\") {\n // Genuine SQL error — record it for the caller's prod/dev gate.\n // Not cached: re-described next run so a fixed query recovers.\n syntaxErrors.push({\n name: queryName,\n message: res.error.message,\n });\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind: \"syntax\",\n error: res.error,\n });\n } else if (res.status === \"empty\") {\n // status === \"empty\": described, no columns. Soft unknown, not cached.\n logEntries.push({ queryName, status: \"MISS\", kind: \"empty\" });\n } else {\n // status === \"unavailable\": non-terminal DESCRIBE (warehouse\n // stopped/cold-starting/busy). Degrade like a transient outage:\n // tag OFFLINE, count as degraded, never cache.\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind: \"connectivity\",\n });\n }\n } else {\n // executeStatement rejected without a normal StatementExecution result.\n // Only structured transport/connectivity failures are treated as\n // offline; auth, bad warehouse IDs, malformed requests, and SDK/config\n // failures stay fatal so users fix the underlying setup issue.\n completed++;\n spinner.update(\n `Describing ${total} ${total === 1 ? \"query\" : \"queries\"} (${completed}/${total})`,\n );\n\n const { sql, sqlHash, index } = uncachedQueries[batchOffset + i];\n const reason = getErrorDiagnostic(entry.reason);\n const error = parseError(reason);\n const priorEntry = cache.queries[queryName];\n const canReusePrior =\n priorEntry?.hash === sqlHash && !priorEntry.retry;\n const type = degradedType(cache, queryName, sql, sqlHash);\n freshResults.push({ index, schema: { name: queryName, type } });\n\n if (!isConnectivityError(entry.reason)) {\n fatalErrors.push({ name: queryName, message: error.message });\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind: \"fatal\",\n error,\n });\n continue;\n }\n\n logger.warn(\n \"DESCRIBE unreachable for %s: %s — %s\",\n queryName,\n reason,\n canReusePrior\n ? \"reusing last cached type\"\n : \"emitting unknown (no matching cache)\",\n );\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind: \"connectivity\",\n error,\n });\n }\n }\n };\n\n if (uncachedQueries.length > concurrency) {\n for (let b = 0; b < uncachedQueries.length; b += concurrency) {\n const batch = uncachedQueries.slice(b, b + concurrency);\n const batchResults = await Promise.allSettled(batch.map(describeOne));\n processBatchResults(batchResults, b);\n await saveCache(cache);\n }\n } else {\n const settled = await Promise.allSettled(\n uncachedQueries.map(describeOne),\n );\n processBatchResults(settled, 0);\n await saveCache(cache);\n }\n\n spinner.stop(\"\");\n }\n }\n\n const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);\n\n // Print formatted table\n if (logEntries.length > 0) {\n const maxNameLen = Math.max(...logEntries.map((e) => e.queryName.length));\n const separator = pc.dim(\"─\".repeat(50));\n console.log(\"\");\n console.log(\n ` ${pc.bold(\"Typegen Queries\")} ${pc.dim(`(${logEntries.length})`)}`,\n );\n console.log(` ${separator}`);\n for (const entry of logEntries) {\n let tag: string;\n switch (entry.kind) {\n case \"syntax\":\n tag = pc.bold(pc.red(\"SQL ERR\"));\n break;\n case \"connectivity\":\n tag = pc.bold(pc.yellow(\"OFFLINE\"));\n break;\n case \"empty\":\n tag = pc.dim(\"EMPTY \");\n break;\n case \"fatal\":\n tag = pc.bold(pc.red(\"FATAL \"));\n break;\n default:\n tag =\n entry.status === \"HIT\"\n ? `cache ${pc.bold(pc.green(\"HIT \"))}`\n : `cache ${pc.bold(pc.yellow(\"MISS \"))}`;\n }\n const rawName = entry.queryName.padEnd(maxNameLen);\n // Only genuine SQL errors are struck through. Connectivity/empty kept a\n // usable type (reused or unknown), so they read as degraded, not broken.\n const name =\n entry.kind === \"syntax\" || entry.kind === \"fatal\"\n ? pc.dim(pc.strikethrough(rawName))\n : rawName;\n const errorCode = entry.error?.message.match(/\\[([^\\]]+)\\]/)?.[1];\n const reason = errorCode ? ` ${pc.dim(errorCode)}` : \"\";\n console.log(` ${tag} ${name}${reason}`);\n }\n const newCount = logEntries.filter(\n (e) => e.status === \"MISS\" && !e.kind,\n ).length;\n const cacheCount = logEntries.filter((e) => e.status === \"HIT\").length;\n const syntaxCount = logEntries.filter((e) => e.kind === \"syntax\").length;\n const offlineCount = logEntries.filter(\n (e) => e.kind === \"connectivity\",\n ).length;\n const emptyCount = logEntries.filter((e) => e.kind === \"empty\").length;\n const fatalCount = logEntries.filter((e) => e.kind === \"fatal\").length;\n console.log(` ${separator}`);\n const parts = [`${newCount} new`, `${cacheCount} from cache`];\n if (syntaxCount > 0)\n parts.push(\n `${syntaxCount} SQL ${syntaxCount === 1 ? \"error\" : \"errors\"}`,\n );\n if (offlineCount > 0) parts.push(`${offlineCount} degraded`);\n if (emptyCount > 0) parts.push(`${emptyCount} empty`);\n if (fatalCount > 0)\n parts.push(\n `${fatalCount} fatal ${fatalCount === 1 ? \"error\" : \"errors\"}`,\n );\n console.log(` ${parts.join(\", \")}. ${pc.dim(`${elapsed}s`)}`);\n console.log(\"\");\n }\n\n // Merge and sort by original file index for deterministic output\n const schemas = [...cachedResults, ...freshResults]\n .sort((a, b) => a.index - b.index)\n .map((r) => r.schema);\n\n return { schemas, syntaxErrors, fatalErrors };\n}\n\n/**\n * Normalize query name by removing the .obo extension\n * @param queryName - the query name to normalize\n * @returns the normalized query name\n */\nfunction normalizeQueryName(fileName: string): string {\n return fileName.replace(/\\.obo$/, \"\");\n}\n\n/**\n * Normalize SQL type name by removing parameters/generics\n * Examples:\n * DECIMAL(38,6) -> DECIMAL\n * ARRAY<STRING> -> ARRAY\n * MAP<STRING,INT> -> MAP\n * STRUCT<name:STRING> -> STRUCT\n * INTERVAL DAY TO SECOND -> INTERVAL\n * GEOGRAPHY(4326) -> GEOGRAPHY\n */\nexport function normalizeTypeName(typeName: string): string {\n return typeName\n .replace(/\\(.*\\)$/, \"\") // remove (p, s) eg: DECIMAL(38,6) -> DECIMAL\n .replace(/<.*>$/, \"\") // remove <T> eg: ARRAY<STRING> -> ARRAY\n .split(\" \")[0]; // take first word eg: INTERVAL DAY TO SECOND -> INTERVAL\n}\n\n/** Type Map for Databricks data types to JavaScript types */\nconst typeMap: Record<string, string> = {\n // string types\n STRING: \"string\",\n BINARY: \"string\",\n // boolean\n BOOLEAN: \"boolean\",\n // numeric types\n TINYINT: \"number\",\n SMALLINT: \"number\",\n INT: \"number\",\n BIGINT: \"number\",\n FLOAT: \"number\",\n DOUBLE: \"number\",\n DECIMAL: \"number\",\n // date/time types\n DATE: \"string\",\n TIMESTAMP: \"string\",\n TIMESTAMP_NTZ: \"string\",\n INTERVAL: \"string\",\n // complex types\n ARRAY: \"unknown[]\",\n MAP: \"Record<string, unknown>\",\n STRUCT: \"Record<string, unknown>\",\n OBJECT: \"Record<string, unknown>\",\n VARIANT: \"unknown\",\n // spatial types\n GEOGRAPHY: \"unknown\",\n GEOMETRY: \"unknown\",\n // null type\n VOID: \"null\",\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"query-registry.js","names":[],"sources":["../../src/type-generator/query-registry.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { tableFromIPC } from \"apache-arrow\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport { createWorkspaceClient } from \"../workspace-client\";\nimport { CACHE_VERSION, hashSQL, loadCache, saveCache } from \"./cache\";\nimport { getErrorDiagnostic, isConnectivityError } from \"./errors\";\nimport { decidePreflight, type PreflightMode } from \"./preflight\";\nimport { Spinner } from \"./spinner\";\nimport { type DescribeFormatMemo, describeAdaptive } from \"./statement-result\";\nimport {\n type DatabricksStatementExecutionResponse,\n type QueryFatalError,\n type QueryGenerationResult,\n type QuerySchema,\n type QuerySyntaxError,\n sqlTypeToHelper,\n sqlTypeToMarker,\n} from \"./types\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:query-registry\");\n\n/**\n * Upper bound on how long a `blocking`-mode preflight will wait for a starting\n * warehouse to reach RUNNING before giving up (~5 min). Generous enough to ride\n * out a cold start without hanging an interactive CLI invocation indefinitely.\n */\nconst PREFLIGHT_WAIT_MAX_MS = 300_000;\n\n/**\n * Regex breakdown:\n * '(?:[^']|'')*' — matches a SQL string literal, including escaped '' pairs\n * | — alternation: whichever branch matches first at a position wins\n * --[^\\n]* — matches a single-line SQL comment\n *\n * Because the regex engine scans left-to-right, a `'` is consumed as a string\n * literal before any `--` inside it could match as a comment — giving us\n * correct single-pass ordering without a manual state machine.\n *\n * V1: no block-comment support (deferred to next PR).\n */\nconst PROTECTED_RANGE_RE = /'(?:[^']|'')*'|--[^\\n]*/g;\n\n/**\n * Numeric-context patterns for positional type inference.\n * Hoisted to module scope — safe because matchAll() clones the regex internally.\n */\nconst NUMERIC_PATTERNS: RegExp[] = [\n /\\bLIMIT\\s+:([a-zA-Z_]\\w*)/gi,\n /\\bOFFSET\\s+:([a-zA-Z_]\\w*)/gi,\n /\\bTOP\\s+:([a-zA-Z_]\\w*)/gi,\n /\\bFETCH\\s+FIRST\\s+:([a-zA-Z_]\\w*)\\s+ROWS/gi,\n // V1 limitation: arithmetic operators may false-positive for date\n // expressions like `:start_date - INTERVAL '1 day'`. A smarter\n // heuristic (e.g. look-ahead for INTERVAL) is deferred to a future PR.\n /[+\\-*/]\\s*:([a-zA-Z_]\\w*)/g,\n /:([a-zA-Z_]\\w*)\\s*[+\\-*/]/g,\n];\n\nexport function getProtectedRanges(sql: string): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n for (const m of sql.matchAll(PROTECTED_RANGE_RE)) {\n ranges.push([m.index, m.index + m[0].length]);\n }\n return ranges;\n}\n\nfunction isInsideProtectedRange(\n offset: number,\n ranges: Array<[number, number]>,\n): boolean {\n return ranges.some(([start, end]) => offset >= start && offset < end);\n}\n\n/**\n * Parse a raw API/SDK error into a structured code + message.\n * Handles Databricks-style JSON bodies embedded in the message string,\n * e.g. `Response from server (Bad Request) {\"error_code\":\"...\",\"message\":\"...\"}`.\n */\nfunction parseError(raw: string): { code?: string; message: string } {\n const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n try {\n const parsed = JSON.parse(jsonMatch[0]);\n if (parsed.error_code || parsed.message) {\n return {\n code: parsed.error_code,\n message: parsed.message || raw,\n };\n }\n } catch {\n // not valid JSON, fall through\n }\n }\n return { message: raw };\n}\n\n/**\n * Extract parameters from a SQL query\n * @param sql - the SQL query to extract parameters from\n * @returns an array of parameter names\n */\nexport function extractParameters(\n sql: string,\n ranges?: Array<[number, number]>,\n): string[] {\n const protectedRanges = ranges ?? getProtectedRanges(sql);\n const matches = sql.matchAll(/(?<!:):([a-zA-Z_]\\w*)/g);\n const params = new Set<string>();\n for (const match of matches) {\n if (!isInsideProtectedRange(match.index, protectedRanges)) {\n params.add(match[1]);\n }\n }\n return Array.from(params);\n}\n\n// parameters that are injected by the server\nexport const SERVER_INJECTED_PARAMS = [\"workspaceId\"];\n\n/**\n * Generates the TypeScript type literal for query parameters from SQL.\n * Shared by both the success and failure paths.\n */\nfunction formatParametersType(sql: string): string {\n const params = extractParameters(sql).filter(\n (p) => !SERVER_INJECTED_PARAMS.includes(p),\n );\n const paramTypes = extractParameterTypes(sql);\n\n return params.length > 0\n ? `{\\n ${params\n .map((p) => {\n const sqlType = paramTypes[p];\n const markerType = sqlType\n ? sqlTypeToMarker[sqlType]\n : \"SQLTypeMarker\";\n const helper = sqlType ? sqlTypeToHelper[sqlType] : \"sql.*()\";\n return `/** ${sqlType || \"any\"} - use ${helper} */\\n ${p}: ${markerType}`;\n })\n .join(\";\\n \")};\\n }`\n : \"Record<string, never>\";\n}\n\n/**\n * Decode a base64 Arrow IPC attachment from a DESCRIBE QUERY response and\n * extract column metadata. Returns the same shape as rows parsed from the\n * legacy data_array path.\n *\n * IMPORTANT: a DESCRIBE QUERY response is itself a result *table* with rows\n * shaped like `(col_name, data_type, comment)` describing the user query's\n * output schema. We must read those rows — NOT `table.schema.fields`, which\n * would describe DESCRIBE QUERY's own output (`col_name`, `data_type`,\n * `comment`) and yield bogus types for every query.\n */\nfunction columnsFromArrowAttachment(\n attachment: string,\n): Array<{ name: string; type_name: string; comment: string | undefined }> {\n const buf = Buffer.from(attachment, \"base64\");\n const table = tableFromIPC(buf);\n return table.toArray().map((row) => {\n const obj = row.toJSON() as {\n col_name?: unknown;\n data_type?: unknown;\n comment?: unknown;\n };\n return {\n name: typeof obj.col_name === \"string\" ? obj.col_name : \"\",\n type_name:\n typeof obj.data_type === \"string\"\n ? obj.data_type.toUpperCase()\n : \"STRING\",\n comment:\n typeof obj.comment === \"string\" && obj.comment !== \"\"\n ? obj.comment\n : undefined,\n };\n });\n}\n\nexport function convertToQueryType(\n result: DatabricksStatementExecutionResponse,\n sql: string,\n queryName: string,\n): { type: string; hasResults: boolean } {\n const dataRows = result.result?.data_array || [];\n let columns = dataRows.map((row) => ({\n name: row[0] || \"\",\n type_name: row[1]?.toUpperCase() || \"STRING\",\n comment: row[2] || undefined,\n }));\n\n // Fallback: serverless warehouses return ARROW_STREAM format with an inline\n // base64 attachment instead of data_array. Decode the Arrow IPC rows (the\n // DESCRIBE QUERY result table) to extract column names and types.\n if (columns.length === 0 && result.result?.attachment) {\n logger.debug(\"data_array empty, decoding Arrow IPC attachment for schema\");\n try {\n columns = columnsFromArrowAttachment(result.result.attachment);\n } catch (err) {\n logger.warn(\n \"Failed to decode Arrow IPC attachment: %s\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n const paramsType = formatParametersType(sql);\n\n // generate result fields with JSDoc\n const resultFields = columns.map((column) => {\n const normalizedType = normalizeTypeName(column.type_name);\n const mappedType = typeMap[normalizedType] || \"unknown\";\n // validate column name is a valid identifier\n const name = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(column.name)\n ? column.name\n : `\"${column.name}\"`;\n\n // generate comment for column\n const comment = column.comment\n ? `/** ${column.comment.replace(/\\*\\//g, \"* /\")} */\\n `\n : `/** @sqlType ${column.type_name.replace(/\\*\\//g, \"* /\")} */\\n `;\n\n return `${comment}${name}: ${mappedType}`;\n });\n\n const hasResults = resultFields.length > 0;\n\n const type = `{\n name: \"${queryName}\";\n parameters: ${paramsType};\n result: ${\n hasResults\n ? `Array<{\n ${resultFields.join(\";\\n \")};\n }>`\n : \"unknown\"\n };\n }`;\n\n return { type, hasResults };\n}\n\n/**\n * Used when DESCRIBE QUERY fails so the query still appears in QueryRegistry.\n * Generates a type with unknown result from SQL alone (no warehouse call).\n */\nfunction generateUnknownResultQuery(sql: string, queryName: string): string {\n const paramsType = formatParametersType(sql);\n\n return `{\n name: \"${queryName}\";\n parameters: ${paramsType};\n result: unknown;\n }`;\n}\n\n/**\n * Degrade gracefully when DESCRIBE can't produce a fresh schema (transient\n * connectivity outage, or a warehouse that's reachable but not ready). Reuse\n * the last-good cached type when the SQL hash is unchanged, otherwise emit\n * `unknown` from SQL alone. Never persists `result: unknown`.\n */\nfunction degradedType(\n cache: Awaited<ReturnType<typeof loadCache>>,\n queryName: string,\n sql: string,\n sqlHash: string,\n): string {\n const prior = cache.queries[queryName];\n const canReusePrior = prior?.hash === sqlHash && !prior.retry;\n return canReusePrior\n ? prior.type\n : generateUnknownResultQuery(sql, queryName);\n}\n\n// Single source of truth for the `@param` type alternation, shared by\n// extractParameterTypes and extractParameterDefaults so the two can't drift.\n// Alternation order matters: TIMESTAMP_NTZ must precede TIMESTAMP so the regex\n// engine doesn't greedy-match TIMESTAMP and leave `_NTZ` unconsumed.\nconst PARAM_TYPE_ALTERNATION =\n \"STRING|NUMERIC|DECIMAL|BIGINT|TINYINT|SMALLINT|INT|FLOAT|DOUBLE|BOOLEAN|DATE|TIMESTAMP_NTZ|TIMESTAMP|BINARY\";\n\nexport function extractParameterTypes(sql: string): Record<string, string> {\n const paramTypes: Record<string, string> = {};\n const regex = new RegExp(\n `--\\\\s*@param\\\\s+(\\\\w+)\\\\s+(${PARAM_TYPE_ALTERNATION})\\\\b`,\n \"gi\",\n );\n const matches = sql.matchAll(regex);\n for (const match of matches) {\n const [, paramName, paramType] = match;\n paramTypes[paramName] = paramType.toUpperCase();\n }\n\n return paramTypes;\n}\n\nexport function defaultForType(sqlType: string | undefined): string {\n switch (sqlType?.toUpperCase()) {\n case \"NUMERIC\":\n case \"DECIMAL\":\n case \"BIGINT\":\n case \"TINYINT\":\n case \"SMALLINT\":\n case \"INT\":\n return \"0\";\n case \"FLOAT\":\n case \"DOUBLE\":\n return \"0.0\";\n case \"STRING\":\n return \"''\";\n case \"BOOLEAN\":\n return \"true\";\n case \"DATE\":\n return \"'2000-01-01'\";\n case \"TIMESTAMP\":\n return \"'2000-01-01T00:00:00Z'\";\n case \"TIMESTAMP_NTZ\":\n return \"'2000-01-01T00:00:00'\";\n case \"BINARY\":\n return \"X'00'\";\n default:\n return \"''\";\n }\n}\n\n/**\n * True when `raw` is already a single, well-formed SQL single-quoted string\n * literal — i.e. it opens and closes with `'` and every interior quote is part\n * of an escaped `''` pair. `'2024-01-01'` and `'O''Brien'` qualify;\n * `'a' OR 1=1 OR 'b'` does not (it has lone interior quotes), so it is treated\n * as raw content and re-escaped rather than trusted. A backslash also\n * disqualifies it: Databricks/Spark treats `\\` as an escape inside literals (so\n * `'a\\'` is unterminated), and we never trust such input — it is re-escaped.\n */\nfunction isWellFormedStringLiteral(raw: string): boolean {\n if (raw.length < 2 || !raw.startsWith(\"'\") || !raw.endsWith(\"'\")) {\n return false;\n }\n const inner = raw.slice(1, -1);\n if (inner.includes(\"\\\\\")) return false;\n return !inner.replace(/''/g, \"\").includes(\"'\");\n}\n\n/**\n * Format a user-supplied sample value as a SQL literal for substitution into\n * the build-time DESCRIBE statement. Returns `null` when the value isn't valid\n * for its type, so the caller falls back to the safe type-based placeholder\n * instead of substituting attacker-controllable text.\n *\n * The value comes from a `.sql` file that may be shared via a template or\n * dependency, so it must not be able to inject SQL into `DESCRIBE QUERY`:\n * - string-like types are always emitted as one well-formed, fully-escaped\n * single-quoted literal (a pre-quoted literal is kept as-is, anything else is\n * quoted with both `\\` and `'` doubled so neither can terminate the literal),\n * so the value can never break out of the string;\n * - numeric / boolean / binary values must match a strict literal shape and are\n * rejected (`null`) otherwise, rather than being passed through verbatim.\n */\nfunction formatSampleValue(\n sqlType: string | undefined,\n raw: string,\n): string | null {\n switch (sqlType?.toUpperCase()) {\n case \"STRING\":\n case \"DATE\":\n case \"TIMESTAMP\":\n case \"TIMESTAMP_NTZ\":\n return isWellFormedStringLiteral(raw)\n ? raw\n : `'${raw.replace(/\\\\/g, \"\\\\\\\\\").replace(/'/g, \"''\")}'`;\n case \"NUMERIC\":\n case \"DECIMAL\":\n case \"BIGINT\":\n case \"TINYINT\":\n case \"SMALLINT\":\n case \"INT\":\n case \"FLOAT\":\n case \"DOUBLE\":\n return /^[+-]?\\d+(\\.\\d+)?$/.test(raw) ? raw : null;\n case \"BOOLEAN\":\n return /^(?:true|false)$/i.test(raw) ? raw.toLowerCase() : null;\n case \"BINARY\":\n return /^X'[0-9a-fA-F]*'$/i.test(raw) ? raw : null;\n default:\n return null;\n }\n}\n\n/**\n * Parse optional describe-time sample values from `@param` annotations, e.g.\n * `-- @param target_catalog STRING = main`. The value is substituted into the\n * SQL **only during DESCRIBE QUERY** so type generation can resolve queries\n * whose shape depends on a parameter value — most notably dynamic table names\n * via `IDENTIFIER(:target_catalog || '.schema.table')`, where the empty-string\n * default would otherwise produce malformed SQL. Runtime binding is unaffected:\n * the analytics plugin still binds the real parameter at execution time, so the\n * query stays portable across environments.\n *\n * Returns a map of parameter name to the formatted SQL literal to substitute.\n */\nexport function extractParameterDefaults(sql: string): Record<string, string> {\n const defaults: Record<string, string> = {};\n // Reuses PARAM_TYPE_ALTERNATION, then requires `= <value>` on the same line.\n // All inter-token whitespace is horizontal-only (`[^\\S\\r\\n]`, not `\\s`): `\\s`\n // matches newlines, so a value-less line like `-- @param x STRING =` would let\n // `\\s*=\\s*(.+?)` swallow the *next* line as the sample value. Restricting to\n // same-line whitespace makes such a line simply not match, so it correctly\n // falls back to the type placeholder.\n const regex = new RegExp(\n `--[^\\\\S\\\\r\\\\n]*@param[^\\\\S\\\\r\\\\n]+(\\\\w+)[^\\\\S\\\\r\\\\n]+(${PARAM_TYPE_ALTERNATION})[^\\\\S\\\\r\\\\n]*=[^\\\\S\\\\r\\\\n]*(.+?)[^\\\\S\\\\r\\\\n]*$`,\n \"gim\",\n );\n for (const match of sql.matchAll(regex)) {\n const [, paramName, paramType, rawValue] = match;\n const formatted = formatSampleValue(paramType, rawValue);\n // A value that fails type validation is dropped, not substituted: the param\n // then falls back to the safe type-based placeholder during DESCRIBE.\n if (formatted !== null) {\n defaults[paramName] = formatted;\n }\n }\n return defaults;\n}\n\n/**\n * Replace `:param` placeholders with describe-time literals so `DESCRIBE QUERY`\n * can run without bound parameters. Resolution order per parameter:\n * 1. An explicit `-- @param name TYPE = value` sample value (wins), which lets\n * dynamic table names via `IDENTIFIER(...)` resolve to a real table.\n * 2. Otherwise a placeholder default derived from the annotated/inferred type.\n * Placeholders inside string literals or comments are left untouched.\n */\nexport function substituteParametersForDescribe(sql: string): string {\n const protectedRanges = getProtectedRanges(sql);\n const annotatedTypes = extractParameterTypes(sql);\n const inferredTypes = inferParameterTypes(sql, protectedRanges);\n const parameterTypes = { ...inferredTypes, ...annotatedTypes };\n const parameterDefaults = extractParameterDefaults(sql);\n return sql.replace(\n /(?<!:):([a-zA-Z_]\\w*)/g,\n (original, paramName, offset) => {\n if (isInsideProtectedRange(offset, protectedRanges)) {\n return original;\n }\n const sampleValue = parameterDefaults[paramName];\n if (sampleValue !== undefined) {\n return sampleValue;\n }\n return defaultForType(parameterTypes[paramName]);\n },\n );\n}\n\n/**\n * Append a remediation hint when a DESCRIBE failure looks like a dynamic\n * identifier that couldn't be resolved: the query calls `IDENTIFIER(...)` and\n * has at least one parameter without a describe-time sample value. These fail\n * because typegen substitutes a placeholder default (e.g. `''`) that yields a\n * malformed or non-existent table name. Steering the user to the `= value`\n * annotation turns the fatal error into a one-line fix.\n */\nfunction withIdentifierHint(\n error: { code?: string; message: string },\n sql: string,\n): { code?: string; message: string } {\n if (!/\\bIDENTIFIER\\s*\\(/i.test(sql)) {\n return error;\n }\n const protectedRanges = getProtectedRanges(sql);\n const params = extractParameters(sql, protectedRanges);\n const defaults = extractParameterDefaults(sql);\n const unresolved = params.filter(\n (p) => !SERVER_INJECTED_PARAMS.includes(p) && defaults[p] === undefined,\n );\n if (unresolved.length === 0) {\n return error;\n }\n const example = unresolved[0];\n return {\n ...error,\n message: `${error.message}\\n Hint: this query uses IDENTIFIER() with parameter(s) ${unresolved\n .map((p) => `:${p}`)\n .join(\n \", \",\n )}. Give type generation a sample value so it can resolve the table, e.g. \\`-- @param ${example} STRING = my_catalog\\`. The runtime query still binds the real parameter.`,\n };\n}\n\n/**\n * Infer parameter types from positional context in SQL.\n * V1 only infers NUMERIC from patterns like LIMIT, OFFSET, TOP,\n * FETCH FIRST ... ROWS, and arithmetic operators.\n * Parameters inside string literals or SQL comments are ignored.\n */\nexport function inferParameterTypes(\n sql: string,\n ranges?: Array<[number, number]>,\n): Record<string, string> {\n const inferred: Record<string, string> = {};\n const protectedRanges = ranges ?? getProtectedRanges(sql);\n\n for (const pattern of NUMERIC_PATTERNS) {\n for (const match of sql.matchAll(pattern)) {\n if (!isInsideProtectedRange(match.index, protectedRanges)) {\n inferred[match[1]] = \"NUMERIC\";\n }\n }\n }\n\n return inferred;\n}\n\n/**\n * Generate query schemas from a folder of SQL files\n * It uses DESCRIBE QUERY to get the schema without executing the query\n * @param queryFolder - the folder containing the SQL files\n * @param warehouseId - the warehouse id to use for schema analysis\n * @param options - options for the query generation\n * @param options.noCache - if true, skip the cache and regenerate all types\n * @param options.mode - preflight policy: \"non-blocking\" never probes the\n * warehouse and never describes (emits cached/`unknown` types and returns\n * immediately), \"blocking\" waits for a starting warehouse and starts (then\n * waits for) a stopped one, treating only a deleted/deleting warehouse as\n * fatal. Defaults to \"non-blocking\".\n * @returns an array of query schemas\n */\nexport async function generateQueriesFromDescribe(\n queryFolder: string,\n warehouseId: string,\n options: {\n noCache?: boolean;\n concurrency?: number;\n mode?: PreflightMode;\n } = {},\n): Promise<QueryGenerationResult> {\n const {\n noCache = false,\n concurrency: rawConcurrency = 10,\n mode = \"non-blocking\",\n } = options;\n const concurrency =\n typeof rawConcurrency === \"number\" && Number.isFinite(rawConcurrency)\n ? Math.max(1, Math.floor(rawConcurrency))\n : 10;\n\n // read all query files and cache in parallel\n const [allFiles, cache] = await Promise.all([\n fs.readdir(queryFolder),\n noCache\n ? ({ version: CACHE_VERSION, queries: {} } as Awaited<\n ReturnType<typeof loadCache>\n >)\n : loadCache(),\n ]);\n\n const queryFiles = allFiles.filter((file) => file.endsWith(\".sql\"));\n logger.debug(\"Found %d SQL queries\", queryFiles.length);\n\n const client = createWorkspaceClient();\n const spinner = new Spinner();\n\n // Read all SQL files in parallel\n const sqlContents = await Promise.all(\n queryFiles.map((file) => fs.readFile(path.join(queryFolder, file), \"utf8\")),\n );\n\n const startTime = performance.now();\n\n // Phase 1: Check cache, separate cached vs uncached\n const cachedResults: Array<{ index: number; schema: QuerySchema }> = [];\n const uncachedQueries: Array<{\n index: number;\n queryName: string;\n sql: string;\n sqlHash: string;\n cleanedSql: string;\n }> = [];\n const logEntries: Array<{\n queryName: string;\n status: \"HIT\" | \"MISS\";\n // Absent for clean hits/misses. \"syntax\" = bad SQL on a reachable warehouse;\n // \"connectivity\" = warehouse unreachable; \"empty\" = described but no columns;\n // \"fatal\" = non-SQL setup/request failure surfaced after .d.ts emission.\n kind?: \"syntax\" | \"connectivity\" | \"empty\" | \"fatal\";\n error?: { code?: string; message: string };\n }> = [];\n\n for (let i = 0; i < queryFiles.length; i++) {\n const file = queryFiles[i];\n const rawName = path.basename(file, \".sql\");\n const queryName = normalizeQueryName(rawName);\n\n const sql = sqlContents[i];\n const sqlHash = hashSQL(sql);\n\n const cached = cache.queries[queryName];\n if (cached && cached.hash === sqlHash && !cached.retry) {\n cachedResults.push({\n index: i,\n schema: { name: queryName, type: cached.type },\n });\n logEntries.push({ queryName, status: \"HIT\" });\n } else {\n const protectedRanges = getProtectedRanges(sql);\n const annotatedTypes = extractParameterTypes(sql);\n const inferredTypes = inferParameterTypes(sql, protectedRanges);\n const parameterTypes = { ...inferredTypes, ...annotatedTypes };\n // substituteParametersForDescribe applies `-- @param name TYPE = value`\n // sample values (e.g. for IDENTIFIER table names) ahead of type defaults;\n // parameterDefaults is recomputed here only to skip them in the warn loop.\n const parameterDefaults = extractParameterDefaults(sql);\n const sqlWithDefaults = substituteParametersForDescribe(sql);\n\n // Warn about unresolved parameters\n const allParams = extractParameters(sql, protectedRanges);\n for (const param of allParams) {\n if (SERVER_INJECTED_PARAMS.includes(param)) continue;\n if (parameterDefaults[param]) continue;\n if (parameterTypes[param]) continue;\n logger.warn(\n '%s: parameter \":%s\" has no type annotation or inference. Add %s to the query file.',\n queryFiles[i],\n param,\n `-- @param ${param} <TYPE>`,\n );\n }\n\n const cleanedSql = sqlWithDefaults.trim().replace(/;\\s*$/, \"\");\n uncachedQueries.push({ index: i, queryName, sql, sqlHash, cleanedSql });\n }\n }\n\n // Phase 2: Execute all uncached DESCRIBE calls in parallel\n type DescribeResult =\n | {\n // Described successfully with a result schema — the only case we cache.\n status: \"ok\";\n index: number;\n schema: QuerySchema;\n cacheEntry: { hash: string; type: string; retry: boolean };\n }\n | {\n // Reachable warehouse ran DESCRIBE and rejected the statement — a\n // genuine SQL error. Eligible to fail the build; never cached.\n status: \"syntax\";\n index: number;\n schema: QuerySchema;\n error: { code?: string; message: string };\n }\n | {\n // DESCRIBE succeeded but returned no columns — soft `unknown`. Not a\n // failure, not cached, retried next run.\n status: \"empty\";\n index: number;\n schema: QuerySchema;\n }\n | {\n // Warehouse reachable but returned a non-terminal state (PENDING/\n // RUNNING) with no rows — stopped/cold-starting/busy. Degrade like a\n // transient outage (reuse cache or `unknown`); not cached, not empty.\n status: \"unavailable\";\n index: number;\n schema: QuerySchema;\n };\n\n const freshResults: Array<{ index: number; schema: QuerySchema }> = [];\n // Genuine SQL errors (reachable warehouse). Connectivity failures are NOT\n // recorded here — they degrade silently so a transient outage isn't fatal.\n const syntaxErrors: QuerySyntaxError[] = [];\n const fatalErrors: QueryFatalError[] = [];\n\n if (uncachedQueries.length > 0) {\n // One-time warehouse preflight (before issuing any DESCRIBE). A single\n // warehouses.get classifies the warehouse so we can skip the whole describe\n // batch when it can't serve this run, instead of letting every query fail\n // (and re-fail next run). Reuses this file's degrade/classify helpers so a\n // not-ready warehouse degrades exactly like a per-query outage.\n let decision: ReturnType<typeof decidePreflight> = \"proceed\";\n let fatalMessage = \"\";\n if (mode === \"non-blocking\") {\n // `non-blocking` never describes and must make ZERO warehouse round-trips:\n // skip the probe entirely (no getWarehouseState) and go straight to\n // degradeAll. A foreground/one-shot run can't describe in the background,\n // so it emits best-available types (reused cache or `unknown`) and returns\n // now.\n decision = \"degradeAll\";\n } else {\n try {\n const state = await getWarehouseState(client, warehouseId);\n decision = decidePreflight(state, mode);\n if (decision === \"fatal\") {\n fatalMessage = `warehouse ${warehouseId} is ${state}`;\n }\n if (decision === \"startWaitProceed\") {\n // Stopped/stopping warehouse: nudge it out of the stopped state, then\n // poll to RUNNING. treatStoppedAsTransient rides out the stale\n // pre-start STOPPED/STOPPING reading the start hasn't propagated past\n // yet — only DELETED/DELETING (or the deadline) ends the wait early.\n await startWarehouse(client, warehouseId);\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: PREFLIGHT_WAIT_MAX_MS,\n treatStoppedAsTransient: true,\n });\n if (final === \"RUNNING\") {\n decision = \"proceed\";\n } else {\n decision = \"fatal\";\n fatalMessage = `warehouse ${warehouseId} did not reach RUNNING (now ${final})`;\n }\n }\n if (decision === \"waitThenProceed\") {\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: PREFLIGHT_WAIT_MAX_MS,\n });\n if (final === \"RUNNING\") {\n decision = \"proceed\";\n } else {\n decision = \"fatal\";\n fatalMessage = `warehouse ${warehouseId} did not reach RUNNING (now ${final})`;\n }\n }\n } catch (err) {\n if (isConnectivityError(err)) {\n // Warehouse unreachable (transient outage): degrade silently like a\n // per-query connectivity failure — never fail a build on a blip.\n decision = \"degradeAll\";\n } else {\n // Auth, bad warehouse id, malformed config, or a timed-out wait: fatal.\n decision = \"fatal\";\n fatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n }\n }\n\n if (decision !== \"proceed\") {\n // degradeAll or fatal: skip DESCRIBE entirely. Every uncached query gets a\n // degraded schema (reused cache or `unknown`); fatal additionally records\n // a fatalError per query so the caller fails the build after writing.\n const kind = decision === \"fatal\" ? \"fatal\" : \"connectivity\";\n for (const { index, queryName, sql, sqlHash } of uncachedQueries) {\n freshResults.push({\n index,\n schema: {\n name: queryName,\n type: degradedType(cache, queryName, sql, sqlHash),\n },\n });\n if (decision === \"fatal\") {\n fatalErrors.push({ name: queryName, message: fatalMessage });\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind,\n error: { message: fatalMessage },\n });\n } else {\n logEntries.push({ queryName, status: \"MISS\", kind });\n }\n }\n } else {\n let completed = 0;\n const total = uncachedQueries.length;\n spinner.start(\n `Describing ${total} ${total === 1 ? \"query\" : \"queries\"} (0/${total})`,\n );\n\n // Shared across this run's DESCRIBE QUERY calls: discover the warehouse's\n // result format once, then reuse it.\n const describeFormat: DescribeFormatMemo = {};\n\n const describeOne = async ({\n index,\n queryName,\n sql,\n sqlHash,\n cleanedSql,\n }: (typeof uncachedQueries)[number]): Promise<DescribeResult> => {\n // describeAdaptive negotiates the result format with the warehouse\n // (JSON_ARRAY for standard DBSQL, ARROW_STREAM for Reyden) and returns\n // an already-normalized response, so the state classification below\n // reads it directly.\n const result = await describeAdaptive(\n client,\n `DESCRIBE QUERY ${cleanedSql}`,\n warehouseId,\n describeFormat,\n );\n\n completed++;\n spinner.update(\n `Describing ${total} ${total === 1 ? \"query\" : \"queries\"} (${completed}/${total})`,\n );\n\n logger.debug(\n \"DESCRIBE result for %s: state=%s, rows=%d, hasAttachment=%s\",\n queryName,\n result.status.state,\n result.result?.data_array?.length ?? 0,\n !!result.result?.attachment,\n );\n\n if (result.status.state === \"FAILED\") {\n // The warehouse was reachable and ran DESCRIBE, but the statement\n // failed — a genuine SQL error (bad table, syntax, incompatible type).\n const sqlError =\n result.status.error?.message || \"Query execution failed\";\n // The failure is surfaced once, formatted, by the aggregated\n // TypegenSyntaxError (and the summary table) — don't also log the raw\n // message here or every SQL error prints twice in dev.\n const type = generateUnknownResultQuery(sql, queryName);\n return {\n status: \"syntax\",\n index,\n schema: { name: queryName, type },\n error: withIdentifierHint(parseError(sqlError), sql),\n };\n }\n\n if (result.status.state !== \"SUCCEEDED\") {\n // Non-terminal state (PENDING/RUNNING) with no result rows: the\n // warehouse is reachable but not ready (stopped, cold-starting, or\n // busy). Degrade like a transient outage — reuse the last-good cached\n // type when the SQL is unchanged, else emit `unknown`. Never \"empty\":\n // treating this as empty would emit `result: unknown` AND discard the\n // good cached type, silently throwing away working types.\n return {\n status: \"unavailable\",\n index,\n schema: {\n name: queryName,\n type: degradedType(cache, queryName, sql, sqlHash),\n },\n };\n }\n\n const { type, hasResults } = convertToQueryType(result, sql, queryName);\n if (!hasResults) {\n // Described, but no result columns. Emit `unknown` and retry next run;\n // do not cache (we never persist `result: unknown`).\n return { status: \"empty\", index, schema: { name: queryName, type } };\n }\n return {\n status: \"ok\",\n index,\n schema: { name: queryName, type },\n cacheEntry: { hash: sqlHash, type, retry: false },\n };\n };\n\n // Process in chunks, saving cache after each chunk\n const processBatchResults = (\n settled: PromiseSettledResult<DescribeResult>[],\n batchOffset: number,\n ) => {\n for (let i = 0; i < settled.length; i++) {\n const entry = settled[i];\n const { queryName } = uncachedQueries[batchOffset + i];\n\n if (entry.status === \"fulfilled\") {\n const res = entry.value;\n freshResults.push({ index: res.index, schema: res.schema });\n\n if (res.status === \"ok\") {\n // Only a successful describe with a result schema is cached.\n cache.queries[queryName] = res.cacheEntry;\n logEntries.push({ queryName, status: \"MISS\" });\n } else if (res.status === \"syntax\") {\n // Genuine SQL error — record it for the caller's prod/dev gate.\n // Not cached: re-described next run so a fixed query recovers.\n syntaxErrors.push({\n name: queryName,\n message: res.error.message,\n });\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind: \"syntax\",\n error: res.error,\n });\n } else if (res.status === \"empty\") {\n // status === \"empty\": described, no columns. Soft unknown, not cached.\n logEntries.push({ queryName, status: \"MISS\", kind: \"empty\" });\n } else {\n // status === \"unavailable\": non-terminal DESCRIBE (warehouse\n // stopped/cold-starting/busy). Degrade like a transient outage:\n // tag OFFLINE, count as degraded, never cache.\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind: \"connectivity\",\n });\n }\n } else {\n // executeStatement rejected without a normal StatementExecution result.\n // Only structured transport/connectivity failures are treated as\n // offline; auth, bad warehouse IDs, malformed requests, and SDK/config\n // failures stay fatal so users fix the underlying setup issue.\n completed++;\n spinner.update(\n `Describing ${total} ${total === 1 ? \"query\" : \"queries\"} (${completed}/${total})`,\n );\n\n const { sql, sqlHash, index } = uncachedQueries[batchOffset + i];\n const reason = getErrorDiagnostic(entry.reason);\n const error = parseError(reason);\n const priorEntry = cache.queries[queryName];\n const canReusePrior =\n priorEntry?.hash === sqlHash && !priorEntry.retry;\n const type = degradedType(cache, queryName, sql, sqlHash);\n freshResults.push({ index, schema: { name: queryName, type } });\n\n if (!isConnectivityError(entry.reason)) {\n fatalErrors.push({ name: queryName, message: error.message });\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind: \"fatal\",\n error,\n });\n continue;\n }\n\n logger.warn(\n \"DESCRIBE unreachable for %s: %s — %s\",\n queryName,\n reason,\n canReusePrior\n ? \"reusing last cached type\"\n : \"emitting unknown (no matching cache)\",\n );\n logEntries.push({\n queryName,\n status: \"MISS\",\n kind: \"connectivity\",\n error,\n });\n }\n }\n };\n\n if (uncachedQueries.length > concurrency) {\n for (let b = 0; b < uncachedQueries.length; b += concurrency) {\n const batch = uncachedQueries.slice(b, b + concurrency);\n const batchResults = await Promise.allSettled(batch.map(describeOne));\n processBatchResults(batchResults, b);\n await saveCache(cache);\n }\n } else {\n const settled = await Promise.allSettled(\n uncachedQueries.map(describeOne),\n );\n processBatchResults(settled, 0);\n await saveCache(cache);\n }\n\n spinner.stop(\"\");\n }\n }\n\n const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);\n\n // Print formatted table\n if (logEntries.length > 0) {\n const maxNameLen = Math.max(...logEntries.map((e) => e.queryName.length));\n const separator = pc.dim(\"─\".repeat(50));\n console.log(\"\");\n console.log(\n ` ${pc.bold(\"Typegen Queries\")} ${pc.dim(`(${logEntries.length})`)}`,\n );\n console.log(` ${separator}`);\n for (const entry of logEntries) {\n let tag: string;\n switch (entry.kind) {\n case \"syntax\":\n tag = pc.bold(pc.red(\"SQL ERR\"));\n break;\n case \"connectivity\":\n tag = pc.bold(pc.yellow(\"OFFLINE\"));\n break;\n case \"empty\":\n tag = pc.dim(\"EMPTY \");\n break;\n case \"fatal\":\n tag = pc.bold(pc.red(\"FATAL \"));\n break;\n default:\n tag =\n entry.status === \"HIT\"\n ? `cache ${pc.bold(pc.green(\"HIT \"))}`\n : `cache ${pc.bold(pc.yellow(\"MISS \"))}`;\n }\n const rawName = entry.queryName.padEnd(maxNameLen);\n // Only genuine SQL errors are struck through. Connectivity/empty kept a\n // usable type (reused or unknown), so they read as degraded, not broken.\n const name =\n entry.kind === \"syntax\" || entry.kind === \"fatal\"\n ? pc.dim(pc.strikethrough(rawName))\n : rawName;\n const errorCode = entry.error?.message.match(/\\[([^\\]]+)\\]/)?.[1];\n const reason = errorCode ? ` ${pc.dim(errorCode)}` : \"\";\n console.log(` ${tag} ${name}${reason}`);\n }\n const newCount = logEntries.filter(\n (e) => e.status === \"MISS\" && !e.kind,\n ).length;\n const cacheCount = logEntries.filter((e) => e.status === \"HIT\").length;\n const syntaxCount = logEntries.filter((e) => e.kind === \"syntax\").length;\n const offlineCount = logEntries.filter(\n (e) => e.kind === \"connectivity\",\n ).length;\n const emptyCount = logEntries.filter((e) => e.kind === \"empty\").length;\n const fatalCount = logEntries.filter((e) => e.kind === \"fatal\").length;\n console.log(` ${separator}`);\n const parts = [`${newCount} new`, `${cacheCount} from cache`];\n if (syntaxCount > 0)\n parts.push(\n `${syntaxCount} SQL ${syntaxCount === 1 ? \"error\" : \"errors\"}`,\n );\n if (offlineCount > 0) parts.push(`${offlineCount} degraded`);\n if (emptyCount > 0) parts.push(`${emptyCount} empty`);\n if (fatalCount > 0)\n parts.push(\n `${fatalCount} fatal ${fatalCount === 1 ? \"error\" : \"errors\"}`,\n );\n console.log(` ${parts.join(\", \")}. ${pc.dim(`${elapsed}s`)}`);\n console.log(\"\");\n }\n\n // Merge and sort by original file index for deterministic output\n const schemas = [...cachedResults, ...freshResults]\n .sort((a, b) => a.index - b.index)\n .map((r) => r.schema);\n\n return { schemas, syntaxErrors, fatalErrors };\n}\n\n/**\n * Normalize query name by removing the .obo extension\n * @param queryName - the query name to normalize\n * @returns the normalized query name\n */\nfunction normalizeQueryName(fileName: string): string {\n return fileName.replace(/\\.obo$/, \"\");\n}\n\n/**\n * Normalize SQL type name by removing parameters/generics\n * Examples:\n * DECIMAL(38,6) -> DECIMAL\n * ARRAY<STRING> -> ARRAY\n * MAP<STRING,INT> -> MAP\n * STRUCT<name:STRING> -> STRUCT\n * INTERVAL DAY TO SECOND -> INTERVAL\n * GEOGRAPHY(4326) -> GEOGRAPHY\n */\nexport function normalizeTypeName(typeName: string): string {\n return typeName\n .replace(/\\(.*\\)$/, \"\") // remove (p, s) eg: DECIMAL(38,6) -> DECIMAL\n .replace(/<.*>$/, \"\") // remove <T> eg: ARRAY<STRING> -> ARRAY\n .split(\" \")[0]; // take first word eg: INTERVAL DAY TO SECOND -> INTERVAL\n}\n\n/** Type Map for Databricks data types to JavaScript types */\nconst typeMap: Record<string, string> = {\n // string types\n STRING: \"string\",\n BINARY: \"string\",\n // boolean\n BOOLEAN: \"boolean\",\n // numeric types\n TINYINT: \"number\",\n SMALLINT: \"number\",\n INT: \"number\",\n BIGINT: \"number\",\n FLOAT: \"number\",\n DOUBLE: \"number\",\n DECIMAL: \"number\",\n // date/time types\n DATE: \"string\",\n TIMESTAMP: \"string\",\n TIMESTAMP_NTZ: \"string\",\n INTERVAL: \"string\",\n // complex types\n ARRAY: \"unknown[]\",\n MAP: \"Record<string, unknown>\",\n STRUCT: \"Record<string, unknown>\",\n OBJECT: \"Record<string, unknown>\",\n VARIANT: \"unknown\",\n // spatial types\n GEOGRAPHY: \"unknown\",\n GEOMETRY: \"unknown\",\n // null type\n VOID: \"null\",\n};\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
|
-
import { ApiError } from "
|
|
2
|
+
import { ApiError } from "../../workspace-client/errors.js";
|
|
3
|
+
import "../../workspace-client/index.js";
|
|
3
4
|
|
|
4
5
|
//#region src/type-generator/serving/fetcher.ts
|
|
5
6
|
const logger = createLogger("type-generator:serving:fetcher");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetcher.js","names":[],"sources":["../../../src/type-generator/serving/fetcher.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"fetcher.js","names":[],"sources":["../../../src/type-generator/serving/fetcher.ts"],"sourcesContent":["import { createLogger } from \"../../logging/logger\";\nimport { ApiError, type WorkspaceClient } from \"../../workspace-client\";\n\nconst logger = createLogger(\"type-generator:serving:fetcher\");\n\ninterface OpenApiSpec {\n openapi: string;\n info: { title: string; version: string };\n paths: Record<string, Record<string, OpenApiOperation>>;\n}\n\nexport interface OpenApiOperation {\n requestBody?: {\n content: {\n \"application/json\": {\n schema: OpenApiSchema;\n };\n };\n };\n responses?: Record<\n string,\n {\n content?: {\n \"application/json\": {\n schema: OpenApiSchema;\n };\n };\n }\n >;\n}\n\nexport interface OpenApiSchema {\n type?: string;\n properties?: Record<string, OpenApiSchema>;\n required?: string[];\n items?: OpenApiSchema;\n enum?: string[];\n nullable?: boolean;\n oneOf?: OpenApiSchema[];\n format?: string;\n}\n\n/**\n * Fetches the OpenAPI schema for a serving endpoint using the SDK.\n * Returns null if the endpoint is not found or access is denied.\n */\nexport async function fetchOpenApiSchema(\n client: WorkspaceClient,\n endpointName: string,\n servedModel?: string,\n): Promise<{ spec: OpenApiSpec; pathKey: string } | null> {\n try {\n const response = await client.servingEndpoints.getOpenApi({\n name: endpointName,\n });\n\n if (!response.contents) {\n logger.warn(\n \"Empty OpenAPI response for '%s', skipping type generation\",\n endpointName,\n );\n return null;\n }\n\n const text = await new Response(response.contents).text();\n const rawSpec: unknown = JSON.parse(text);\n\n if (\n typeof rawSpec !== \"object\" ||\n rawSpec === null ||\n !(\"paths\" in rawSpec) ||\n typeof (rawSpec as OpenApiSpec).paths !== \"object\"\n ) {\n logger.warn(\n \"Invalid OpenAPI schema structure for '%s', skipping\",\n endpointName,\n );\n return null;\n }\n const spec = rawSpec as OpenApiSpec;\n\n // Find the right path key\n const pathKeys = Object.keys(spec.paths ?? {});\n if (pathKeys.length === 0) {\n logger.warn(\"No paths in OpenAPI schema for '%s'\", endpointName);\n return null;\n }\n\n let pathKey: string;\n if (servedModel) {\n const match = pathKeys.find((k) => k.includes(`/${servedModel}/`));\n if (!match) {\n logger.warn(\n \"Served model '%s' not found in schema for '%s', using first path\",\n servedModel,\n endpointName,\n );\n pathKey = pathKeys[0];\n } else {\n pathKey = match;\n }\n } else {\n pathKey = pathKeys[0];\n }\n\n return { spec, pathKey };\n } catch (err) {\n if (err instanceof ApiError) {\n const status = err.statusCode ?? 0;\n if (status === 404) {\n logger.warn(\n \"Endpoint '%s' not found, skipping type generation\",\n endpointName,\n );\n } else if (status === 403) {\n logger.warn(\n \"Access denied to endpoint '%s' schema, skipping type generation\",\n endpointName,\n );\n } else {\n logger.warn(\n \"Failed to fetch schema for '%s' (HTTP %d), skipping: %s\",\n endpointName,\n status,\n err.message,\n );\n }\n } else {\n logger.warn(\n \"Error fetching schema for '%s': %s\",\n endpointName,\n (err as Error).message,\n );\n }\n return null;\n }\n}\n"],"mappings":";;;;;AAGA,MAAM,SAAS,aAAa,iCAAiC;;;;;AA2C7D,eAAsB,mBACpB,QACA,cACA,aACwD;AACxD,KAAI;EACF,MAAM,WAAW,MAAM,OAAO,iBAAiB,WAAW,EACxD,MAAM,cACP,CAAC;AAEF,MAAI,CAAC,SAAS,UAAU;AACtB,UAAO,KACL,6DACA,aACD;AACD,UAAO;;EAGT,MAAM,OAAO,MAAM,IAAI,SAAS,SAAS,SAAS,CAAC,MAAM;EACzD,MAAM,UAAmB,KAAK,MAAM,KAAK;AAEzC,MACE,OAAO,YAAY,YACnB,YAAY,QACZ,EAAE,WAAW,YACb,OAAQ,QAAwB,UAAU,UAC1C;AACA,UAAO,KACL,uDACA,aACD;AACD,UAAO;;EAET,MAAM,OAAO;EAGb,MAAM,WAAW,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;AAC9C,MAAI,SAAS,WAAW,GAAG;AACzB,UAAO,KAAK,uCAAuC,aAAa;AAChE,UAAO;;EAGT,IAAI;AACJ,MAAI,aAAa;GACf,MAAM,QAAQ,SAAS,MAAM,MAAM,EAAE,SAAS,IAAI,YAAY,GAAG,CAAC;AAClE,OAAI,CAAC,OAAO;AACV,WAAO,KACL,oEACA,aACA,aACD;AACD,cAAU,SAAS;SAEnB,WAAU;QAGZ,WAAU,SAAS;AAGrB,SAAO;GAAE;GAAM;GAAS;UACjB,KAAK;AACZ,MAAI,eAAe,UAAU;GAC3B,MAAM,SAAS,IAAI,cAAc;AACjC,OAAI,WAAW,IACb,QAAO,KACL,qDACA,aACD;YACQ,WAAW,IACpB,QAAO,KACL,mEACA,aACD;OAED,QAAO,KACL,2DACA,cACA,QACA,IAAI,QACL;QAGH,QAAO,KACL,sCACA,cACC,IAAc,QAChB;AAEH,SAAO"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
|
+
import { createWorkspaceClient } from "../../workspace-client/factory.js";
|
|
3
|
+
import "../../workspace-client/index.js";
|
|
2
4
|
import { migrateProjectConfig, removeOldGeneratedTypes, resolveProjectRoot } from "../migration.js";
|
|
3
5
|
import { CACHE_VERSION, hashSchema, loadServingCache, saveServingCache } from "./cache.js";
|
|
4
6
|
import { convertRequestSchema, convertResponseSchema, deriveChunkType, extractRequestKeys } from "./converter.js";
|
|
5
7
|
import { fetchOpenApiSchema } from "./fetcher.js";
|
|
6
8
|
import { extractServingEndpoints, findServerFile } from "./server-file-extractor.js";
|
|
7
|
-
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
8
9
|
import pc from "picocolors";
|
|
9
10
|
import fs from "node:fs/promises";
|
|
10
11
|
import path from "node:path";
|
|
@@ -40,7 +41,7 @@ async function generateServingTypes(options) {
|
|
|
40
41
|
const registryEntries = [];
|
|
41
42
|
const logEntries = [];
|
|
42
43
|
for (const [alias, config] of Object.entries(endpoints)) {
|
|
43
|
-
client ??=
|
|
44
|
+
client ??= createWorkspaceClient();
|
|
44
45
|
const result = await processEndpoint(alias, config, client, cache);
|
|
45
46
|
if (result.cacheUpdated) updated = true;
|
|
46
47
|
registryEntries.push(result.entry);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.js","names":[],"sources":["../../../src/type-generator/serving/generator.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { EndpointConfig } from \"../../plugins/serving/types\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"../migration\";\nimport {\n CACHE_VERSION,\n hashSchema,\n loadServingCache,\n type ServingCache,\n saveServingCache,\n} from \"./cache\";\nimport {\n convertRequestSchema,\n convertResponseSchema,\n deriveChunkType,\n extractRequestKeys,\n} from \"./converter\";\nimport { fetchOpenApiSchema } from \"./fetcher\";\nimport {\n extractServingEndpoints,\n findServerFile,\n} from \"./server-file-extractor\";\n\nconst logger = createLogger(\"type-generator:serving\");\n\nconst GENERIC_REQUEST = \"Record<string, unknown>\";\nconst GENERIC_RESPONSE = \"unknown\";\nconst GENERIC_CHUNK = \"unknown\";\n\ninterface GenerateServingTypesOptions {\n outFile: string;\n endpoints?: Record<string, EndpointConfig>;\n noCache?: boolean;\n}\n\n/**\n * Generates TypeScript type declarations for serving endpoints\n * by fetching their OpenAPI schemas and converting to TypeScript.\n *\n * Endpoint discovery order (when `endpoints` is not provided):\n * 1. AST extraction from server file (server/index.ts or server/server.ts)\n * 2. DATABRICKS_SERVING_ENDPOINT_NAME env var (single default endpoint)\n */\nexport async function generateServingTypes(\n options: GenerateServingTypesOptions,\n): Promise<void> {\n const { outFile, noCache } = options;\n const projectRoot = resolveProjectRoot(outFile);\n\n // Resolve endpoints: explicit > AST extraction from server file > env var fallback\n const endpoints =\n options.endpoints ??\n resolveEndpointsFromServerFile() ??\n resolveDefaultEndpoints();\n if (Object.keys(endpoints).length === 0) {\n logger.debug(\"No serving endpoints configured, skipping type generation\");\n return;\n }\n\n const startTime = performance.now();\n\n const cache = noCache\n ? { version: CACHE_VERSION, endpoints: {} }\n : await loadServingCache();\n\n let client: WorkspaceClient | undefined;\n let updated = false;\n\n const registryEntries: string[] = [];\n const logEntries: Array<{\n alias: string;\n status: \"HIT\" | \"MISS\";\n error?: string;\n }> = [];\n\n for (const [alias, config] of Object.entries(endpoints)) {\n client ??= new WorkspaceClient({});\n const result = await processEndpoint(alias, config, client, cache);\n if (result.cacheUpdated) updated = true;\n registryEntries.push(result.entry);\n logEntries.push(result.log);\n }\n\n printLogTable(logEntries, startTime);\n\n const output = generateTypeDeclarations(registryEntries);\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, output, \"utf-8\");\n\n // One-time migration: remove old generated file and patch project configs\n await removeOldGeneratedTypes(projectRoot, \"appKitServingTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n if (registryEntries.length === 0) {\n logger.debug(\n \"Wrote empty serving types to %s (no endpoints resolved)\",\n outFile,\n );\n } else {\n logger.debug(\"Wrote serving types to %s\", outFile);\n }\n\n if (updated) {\n await saveServingCache(cache as ServingCache);\n }\n}\n\ninterface EndpointResult {\n entry: string;\n log: { alias: string; status: \"HIT\" | \"MISS\"; error?: string };\n cacheUpdated: boolean;\n}\n\nfunction genericEntry(alias: string): string {\n return buildRegistryEntry(\n alias,\n GENERIC_REQUEST,\n GENERIC_RESPONSE,\n GENERIC_CHUNK,\n );\n}\n\nasync function processEndpoint(\n alias: string,\n config: EndpointConfig,\n client: WorkspaceClient,\n cache: { endpoints: Record<string, any> },\n): Promise<EndpointResult> {\n const endpointName = process.env[config.env];\n if (!endpointName) {\n return {\n entry: genericEntry(alias),\n log: { alias, status: \"MISS\", error: `env ${config.env} not set` },\n cacheUpdated: false,\n };\n }\n\n const result = await fetchOpenApiSchema(\n client,\n endpointName,\n config.servedModel,\n );\n if (!result) {\n return {\n entry: genericEntry(alias),\n log: { alias, status: \"MISS\", error: \"schema fetch failed\" },\n cacheUpdated: false,\n };\n }\n\n const { spec, pathKey } = result;\n const hash = hashSchema(JSON.stringify(spec));\n\n // Cache hit\n const cached = cache.endpoints[alias];\n if (cached && cached.hash === hash) {\n return {\n entry: buildRegistryEntry(\n alias,\n cached.requestType,\n cached.responseType,\n cached.chunkType,\n ),\n log: { alias, status: \"HIT\" },\n cacheUpdated: false,\n };\n }\n\n // Cache miss — convert schema to types\n const operation = spec.paths[pathKey]?.post;\n if (!operation) {\n return {\n entry: genericEntry(alias),\n log: { alias, status: \"MISS\", error: \"no POST operation\" },\n cacheUpdated: false,\n };\n }\n\n try {\n const requestType = convertRequestSchema(operation);\n const responseType = convertResponseSchema(operation);\n const chunkType = deriveChunkType(operation);\n const requestKeys = extractRequestKeys(operation);\n\n cache.endpoints[alias] = {\n hash,\n requestType,\n responseType,\n chunkType,\n requestKeys,\n };\n\n return {\n entry: buildRegistryEntry(alias, requestType, responseType, chunkType),\n log: { alias, status: \"MISS\" },\n cacheUpdated: true,\n };\n } catch (convErr) {\n logger.warn(\n \"Schema conversion failed for '%s': %s\",\n alias,\n (convErr as Error).message,\n );\n return {\n entry: genericEntry(alias),\n log: { alias, status: \"MISS\", error: \"schema conversion failed\" },\n cacheUpdated: false,\n };\n }\n}\n\nfunction printLogTable(\n logEntries: Array<{ alias: string; status: \"HIT\" | \"MISS\"; error?: string }>,\n startTime: number,\n): void {\n if (logEntries.length === 0) return;\n\n const maxNameLen = Math.max(...logEntries.map((e) => e.alias.length));\n const separator = pc.dim(\"─\".repeat(50));\n console.log(\"\");\n console.log(\n ` ${pc.bold(\"Typegen Serving\")} ${pc.dim(`(${logEntries.length})`)}`,\n );\n console.log(` ${separator}`);\n for (const entry of logEntries) {\n const tag =\n entry.status === \"HIT\"\n ? `cache ${pc.bold(pc.green(\"HIT \"))}`\n : `cache ${pc.bold(pc.yellow(\"MISS \"))}`;\n const rawName = entry.alias.padEnd(maxNameLen);\n const reason = entry.error ? ` ${pc.dim(entry.error)}` : \"\";\n console.log(` ${tag} ${rawName}${reason}`);\n }\n const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);\n const newCount = logEntries.filter((e) => e.status === \"MISS\").length;\n const cacheCount = logEntries.filter((e) => e.status === \"HIT\").length;\n console.log(` ${separator}`);\n console.log(\n ` ${newCount} new, ${cacheCount} from cache. ${pc.dim(`${elapsed}s`)}`,\n );\n console.log(\"\");\n}\n\nfunction resolveEndpointsFromServerFile():\n | Record<string, EndpointConfig>\n | undefined {\n try {\n const serverFile = findServerFile(process.cwd());\n if (!serverFile) return undefined;\n return extractServingEndpoints(serverFile) ?? undefined;\n } catch (error) {\n logger.debug(\n \"Failed to extract endpoints from server file: %s\",\n (error as Error).message,\n );\n return undefined;\n }\n}\n\nfunction resolveDefaultEndpoints(): Record<string, EndpointConfig> {\n if (process.env.DATABRICKS_SERVING_ENDPOINT_NAME) {\n return { default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" } };\n }\n return {};\n}\n\nfunction buildRegistryEntry(\n alias: string,\n requestType: string,\n responseType: string,\n chunkType: string | null,\n): string {\n const indent = \" \";\n const chunkEntry = chunkType ? chunkType : \"unknown\";\n return ` ${alias}: {\n${indent}request: ${indentType(requestType, indent)};\n${indent}response: ${indentType(responseType, indent)};\n${indent}chunk: ${indentType(chunkEntry, indent)};\n };`;\n}\n\nfunction indentType(typeStr: string, baseIndent: string): string {\n if (!typeStr.includes(\"\\n\")) return typeStr;\n return typeStr\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : `${baseIndent}${line}`))\n .join(\"\\n\");\n}\n\nfunction generateTypeDeclarations(entries: string[]): string {\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated from serving endpoint OpenAPI schemas\nimport \"@databricks/appkit\";\nimport \"@databricks/appkit-ui/react\";\n\ndeclare module \"@databricks/appkit\" {\n interface ServingEndpointRegistry {\n${entries.join(\"\\n\")}\n }\n}\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface ServingEndpointRegistry {\n${entries.join(\"\\n\")}\n }\n}\n`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generator.js","names":[],"sources":["../../../src/type-generator/serving/generator.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { EndpointConfig } from \"../../plugins/serving/types\";\nimport {\n createWorkspaceClient,\n type WorkspaceClient,\n} from \"../../workspace-client\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"../migration\";\nimport {\n CACHE_VERSION,\n hashSchema,\n loadServingCache,\n type ServingCache,\n saveServingCache,\n} from \"./cache\";\nimport {\n convertRequestSchema,\n convertResponseSchema,\n deriveChunkType,\n extractRequestKeys,\n} from \"./converter\";\nimport { fetchOpenApiSchema } from \"./fetcher\";\nimport {\n extractServingEndpoints,\n findServerFile,\n} from \"./server-file-extractor\";\n\nconst logger = createLogger(\"type-generator:serving\");\n\nconst GENERIC_REQUEST = \"Record<string, unknown>\";\nconst GENERIC_RESPONSE = \"unknown\";\nconst GENERIC_CHUNK = \"unknown\";\n\ninterface GenerateServingTypesOptions {\n outFile: string;\n endpoints?: Record<string, EndpointConfig>;\n noCache?: boolean;\n}\n\n/**\n * Generates TypeScript type declarations for serving endpoints\n * by fetching their OpenAPI schemas and converting to TypeScript.\n *\n * Endpoint discovery order (when `endpoints` is not provided):\n * 1. AST extraction from server file (server/index.ts or server/server.ts)\n * 2. DATABRICKS_SERVING_ENDPOINT_NAME env var (single default endpoint)\n */\nexport async function generateServingTypes(\n options: GenerateServingTypesOptions,\n): Promise<void> {\n const { outFile, noCache } = options;\n const projectRoot = resolveProjectRoot(outFile);\n\n // Resolve endpoints: explicit > AST extraction from server file > env var fallback\n const endpoints =\n options.endpoints ??\n resolveEndpointsFromServerFile() ??\n resolveDefaultEndpoints();\n if (Object.keys(endpoints).length === 0) {\n logger.debug(\"No serving endpoints configured, skipping type generation\");\n return;\n }\n\n const startTime = performance.now();\n\n const cache = noCache\n ? { version: CACHE_VERSION, endpoints: {} }\n : await loadServingCache();\n\n let client: WorkspaceClient | undefined;\n let updated = false;\n\n const registryEntries: string[] = [];\n const logEntries: Array<{\n alias: string;\n status: \"HIT\" | \"MISS\";\n error?: string;\n }> = [];\n\n for (const [alias, config] of Object.entries(endpoints)) {\n client ??= createWorkspaceClient();\n const result = await processEndpoint(alias, config, client, cache);\n if (result.cacheUpdated) updated = true;\n registryEntries.push(result.entry);\n logEntries.push(result.log);\n }\n\n printLogTable(logEntries, startTime);\n\n const output = generateTypeDeclarations(registryEntries);\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, output, \"utf-8\");\n\n // One-time migration: remove old generated file and patch project configs\n await removeOldGeneratedTypes(projectRoot, \"appKitServingTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n if (registryEntries.length === 0) {\n logger.debug(\n \"Wrote empty serving types to %s (no endpoints resolved)\",\n outFile,\n );\n } else {\n logger.debug(\"Wrote serving types to %s\", outFile);\n }\n\n if (updated) {\n await saveServingCache(cache as ServingCache);\n }\n}\n\ninterface EndpointResult {\n entry: string;\n log: { alias: string; status: \"HIT\" | \"MISS\"; error?: string };\n cacheUpdated: boolean;\n}\n\nfunction genericEntry(alias: string): string {\n return buildRegistryEntry(\n alias,\n GENERIC_REQUEST,\n GENERIC_RESPONSE,\n GENERIC_CHUNK,\n );\n}\n\nasync function processEndpoint(\n alias: string,\n config: EndpointConfig,\n client: WorkspaceClient,\n cache: { endpoints: Record<string, any> },\n): Promise<EndpointResult> {\n const endpointName = process.env[config.env];\n if (!endpointName) {\n return {\n entry: genericEntry(alias),\n log: { alias, status: \"MISS\", error: `env ${config.env} not set` },\n cacheUpdated: false,\n };\n }\n\n const result = await fetchOpenApiSchema(\n client,\n endpointName,\n config.servedModel,\n );\n if (!result) {\n return {\n entry: genericEntry(alias),\n log: { alias, status: \"MISS\", error: \"schema fetch failed\" },\n cacheUpdated: false,\n };\n }\n\n const { spec, pathKey } = result;\n const hash = hashSchema(JSON.stringify(spec));\n\n // Cache hit\n const cached = cache.endpoints[alias];\n if (cached && cached.hash === hash) {\n return {\n entry: buildRegistryEntry(\n alias,\n cached.requestType,\n cached.responseType,\n cached.chunkType,\n ),\n log: { alias, status: \"HIT\" },\n cacheUpdated: false,\n };\n }\n\n // Cache miss — convert schema to types\n const operation = spec.paths[pathKey]?.post;\n if (!operation) {\n return {\n entry: genericEntry(alias),\n log: { alias, status: \"MISS\", error: \"no POST operation\" },\n cacheUpdated: false,\n };\n }\n\n try {\n const requestType = convertRequestSchema(operation);\n const responseType = convertResponseSchema(operation);\n const chunkType = deriveChunkType(operation);\n const requestKeys = extractRequestKeys(operation);\n\n cache.endpoints[alias] = {\n hash,\n requestType,\n responseType,\n chunkType,\n requestKeys,\n };\n\n return {\n entry: buildRegistryEntry(alias, requestType, responseType, chunkType),\n log: { alias, status: \"MISS\" },\n cacheUpdated: true,\n };\n } catch (convErr) {\n logger.warn(\n \"Schema conversion failed for '%s': %s\",\n alias,\n (convErr as Error).message,\n );\n return {\n entry: genericEntry(alias),\n log: { alias, status: \"MISS\", error: \"schema conversion failed\" },\n cacheUpdated: false,\n };\n }\n}\n\nfunction printLogTable(\n logEntries: Array<{ alias: string; status: \"HIT\" | \"MISS\"; error?: string }>,\n startTime: number,\n): void {\n if (logEntries.length === 0) return;\n\n const maxNameLen = Math.max(...logEntries.map((e) => e.alias.length));\n const separator = pc.dim(\"─\".repeat(50));\n console.log(\"\");\n console.log(\n ` ${pc.bold(\"Typegen Serving\")} ${pc.dim(`(${logEntries.length})`)}`,\n );\n console.log(` ${separator}`);\n for (const entry of logEntries) {\n const tag =\n entry.status === \"HIT\"\n ? `cache ${pc.bold(pc.green(\"HIT \"))}`\n : `cache ${pc.bold(pc.yellow(\"MISS \"))}`;\n const rawName = entry.alias.padEnd(maxNameLen);\n const reason = entry.error ? ` ${pc.dim(entry.error)}` : \"\";\n console.log(` ${tag} ${rawName}${reason}`);\n }\n const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);\n const newCount = logEntries.filter((e) => e.status === \"MISS\").length;\n const cacheCount = logEntries.filter((e) => e.status === \"HIT\").length;\n console.log(` ${separator}`);\n console.log(\n ` ${newCount} new, ${cacheCount} from cache. ${pc.dim(`${elapsed}s`)}`,\n );\n console.log(\"\");\n}\n\nfunction resolveEndpointsFromServerFile():\n | Record<string, EndpointConfig>\n | undefined {\n try {\n const serverFile = findServerFile(process.cwd());\n if (!serverFile) return undefined;\n return extractServingEndpoints(serverFile) ?? undefined;\n } catch (error) {\n logger.debug(\n \"Failed to extract endpoints from server file: %s\",\n (error as Error).message,\n );\n return undefined;\n }\n}\n\nfunction resolveDefaultEndpoints(): Record<string, EndpointConfig> {\n if (process.env.DATABRICKS_SERVING_ENDPOINT_NAME) {\n return { default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" } };\n }\n return {};\n}\n\nfunction buildRegistryEntry(\n alias: string,\n requestType: string,\n responseType: string,\n chunkType: string | null,\n): string {\n const indent = \" \";\n const chunkEntry = chunkType ? chunkType : \"unknown\";\n return ` ${alias}: {\n${indent}request: ${indentType(requestType, indent)};\n${indent}response: ${indentType(responseType, indent)};\n${indent}chunk: ${indentType(chunkEntry, indent)};\n };`;\n}\n\nfunction indentType(typeStr: string, baseIndent: string): string {\n if (!typeStr.includes(\"\\n\")) return typeStr;\n return typeStr\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : `${baseIndent}${line}`))\n .join(\"\\n\");\n}\n\nfunction generateTypeDeclarations(entries: string[]): string {\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated from serving endpoint OpenAPI schemas\nimport \"@databricks/appkit\";\nimport \"@databricks/appkit-ui/react\";\n\ndeclare module \"@databricks/appkit\" {\n interface ServingEndpointRegistry {\n${entries.join(\"\\n\")}\n }\n}\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface ServingEndpointRegistry {\n${entries.join(\"\\n\")}\n }\n}\n`;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statement-result.js","names":[],"sources":["../../src/type-generator/statement-result.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport { createLogger } from \"../logging/logger\";\nimport { getErrorMessage } from \"./errors\";\nimport type { DatabricksStatementExecutionResponse } from \"./types\";\n\nconst logger = createLogger(\"type-generator:statement-result\");\n\n/**\n * Normalize a Statement Execution response so downstream parsers can always\n * read rows from `result.data_array`, regardless of the wire format the\n * warehouse chose.\n *\n * `@databricks/sdk-experimental`'s `executeStatement` defaults to an\n * `ARROW_STREAM` disposition. With an `INLINE` disposition the single\n * DESCRIBE row is returned as a base64-encoded Arrow IPC stream in\n * `result.attachment` and `result.data_array` is left undefined. The metric\n * and query type generators only ever read `result.data_array`, so without\n * this normalization an Arrow response reads as \"returned no rows\" — the\n * registry ships empty and the runtime fail-closed gate 503s every affected\n * metric/query. (A warehouse configured to return `JSON_ARRAY` populates\n * `data_array` directly and needs no decoding — that path, and every mocked\n * test, flows through here unchanged.)\n */\nexport async function normalizeResultRows(\n response: DatabricksStatementExecutionResponse,\n): Promise<DatabricksStatementExecutionResponse> {\n // Truncation guard, above the passthrough so it runs on either transport. A\n // result exceeding INLINE's size limit is paginated (`next_chunk_*` set) and\n // we hold only the first chunk; emitting types from it would cache partial\n // types. A deliberate throw — unlike the best-effort decode below — that both\n // callers catch per-entry as a loud per-key/per-query failure.\n if (\n response.result?.next_chunk_index != null ||\n response.result?.next_chunk_internal_link != null\n ) {\n throw new Error(\n \"DESCRIBE result is multi-chunk (truncated); refusing to emit partial types — see next_chunk_index\",\n );\n }\n\n // Passthrough: rows already materialized (JSON_ARRAY warehouses + every\n // mocked test). `data_array` being an empty array still counts as present —\n // that is a genuine \"no rows\" answer we must not overwrite with a decode.\n if (response.result?.data_array !== undefined) {\n return response;\n }\n\n const attachment = response.result?.attachment;\n if (attachment === undefined) {\n // No rows, no attachment: let the downstream \"no rows\" degrade path fire.\n return response;\n }\n\n try {\n // Lazy import: only pull apache-arrow into the process when an attachment\n // genuinely needs decoding.\n const { tableFromIPC } = await import(\"apache-arrow\");\n const bytes = Buffer.from(attachment, \"base64\");\n const table = tableFromIPC(bytes);\n // Extract each cell in SCHEMA/FIELD order. Spreading a StructRow\n // (`[...row]`) drives apache-arrow's StructRowIterator, which walks the\n // struct's children by positional index and yields `[fieldName, value]`\n // pairs in field order. We deliberately do NOT use `Object.values(row)`\n // nor `row.toArray()`: both funnel through `Object.values` (toArray() is\n // literally `Object.values(this.toJSON())` in apache-arrow@21), and\n // `Object.values` re-sorts integer-like keys ascending per the ECMAScript\n // spec — so an integer-named DESCRIBE column would scramble the positional\n // `[col_name, data_type, comment]` order. The iterator is immune to that.\n const dataArray: (string | null)[][] = table\n .toArray()\n .map((row) =>\n [...(row as Iterable<[unknown, unknown]>)].map(([, value]) =>\n value == null ? null : String(value),\n ),\n );\n\n return {\n ...response,\n result: {\n ...response.result,\n data_array: dataArray,\n },\n };\n } catch (err) {\n // Best-effort: a corrupt/partial Arrow payload — or a missing apache-arrow\n // module — must not crash the pass. Warn so a Reyden user whose decode failed\n // gets a breadcrumb instead of mysteriously-empty types, then return the\n // response unchanged: it routes into the deterministic \"no rows\" degrade.\n logger.warn(\n \"failed to decode ARROW_STREAM DESCRIBE attachment (%s); emitting no rows — metric/query types may degrade\",\n getErrorMessage(err),\n );\n return response;\n }\n}\n\n/** Result format the typegen requests for a DESCRIBE. */\ntype DescribeFormat = \"JSON_ARRAY\" | \"ARROW_STREAM\";\n\n/**\n * Per-path memo of the result format a warehouse accepts for a DESCRIBE shape.\n * Create one per describe path (metric / query) and reuse it across that path's\n * statements: a typegen run targets a single warehouse, so the working format\n * is discovered once and every later DESCRIBE skips the probe. NOT shared\n * across paths — `DESCRIBE QUERY` and `DESCRIBE … AS JSON` can differ (Reyden\n * fails `JSON_ARRAY` only for the single-cell `AS JSON` result).\n */\nexport interface DescribeFormatMemo {\n format?: \"JSON_ARRAY\" | \"ARROW_STREAM\";\n}\n\n/**\n * True when a failure means the warehouse REJECTED the requested result format\n * (so another format is worth trying), not that it ran the statement and hit a\n * real error. There is no single structured signal for this, so we combine two:\n *\n * - `errorCode` — a statement that actually RAN and failed carries a SQL error\n * code (`TABLE_OR_VIEW_NOT_FOUND`, `PARSE_SYNTAX_ERROR`, …); a request-shape\n * rejection comes back as `INVALID_PARAMETER_VALUE`. Any other code means the\n * statement ran, so it is never a format rejection — whatever its message\n * text happens to contain. This gate matters because DESCRIBE runs over\n * user-supplied SQL and analysis errors echo the offending SQL back: a source\n * with columns named `disposition`/`format` would otherwise trip the message\n * match below and get its real diagnostic masked by a pointless retry.\n * - the message signatures — Reyden's `merge_json_arrays` (its `JSON_ARRAY`\n * assembly fails on a `… AS JSON` single-cell result) and standard DBSQL's\n * rejection of `ARROW_STREAM` under an `INLINE` disposition.\n *\n * A genuine SQL error, connectivity failure, or not-ready warehouse does NOT\n * match — those are returned/propagated for the caller's normal handling, never\n * re-tried. `errorCode` is optional: a thrown SDK error carries no reliable\n * structured code, so the catch-path caller passes message only.\n */\nfunction isFormatRejection(\n message: string | undefined,\n errorCode?: string,\n): boolean {\n if (!message) return false;\n // It ran and produced a SQL error — never a format rejection, whatever the\n // message text happens to contain.\n if (errorCode && errorCode !== \"INVALID_PARAMETER_VALUE\") return false;\n const m = message.toLowerCase();\n return (\n m.includes(\"merge_json_arrays\") ||\n m.includes(\"must be json_array\") ||\n (m.includes(\"disposition\") && m.includes(\"format\"))\n );\n}\n\n/**\n * Run a DESCRIBE and return a response whose rows are readable via\n * `result.data_array`, adapting to the warehouse's result-format capability.\n *\n * No single format is portable: standard DBSQL (PRO/CLASSIC) serves\n * `INLINE`+`JSON_ARRAY` and rejects `INLINE`+`ARROW_STREAM`; the Reyden engine\n * is the inverse — it rejects `JSON_ARRAY` on a `… AS JSON` result\n * (`merge_json_arrays`) and only returns rows as an `INLINE`+`ARROW_STREAM`\n * attachment. We try `JSON_ARRAY` first (the documented default) and ONLY when\n * the warehouse rejects that format ({@link isFormatRejection}) fall back to\n * `ARROW_STREAM` (decoded by {@link normalizeResultRows}); the accepted format\n * is memoized so the rest of the run skips the probe. Any other outcome —\n * success, SQL error, degrade, connectivity failure — is returned or propagated\n * unchanged, exactly as a single executeStatement would.\n */\nexport async function describeAdaptive(\n client: WorkspaceClient,\n statement: string,\n warehouseId: string,\n memo: DescribeFormatMemo,\n): Promise<DatabricksStatementExecutionResponse> {\n const formats: DescribeFormat[] = memo.format\n ? [memo.format]\n : [\"JSON_ARRAY\", \"ARROW_STREAM\"];\n let lastResponse: DatabricksStatementExecutionResponse | undefined;\n let lastError: unknown;\n for (const format of formats) {\n try {\n const response = (await client.statementExecution.executeStatement({\n statement,\n warehouse_id: warehouseId,\n // Synchronous wait: without it the call can return PENDING/RUNNING with\n // no rows, which downstream misreads as a no-result degrade.\n wait_timeout: \"30s\",\n format,\n disposition: \"INLINE\",\n })) as DatabricksStatementExecutionResponse;\n const normalized = await normalizeResultRows(response);\n if (\n normalized.status?.state === \"FAILED\" &&\n isFormatRejection(\n normalized.status.error?.message,\n normalized.status.error?.error_code,\n )\n ) {\n lastResponse = normalized;\n continue; // warehouse rejected this format — try the next\n }\n if (normalized.status?.state === \"SUCCEEDED\") {\n memo.format = format;\n }\n return normalized;\n } catch (error) {\n if (isFormatRejection(getErrorMessage(error))) {\n lastError = error;\n continue; // format rejected via a thrown error — try the next\n }\n throw error;\n }\n }\n // Every attempted format was rejected. Surface the last outcome so the caller\n // degrades / reports as usual.\n if (lastResponse !== undefined) return lastResponse;\n throw lastError;\n}\n"],"mappings":";;;;AAKA,MAAM,SAAS,aAAa,kCAAkC;;;;;;;;;;;;;;;;;AAkB9D,eAAsB,oBACpB,UAC+C;AAM/C,KACE,SAAS,QAAQ,oBAAoB,QACrC,SAAS,QAAQ,4BAA4B,KAE7C,OAAM,IAAI,MACR,oGACD;AAMH,KAAI,SAAS,QAAQ,eAAe,OAClC,QAAO;CAGT,MAAM,aAAa,SAAS,QAAQ;AACpC,KAAI,eAAe,OAEjB,QAAO;AAGT,KAAI;EAGF,MAAM,EAAE,iBAAiB,MAAM,OAAO;EAYtC,MAAM,YAVQ,aADA,OAAO,KAAK,YAAY,SAAS,CACd,CAW9B,SAAS,CACT,KAAK,QACJ,CAAC,GAAI,IAAqC,CAAC,KAAK,GAAG,WACjD,SAAS,OAAO,OAAO,OAAO,MAAM,CACrC,CACF;AAEH,SAAO;GACL,GAAG;GACH,QAAQ;IACN,GAAG,SAAS;IACZ,YAAY;IACb;GACF;UACM,KAAK;AAKZ,SAAO,KACL,6GACA,gBAAgB,IAAI,CACrB;AACD,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;AAyCX,SAAS,kBACP,SACA,WACS;AACT,KAAI,CAAC,QAAS,QAAO;AAGrB,KAAI,aAAa,cAAc,0BAA2B,QAAO;CACjE,MAAM,IAAI,QAAQ,aAAa;AAC/B,QACE,EAAE,SAAS,oBAAoB,IAC/B,EAAE,SAAS,qBAAqB,IAC/B,EAAE,SAAS,cAAc,IAAI,EAAE,SAAS,SAAS;;;;;;;;;;;;;;;;;AAmBtD,eAAsB,iBACpB,QACA,WACA,aACA,MAC+C;CAC/C,MAAM,UAA4B,KAAK,SACnC,CAAC,KAAK,OAAO,GACb,CAAC,cAAc,eAAe;CAClC,IAAI;CACJ,IAAI;AACJ,MAAK,MAAM,UAAU,QACnB,KAAI;EAUF,MAAM,aAAa,MAAM,oBATP,MAAM,OAAO,mBAAmB,iBAAiB;GACjE;GACA,cAAc;GAGd,cAAc;GACd;GACA,aAAa;GACd,CAAC,CACoD;AACtD,MACE,WAAW,QAAQ,UAAU,YAC7B,kBACE,WAAW,OAAO,OAAO,SACzB,WAAW,OAAO,OAAO,WAC1B,EACD;AACA,kBAAe;AACf;;AAEF,MAAI,WAAW,QAAQ,UAAU,YAC/B,MAAK,SAAS;AAEhB,SAAO;UACA,OAAO;AACd,MAAI,kBAAkB,gBAAgB,MAAM,CAAC,EAAE;AAC7C,eAAY;AACZ;;AAEF,QAAM;;AAKV,KAAI,iBAAiB,OAAW,QAAO;AACvC,OAAM"}
|
|
1
|
+
{"version":3,"file":"statement-result.js","names":[],"sources":["../../src/type-generator/statement-result.ts"],"sourcesContent":["import { createLogger } from \"../logging/logger\";\nimport type { WorkspaceClient } from \"../workspace-client\";\nimport { getErrorMessage } from \"./errors\";\nimport type { DatabricksStatementExecutionResponse } from \"./types\";\n\nconst logger = createLogger(\"type-generator:statement-result\");\n\n/**\n * Normalize a Statement Execution response so downstream parsers can always\n * read rows from `result.data_array`, regardless of the wire format the\n * warehouse chose.\n *\n * `@databricks/sdk-experimental`'s `executeStatement` defaults to an\n * `ARROW_STREAM` disposition. With an `INLINE` disposition the single\n * DESCRIBE row is returned as a base64-encoded Arrow IPC stream in\n * `result.attachment` and `result.data_array` is left undefined. The metric\n * and query type generators only ever read `result.data_array`, so without\n * this normalization an Arrow response reads as \"returned no rows\" — the\n * registry ships empty and the runtime fail-closed gate 503s every affected\n * metric/query. (A warehouse configured to return `JSON_ARRAY` populates\n * `data_array` directly and needs no decoding — that path, and every mocked\n * test, flows through here unchanged.)\n */\nexport async function normalizeResultRows(\n response: DatabricksStatementExecutionResponse,\n): Promise<DatabricksStatementExecutionResponse> {\n // Truncation guard, above the passthrough so it runs on either transport. A\n // result exceeding INLINE's size limit is paginated (`next_chunk_*` set) and\n // we hold only the first chunk; emitting types from it would cache partial\n // types. A deliberate throw — unlike the best-effort decode below — that both\n // callers catch per-entry as a loud per-key/per-query failure.\n if (\n response.result?.next_chunk_index != null ||\n response.result?.next_chunk_internal_link != null\n ) {\n throw new Error(\n \"DESCRIBE result is multi-chunk (truncated); refusing to emit partial types — see next_chunk_index\",\n );\n }\n\n // Passthrough: rows already materialized (JSON_ARRAY warehouses + every\n // mocked test). `data_array` being an empty array still counts as present —\n // that is a genuine \"no rows\" answer we must not overwrite with a decode.\n if (response.result?.data_array !== undefined) {\n return response;\n }\n\n const attachment = response.result?.attachment;\n if (attachment === undefined) {\n // No rows, no attachment: let the downstream \"no rows\" degrade path fire.\n return response;\n }\n\n try {\n // Lazy import: only pull apache-arrow into the process when an attachment\n // genuinely needs decoding.\n const { tableFromIPC } = await import(\"apache-arrow\");\n const bytes = Buffer.from(attachment, \"base64\");\n const table = tableFromIPC(bytes);\n // Extract each cell in SCHEMA/FIELD order. Spreading a StructRow\n // (`[...row]`) drives apache-arrow's StructRowIterator, which walks the\n // struct's children by positional index and yields `[fieldName, value]`\n // pairs in field order. We deliberately do NOT use `Object.values(row)`\n // nor `row.toArray()`: both funnel through `Object.values` (toArray() is\n // literally `Object.values(this.toJSON())` in apache-arrow@21), and\n // `Object.values` re-sorts integer-like keys ascending per the ECMAScript\n // spec — so an integer-named DESCRIBE column would scramble the positional\n // `[col_name, data_type, comment]` order. The iterator is immune to that.\n const dataArray: (string | null)[][] = table\n .toArray()\n .map((row) =>\n [...(row as Iterable<[unknown, unknown]>)].map(([, value]) =>\n value == null ? null : String(value),\n ),\n );\n\n return {\n ...response,\n result: {\n ...response.result,\n data_array: dataArray,\n },\n };\n } catch (err) {\n // Best-effort: a corrupt/partial Arrow payload — or a missing apache-arrow\n // module — must not crash the pass. Warn so a Reyden user whose decode failed\n // gets a breadcrumb instead of mysteriously-empty types, then return the\n // response unchanged: it routes into the deterministic \"no rows\" degrade.\n logger.warn(\n \"failed to decode ARROW_STREAM DESCRIBE attachment (%s); emitting no rows — metric/query types may degrade\",\n getErrorMessage(err),\n );\n return response;\n }\n}\n\n/** Result format the typegen requests for a DESCRIBE. */\ntype DescribeFormat = \"JSON_ARRAY\" | \"ARROW_STREAM\";\n\n/**\n * Per-path memo of the result format a warehouse accepts for a DESCRIBE shape.\n * Create one per describe path (metric / query) and reuse it across that path's\n * statements: a typegen run targets a single warehouse, so the working format\n * is discovered once and every later DESCRIBE skips the probe. NOT shared\n * across paths — `DESCRIBE QUERY` and `DESCRIBE … AS JSON` can differ (Reyden\n * fails `JSON_ARRAY` only for the single-cell `AS JSON` result).\n */\nexport interface DescribeFormatMemo {\n format?: \"JSON_ARRAY\" | \"ARROW_STREAM\";\n}\n\n/**\n * True when a failure means the warehouse REJECTED the requested result format\n * (so another format is worth trying), not that it ran the statement and hit a\n * real error. There is no single structured signal for this, so we combine two:\n *\n * - `errorCode` — a statement that actually RAN and failed carries a SQL error\n * code (`TABLE_OR_VIEW_NOT_FOUND`, `PARSE_SYNTAX_ERROR`, …); a request-shape\n * rejection comes back as `INVALID_PARAMETER_VALUE`. Any other code means the\n * statement ran, so it is never a format rejection — whatever its message\n * text happens to contain. This gate matters because DESCRIBE runs over\n * user-supplied SQL and analysis errors echo the offending SQL back: a source\n * with columns named `disposition`/`format` would otherwise trip the message\n * match below and get its real diagnostic masked by a pointless retry.\n * - the message signatures — Reyden's `merge_json_arrays` (its `JSON_ARRAY`\n * assembly fails on a `… AS JSON` single-cell result) and standard DBSQL's\n * rejection of `ARROW_STREAM` under an `INLINE` disposition.\n *\n * A genuine SQL error, connectivity failure, or not-ready warehouse does NOT\n * match — those are returned/propagated for the caller's normal handling, never\n * re-tried. `errorCode` is optional: a thrown SDK error carries no reliable\n * structured code, so the catch-path caller passes message only.\n */\nfunction isFormatRejection(\n message: string | undefined,\n errorCode?: string,\n): boolean {\n if (!message) return false;\n // It ran and produced a SQL error — never a format rejection, whatever the\n // message text happens to contain.\n if (errorCode && errorCode !== \"INVALID_PARAMETER_VALUE\") return false;\n const m = message.toLowerCase();\n return (\n m.includes(\"merge_json_arrays\") ||\n m.includes(\"must be json_array\") ||\n (m.includes(\"disposition\") && m.includes(\"format\"))\n );\n}\n\n/**\n * Run a DESCRIBE and return a response whose rows are readable via\n * `result.data_array`, adapting to the warehouse's result-format capability.\n *\n * No single format is portable: standard DBSQL (PRO/CLASSIC) serves\n * `INLINE`+`JSON_ARRAY` and rejects `INLINE`+`ARROW_STREAM`; the Reyden engine\n * is the inverse — it rejects `JSON_ARRAY` on a `… AS JSON` result\n * (`merge_json_arrays`) and only returns rows as an `INLINE`+`ARROW_STREAM`\n * attachment. We try `JSON_ARRAY` first (the documented default) and ONLY when\n * the warehouse rejects that format ({@link isFormatRejection}) fall back to\n * `ARROW_STREAM` (decoded by {@link normalizeResultRows}); the accepted format\n * is memoized so the rest of the run skips the probe. Any other outcome —\n * success, SQL error, degrade, connectivity failure — is returned or propagated\n * unchanged, exactly as a single executeStatement would.\n */\nexport async function describeAdaptive(\n client: WorkspaceClient,\n statement: string,\n warehouseId: string,\n memo: DescribeFormatMemo,\n): Promise<DatabricksStatementExecutionResponse> {\n const formats: DescribeFormat[] = memo.format\n ? [memo.format]\n : [\"JSON_ARRAY\", \"ARROW_STREAM\"];\n let lastResponse: DatabricksStatementExecutionResponse | undefined;\n let lastError: unknown;\n for (const format of formats) {\n try {\n const response = (await client.statementExecution.executeStatement({\n statement,\n warehouse_id: warehouseId,\n // Synchronous wait: without it the call can return PENDING/RUNNING with\n // no rows, which downstream misreads as a no-result degrade.\n wait_timeout: \"30s\",\n format,\n disposition: \"INLINE\",\n })) as DatabricksStatementExecutionResponse;\n const normalized = await normalizeResultRows(response);\n if (\n normalized.status?.state === \"FAILED\" &&\n isFormatRejection(\n normalized.status.error?.message,\n normalized.status.error?.error_code,\n )\n ) {\n lastResponse = normalized;\n continue; // warehouse rejected this format — try the next\n }\n if (normalized.status?.state === \"SUCCEEDED\") {\n memo.format = format;\n }\n return normalized;\n } catch (error) {\n if (isFormatRejection(getErrorMessage(error))) {\n lastError = error;\n continue; // format rejected via a thrown error — try the next\n }\n throw error;\n }\n }\n // Every attempted format was rejected. Surface the last outcome so the caller\n // degrades / reports as usual.\n if (lastResponse !== undefined) return lastResponse;\n throw lastError;\n}\n"],"mappings":";;;;AAKA,MAAM,SAAS,aAAa,kCAAkC;;;;;;;;;;;;;;;;;AAkB9D,eAAsB,oBACpB,UAC+C;AAM/C,KACE,SAAS,QAAQ,oBAAoB,QACrC,SAAS,QAAQ,4BAA4B,KAE7C,OAAM,IAAI,MACR,oGACD;AAMH,KAAI,SAAS,QAAQ,eAAe,OAClC,QAAO;CAGT,MAAM,aAAa,SAAS,QAAQ;AACpC,KAAI,eAAe,OAEjB,QAAO;AAGT,KAAI;EAGF,MAAM,EAAE,iBAAiB,MAAM,OAAO;EAYtC,MAAM,YAVQ,aADA,OAAO,KAAK,YAAY,SAAS,CACd,CAW9B,SAAS,CACT,KAAK,QACJ,CAAC,GAAI,IAAqC,CAAC,KAAK,GAAG,WACjD,SAAS,OAAO,OAAO,OAAO,MAAM,CACrC,CACF;AAEH,SAAO;GACL,GAAG;GACH,QAAQ;IACN,GAAG,SAAS;IACZ,YAAY;IACb;GACF;UACM,KAAK;AAKZ,SAAO,KACL,6GACA,gBAAgB,IAAI,CACrB;AACD,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;AAyCX,SAAS,kBACP,SACA,WACS;AACT,KAAI,CAAC,QAAS,QAAO;AAGrB,KAAI,aAAa,cAAc,0BAA2B,QAAO;CACjE,MAAM,IAAI,QAAQ,aAAa;AAC/B,QACE,EAAE,SAAS,oBAAoB,IAC/B,EAAE,SAAS,qBAAqB,IAC/B,EAAE,SAAS,cAAc,IAAI,EAAE,SAAS,SAAS;;;;;;;;;;;;;;;;;AAmBtD,eAAsB,iBACpB,QACA,WACA,aACA,MAC+C;CAC/C,MAAM,UAA4B,KAAK,SACnC,CAAC,KAAK,OAAO,GACb,CAAC,cAAc,eAAe;CAClC,IAAI;CACJ,IAAI;AACJ,MAAK,MAAM,UAAU,QACnB,KAAI;EAUF,MAAM,aAAa,MAAM,oBATP,MAAM,OAAO,mBAAmB,iBAAiB;GACjE;GACA,cAAc;GAGd,cAAc;GACd;GACA,aAAa;GACd,CAAC,CACoD;AACtD,MACE,WAAW,QAAQ,UAAU,YAC7B,kBACE,WAAW,OAAO,OAAO,SACzB,WAAW,OAAO,OAAO,WAC1B,EACD;AACA,kBAAe;AACf;;AAEF,MAAI,WAAW,QAAQ,UAAU,YAC/B,MAAK,SAAS;AAEhB,SAAO;UACA,OAAO;AACd,MAAI,kBAAkB,gBAAgB,MAAM,CAAC,EAAE;AAC7C,eAAY;AACZ;;AAEF,QAAM;;AAKV,KAAI,iBAAiB,OAAW,QAAO;AACvC,OAAM"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin.d.ts","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"vite-plugin.d.ts","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"mappings":";;;;;AAEmC;UA+BzB,wBAAA;EAER,OAAA;EAFgC;;;;EAOhC,SAAA;EAMY;AASd;;;;EATE,YAAA;AAAA;;;;;;;iBASc,iBAAA,CAAkB,OAAA,GAAU,wBAAA,GAA2B,MAAA"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createLogger } from "../logging/logger.js";
|
|
2
|
+
import { createWorkspaceClient } from "../workspace-client/factory.js";
|
|
3
|
+
import "../workspace-client/index.js";
|
|
2
4
|
import { METRIC_CONFIG_FILE } from "../shared/src/schemas/metric-fqn.js";
|
|
3
5
|
import { getWarehouseState, startWarehouse, waitUntilRunning } from "./warehouse-status.js";
|
|
4
6
|
import { ANALYTICS_TYPES_FILE, TYPES_DIR, TypegenFatalError, TypegenSyntaxError, generateFromEntryPoint } from "./index.js";
|
|
5
|
-
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { existsSync } from "node:fs";
|
|
8
9
|
|
|
@@ -142,7 +143,7 @@ function appKitTypesPlugin(options) {
|
|
|
142
143
|
const { signal } = controller;
|
|
143
144
|
(async () => {
|
|
144
145
|
try {
|
|
145
|
-
const client =
|
|
146
|
+
const client = createWorkspaceClient();
|
|
146
147
|
const state = await getWarehouseState(client, warehouseId);
|
|
147
148
|
if (state === "DELETED" || state === "DELETING") return;
|
|
148
149
|
let startedByUs = false;
|