@databricks/appkit 0.39.0 → 0.41.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.
@@ -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 pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport { CACHE_VERSION, hashSQL, loadCache, saveCache } from \"./cache\";\nimport { Spinner } from \"./spinner\";\nimport {\n type DatabricksStatementExecutionResponse,\n type QuerySchema,\n sqlTypeToHelper,\n sqlTypeToMarker,\n} from \"./types\";\n\nconst logger = createLogger(\"type-generator:query-registry\");\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\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 const columns = dataRows.map((row) => ({\n name: row[0] || \"\",\n type_name: row[1]?.toUpperCase() || \"STRING\",\n comment: row[2] || undefined,\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} */\\n `\n : `/** @sqlType ${column.type_name} */\\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\nexport function extractParameterTypes(sql: string): Record<string, string> {\n const paramTypes: Record<string, string> = {};\n // Alternation order matters: TIMESTAMP_NTZ must precede TIMESTAMP so the\n // regex engine doesn't greedy-match TIMESTAMP and leave `_NTZ` unconsumed.\n const regex =\n /--\\s*@param\\s+(\\w+)\\s+(STRING|NUMERIC|DECIMAL|BIGINT|TINYINT|SMALLINT|INT|FLOAT|DOUBLE|BOOLEAN|DATE|TIMESTAMP_NTZ|TIMESTAMP|BINARY)\\b/gi;\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 * 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 * @returns an array of query schemas\n */\nexport async function generateQueriesFromDescribe(\n queryFolder: string,\n warehouseId: string,\n options: { noCache?: boolean; concurrency?: number } = {},\n): Promise<QuerySchema[]> {\n const { noCache = false, concurrency: rawConcurrency = 10 } = 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 failed?: boolean;\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 const sqlWithDefaults = sql.replace(\n /(?<!:):([a-zA-Z_]\\w*)/g,\n (original, paramName, offset) => {\n if (isInsideProtectedRange(offset, protectedRanges)) {\n return original;\n }\n return defaultForType(parameterTypes[paramName]);\n },\n );\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 (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 status: \"ok\";\n index: number;\n schema: QuerySchema;\n cacheEntry: { hash: string; type: string; retry: boolean };\n }\n | {\n status: \"fail\";\n index: number;\n schema: QuerySchema;\n cacheEntry: { hash: string; type: string; retry: boolean };\n error: { code?: string; message: string };\n };\n\n const freshResults: Array<{ index: number; schema: QuerySchema }> = [];\n\n if (uncachedQueries.length > 0) {\n let completed = 0;\n const total = uncachedQueries.length;\n spinner.start(\n `Describing ${total} ${total === 1 ? \"query\" : \"queries\"} (0/${total})`,\n );\n\n const describeOne = async ({\n index,\n queryName,\n sql,\n sqlHash,\n cleanedSql,\n }: (typeof uncachedQueries)[number]): Promise<DescribeResult> => {\n const result = (await client.statementExecution.executeStatement({\n statement: `DESCRIBE QUERY ${cleanedSql}`,\n warehouse_id: warehouseId,\n })) as DatabricksStatementExecutionResponse;\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\",\n queryName,\n result.status.state,\n result.result?.data_array?.length ?? 0,\n );\n\n if (result.status.state === \"FAILED\") {\n const sqlError =\n result.status.error?.message || \"Query execution failed\";\n logger.warn(\"DESCRIBE failed for %s: %s\", queryName, sqlError);\n const type = generateUnknownResultQuery(sql, queryName);\n return {\n status: \"fail\",\n index,\n schema: { name: queryName, type },\n cacheEntry: { hash: sqlHash, type, retry: true },\n error: parseError(sqlError),\n };\n }\n\n const { type, hasResults } = convertToQueryType(result, sql, queryName);\n return {\n status: \"ok\",\n index,\n schema: { name: queryName, type },\n cacheEntry: { hash: sqlHash, type, retry: !hasResults },\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 cache.queries[queryName] = res.cacheEntry;\n logEntries.push({\n queryName,\n status: \"MISS\",\n failed: res.status === \"fail\",\n error: res.status === \"fail\" ? res.error : undefined,\n });\n } else {\n const { sql, sqlHash, index } = uncachedQueries[batchOffset + i];\n const reason =\n entry.reason instanceof Error\n ? entry.reason.message\n : String(entry.reason);\n logger.warn(\"DESCRIBE rejected for %s: %s\", queryName, reason);\n const type = generateUnknownResultQuery(sql, queryName);\n freshResults.push({ index, schema: { name: queryName, type } });\n cache.queries[queryName] = { hash: sqlHash, type, retry: true };\n logEntries.push({\n queryName,\n status: \"MISS\",\n failed: true,\n error: parseError(reason),\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 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 const tag = entry.failed\n ? pc.bold(pc.red(\"ERROR\"))\n : entry.status === \"HIT\"\n ? `cache ${pc.bold(pc.green(\"HIT \"))}`\n : `cache ${pc.bold(pc.yellow(\"MISS \"))}`;\n const rawName = entry.queryName.padEnd(maxNameLen);\n const name = entry.failed ? pc.dim(pc.strikethrough(rawName)) : 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.failed,\n ).length;\n const cacheCount = logEntries.filter(\n (e) => e.status === \"HIT\" && !e.failed,\n ).length;\n const errorCount = logEntries.filter((e) => e.failed).length;\n console.log(` ${separator}`);\n const parts = [`${newCount} new`, `${cacheCount} from cache`];\n if (errorCount > 0)\n parts.push(`${errorCount} ${errorCount === 1 ? \"error\" : \"errors\"}`);\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 return [...cachedResults, ...freshResults]\n .sort((a, b) => a.index - b.index)\n .map((r) => r.schema);\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 { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport { CACHE_VERSION, hashSQL, loadCache, saveCache } from \"./cache\";\nimport { decidePreflight, type PreflightMode } from \"./preflight\";\nimport { Spinner } from \"./spinner\";\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\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n if (isObject(error) && typeof error.message === \"string\") {\n return error.message;\n }\n return String(error);\n}\n\nfunction getErrorDiagnostic(error: unknown): string {\n const seen = new Set<unknown>();\n const messages: string[] = [];\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const message = getErrorMessage(current);\n if (\n message &&\n message !== \"[object Object]\" &&\n !messages.includes(message)\n ) {\n messages.push(message);\n }\n\n const code = getErrorCode(current);\n if (code && !messages.includes(code)) messages.push(code);\n\n stack.push(...getErrorChildren(current));\n }\n\n return messages.length > 0 ? messages.join(\": \") : getErrorMessage(error);\n}\n\nfunction getErrorCode(error: unknown): string | undefined {\n if (!isObject(error)) return undefined;\n const code = error.code ?? error.errno;\n return typeof code === \"string\" ? code : undefined;\n}\n\nfunction getErrorStatus(error: unknown): number | undefined {\n if (!isObject(error)) return undefined;\n const direct = error.status ?? error.statusCode;\n if (typeof direct === \"number\") return direct;\n if (isObject(error.response) && typeof error.response.status === \"number\") {\n return error.response.status;\n }\n return undefined;\n}\n\nfunction getErrorChildren(error: unknown): unknown[] {\n if (!isObject(error)) return [];\n const children: unknown[] = [];\n if (\"cause\" in error) children.push(error.cause);\n if (error instanceof AggregateError) children.push(...error.errors);\n return children;\n}\n\nconst CONNECTIVITY_ERROR_CODES = new Set([\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ENOTFOUND\",\n \"ETIMEDOUT\",\n \"EAI_AGAIN\",\n \"EAI_NODATA\",\n \"EAI_NONAME\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n \"CERT_HAS_EXPIRED\",\n \"DEPTH_ZERO_SELF_SIGNED_CERT\",\n \"ERR_TLS_CERT_ALTNAME_INVALID\",\n \"SELF_SIGNED_CERT_IN_CHAIN\",\n \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\",\n]);\n\nfunction isConnectivityMessage(message: string): boolean {\n return (\n /\\bconnection (?:refused|reset|timed out)\\b/i.test(message) ||\n /\\bsocket hang up\\b/i.test(message) ||\n /\\bnetwork error\\b/i.test(message) ||\n /\\bcan'?t connect to\\b/i.test(message) ||\n /\\bcertificate has expired\\b/i.test(message) ||\n /\\bunable to verify the first certificate\\b/i.test(message) ||\n /\\bupstream connect error or disconnect\\/reset before headers\\b/i.test(\n message,\n )\n );\n}\n\nfunction isConnectivityError(error: unknown): boolean {\n const seen = new Set<unknown>();\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const code = getErrorCode(current);\n if (\n code &&\n (CONNECTIVITY_ERROR_CODES.has(code) || code.startsWith(\"UND_ERR_\"))\n ) {\n return true;\n }\n\n const status = getErrorStatus(current);\n if (status === 502 || status === 503 || status === 504) return true;\n\n if (isConnectivityMessage(getErrorMessage(current))) return true;\n\n stack.push(...getErrorChildren(current));\n }\n\n return false;\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\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 const columns = dataRows.map((row) => ({\n name: row[0] || \"\",\n type_name: row[1]?.toUpperCase() || \"STRING\",\n comment: row[2] || undefined,\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} */\\n `\n : `/** @sqlType ${column.type_name} */\\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\nexport function extractParameterTypes(sql: string): Record<string, string> {\n const paramTypes: Record<string, string> = {};\n // Alternation order matters: TIMESTAMP_NTZ must precede TIMESTAMP so the\n // regex engine doesn't greedy-match TIMESTAMP and leave `_NTZ` unconsumed.\n const regex =\n /--\\s*@param\\s+(\\w+)\\s+(STRING|NUMERIC|DECIMAL|BIGINT|TINYINT|SMALLINT|INT|FLOAT|DOUBLE|BOOLEAN|DATE|TIMESTAMP_NTZ|TIMESTAMP|BINARY)\\b/gi;\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 * 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 const sqlWithDefaults = sql.replace(\n /(?<!:):([a-zA-Z_]\\w*)/g,\n (original, paramName, offset) => {\n if (isInsideProtectedRange(offset, protectedRanges)) {\n return original;\n }\n return defaultForType(parameterTypes[paramName]);\n },\n );\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 (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 const describeOne = async ({\n index,\n queryName,\n sql,\n sqlHash,\n cleanedSql,\n }: (typeof uncachedQueries)[number]): Promise<DescribeResult> => {\n const result = (await client.statementExecution.executeStatement({\n statement: `DESCRIBE QUERY ${cleanedSql}`,\n warehouse_id: warehouseId,\n })) as DatabricksStatementExecutionResponse;\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\",\n queryName,\n result.status.state,\n result.result?.data_array?.length ?? 0,\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: parseError(sqlError),\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 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../src/type-generator/types.ts"],"sourcesContent":["/**\n * Databricks statement execution response interface for DESCRIBE QUERY\n * @property statement_id - the id of the statement\n * @property status - the status of the statement\n * @property result - the result containing column schema as rows [col_name, data_type, comment]\n */\nexport interface DatabricksStatementExecutionResponse {\n statement_id: string;\n status: {\n state: string;\n error?: { error_code?: string; message?: string };\n };\n result?: {\n data_array?: (string | null)[][];\n };\n}\n\n/**\n * Map of SQL types to their corresponding marker types\n * Used to convert SQL types to their corresponding marker types\n */\nexport const sqlTypeToMarker: Record<string, string> = {\n // string\n STRING: \"SQLStringMarker\",\n BINARY: \"SQLBinaryMarker\",\n // boolean\n BOOLEAN: \"SQLBooleanMarker\",\n // numeric\n NUMERIC: \"SQLNumberMarker\",\n INT: \"SQLNumberMarker\",\n BIGINT: \"SQLNumberMarker\",\n TINYINT: \"SQLNumberMarker\",\n SMALLINT: \"SQLNumberMarker\",\n FLOAT: \"SQLNumberMarker\",\n DOUBLE: \"SQLNumberMarker\",\n DECIMAL: \"SQLNumberMarker\",\n // date/time\n DATE: \"SQLDateMarker\",\n TIMESTAMP: \"SQLTimestampMarker\",\n TIMESTAMP_NTZ: \"SQLTimestampMarker\",\n};\n\n/**\n * Map of SQL types to their corresponding helper function names\n * Used to generate JSDoc hints for parameters\n */\nexport const sqlTypeToHelper: Record<string, string> = {\n // string\n STRING: \"sql.string()\",\n BINARY: \"sql.binary()\",\n // boolean\n BOOLEAN: \"sql.boolean()\",\n // numeric — route each SQL type to its closest typed helper. INT/BIGINT\n // are critical for LIMIT/OFFSET; FLOAT/DOUBLE preserve precision intent;\n // NUMERIC/DECIMAL route to sql.numeric() for exact-decimal columns.\n NUMERIC: \"sql.numeric()\",\n DECIMAL: \"sql.numeric()\",\n BIGINT: \"sql.bigint()\",\n INT: \"sql.int()\",\n TINYINT: \"sql.int()\",\n SMALLINT: \"sql.int()\",\n FLOAT: \"sql.float()\",\n DOUBLE: \"sql.double()\",\n // date/time\n DATE: \"sql.date()\",\n TIMESTAMP: \"sql.timestamp()\",\n TIMESTAMP_NTZ: \"sql.timestamp()\",\n};\n\n/**\n * Query schema interface\n * @property name - the name of the query\n * @property type - the type of the query (string, number, boolean, object, array, etc.)\n */\nexport interface QuerySchema {\n name: string;\n type: string;\n}\n"],"mappings":";;;;;AAqBA,MAAa,kBAA0C;CAErD,QAAQ;CACR,QAAQ;CAER,SAAS;CAET,SAAS;CACT,KAAK;CACL,QAAQ;CACR,SAAS;CACT,UAAU;CACV,OAAO;CACP,QAAQ;CACR,SAAS;CAET,MAAM;CACN,WAAW;CACX,eAAe;CAChB;;;;;AAMD,MAAa,kBAA0C;CAErD,QAAQ;CACR,QAAQ;CAER,SAAS;CAIT,SAAS;CACT,SAAS;CACT,QAAQ;CACR,KAAK;CACL,SAAS;CACT,UAAU;CACV,OAAO;CACP,QAAQ;CAER,MAAM;CACN,WAAW;CACX,eAAe;CAChB"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/type-generator/types.ts"],"sourcesContent":["/**\n * Databricks statement execution response interface for DESCRIBE QUERY\n * @property statement_id - the id of the statement\n * @property status - the status of the statement\n * @property result - the result containing column schema as rows [col_name, data_type, comment]\n */\nexport interface DatabricksStatementExecutionResponse {\n statement_id: string;\n status: {\n state: string;\n error?: { error_code?: string; message?: string };\n };\n result?: {\n data_array?: (string | null)[][];\n };\n}\n\n/**\n * Map of SQL types to their corresponding marker types\n * Used to convert SQL types to their corresponding marker types\n */\nexport const sqlTypeToMarker: Record<string, string> = {\n // string\n STRING: \"SQLStringMarker\",\n BINARY: \"SQLBinaryMarker\",\n // boolean\n BOOLEAN: \"SQLBooleanMarker\",\n // numeric\n NUMERIC: \"SQLNumberMarker\",\n INT: \"SQLNumberMarker\",\n BIGINT: \"SQLNumberMarker\",\n TINYINT: \"SQLNumberMarker\",\n SMALLINT: \"SQLNumberMarker\",\n FLOAT: \"SQLNumberMarker\",\n DOUBLE: \"SQLNumberMarker\",\n DECIMAL: \"SQLNumberMarker\",\n // date/time\n DATE: \"SQLDateMarker\",\n TIMESTAMP: \"SQLTimestampMarker\",\n TIMESTAMP_NTZ: \"SQLTimestampMarker\",\n};\n\n/**\n * Map of SQL types to their corresponding helper function names\n * Used to generate JSDoc hints for parameters\n */\nexport const sqlTypeToHelper: Record<string, string> = {\n // string\n STRING: \"sql.string()\",\n BINARY: \"sql.binary()\",\n // boolean\n BOOLEAN: \"sql.boolean()\",\n // numeric — route each SQL type to its closest typed helper. INT/BIGINT\n // are critical for LIMIT/OFFSET; FLOAT/DOUBLE preserve precision intent;\n // NUMERIC/DECIMAL route to sql.numeric() for exact-decimal columns.\n NUMERIC: \"sql.numeric()\",\n DECIMAL: \"sql.numeric()\",\n BIGINT: \"sql.bigint()\",\n INT: \"sql.int()\",\n TINYINT: \"sql.int()\",\n SMALLINT: \"sql.int()\",\n FLOAT: \"sql.float()\",\n DOUBLE: \"sql.double()\",\n // date/time\n DATE: \"sql.date()\",\n TIMESTAMP: \"sql.timestamp()\",\n TIMESTAMP_NTZ: \"sql.timestamp()\",\n};\n\n/**\n * Query schema interface\n * @property name - the name of the query\n * @property type - the type of the query (string, number, boolean, object, array, etc.)\n */\nexport interface QuerySchema {\n name: string;\n type: string;\n}\n\n/**\n * A genuine SQL error: `DESCRIBE QUERY` ran against a *reachable* warehouse and\n * the warehouse reported the statement as FAILED (bad table, syntax error,\n * incompatible type, …). Distinct from a connectivity failure (warehouse\n * unreachable), which is non-fatal and never recorded here.\n * @property name - the query name\n * @property message - the SQL error message reported by the warehouse\n */\nexport interface QuerySyntaxError {\n name: string;\n message: string;\n}\n\n/**\n * A non-SQL fatal error while attempting to describe a query: authentication,\n * authorization, invalid warehouse/configuration, malformed SDK request, or\n * any other setup problem that should not be treated as an offline warehouse.\n * @property name - the query name\n * @property message - the fatal error message\n */\nexport interface QueryFatalError {\n name: string;\n message: string;\n}\n\n/**\n * Result of describing a folder of queries.\n * @property schemas - one schema per query, in original file order. Queries that\n * could not be described carry `result: unknown` so output stays valid.\n * @property syntaxErrors - queries whose DESCRIBE failed against a reachable\n * warehouse (genuine SQL errors). Connectivity failures are deliberately NOT\n * included: they degrade silently (reuse last-known-good type or emit\n * `unknown`) so a transient outage never fails a build.\n * @property fatalErrors - non-SQL fatal describe request failures. These still\n * produce `result: unknown` schemas so callers can write declarations before\n * surfacing the error.\n */\nexport interface QueryGenerationResult {\n schemas: QuerySchema[];\n syntaxErrors: QuerySyntaxError[];\n fatalErrors: QueryFatalError[];\n}\n"],"mappings":";;;;;AAqBA,MAAa,kBAA0C;CAErD,QAAQ;CACR,QAAQ;CAER,SAAS;CAET,SAAS;CACT,KAAK;CACL,QAAQ;CACR,SAAS;CACT,UAAU;CACV,OAAO;CACP,QAAQ;CACR,SAAS;CAET,MAAM;CACN,WAAW;CACX,eAAe;CAChB;;;;;AAMD,MAAa,kBAA0C;CAErD,QAAQ;CACR,QAAQ;CAER,SAAS;CAIT,SAAS;CACT,SAAS;CACT,QAAQ;CACR,KAAK;CACL,SAAS;CACT,UAAU;CACV,OAAO;CACP,QAAQ;CAER,MAAM;CACN,WAAW;CACX,eAAe;CAChB"}
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.d.ts","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"mappings":";;;;;AAEmC;UAazB,wBAAA;EAER,OAAA;EAAA;EAEA,YAAA;AAAA;;;;;;;iBASc,iBAAA,CAAkB,OAAA,GAAU,wBAAA,GAA2B,MAAA"}
1
+ {"version":3,"file":"vite-plugin.d.ts","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"mappings":";;;;;AAGmC;UA6BzB,wBAAA;EAER,OAAA;EAAA;EAEA,YAAA;AAAA;;;;;;;iBASc,iBAAA,CAAkB,OAAA,GAAU,wBAAA,GAA2B,MAAA"}
@@ -1,11 +1,20 @@
1
1
  import { createLogger } from "../logging/logger.js";
2
- import { ANALYTICS_TYPES_FILE, TYPES_DIR, generateFromEntryPoint } from "./index.js";
2
+ import { getWarehouseState, startWarehouse, waitUntilRunning } from "./warehouse-status.js";
3
+ import { ANALYTICS_TYPES_FILE, TYPES_DIR, TypegenFatalError, TypegenSyntaxError, generateFromEntryPoint } from "./index.js";
4
+ import { WorkspaceClient } from "@databricks/sdk-experimental";
3
5
  import path from "node:path";
4
6
  import { existsSync } from "node:fs";
5
7
 
6
8
  //#region src/type-generator/vite-plugin.ts
7
9
  const logger = createLogger("type-generator:vite-plugin");
8
10
  /**
11
+ * How long the DEV background watcher waits for a STARTING warehouse to reach
12
+ * RUNNING before giving up. Short relative to the CLI's preflight budget: this
13
+ * is a best-effort "regenerate once the warehouse warms up" convenience, not a
14
+ * gate, so we'd rather stop polling than hold a detached task open for minutes.
15
+ */
16
+ const DEV_WAREHOUSE_WATCH_MAX_MS = 6e4;
17
+ /**
9
18
  * Vite plugin to generate types for AppKit queries.
10
19
  * Calls generateFromEntryPoint under the hood.
11
20
  * @param options - Options to override default values.
@@ -14,7 +23,22 @@ const logger = createLogger("type-generator:vite-plugin");
14
23
  function appKitTypesPlugin(options) {
15
24
  let outFile;
16
25
  let watchFolders;
17
- async function generate() {
26
+ let inFlight = null;
27
+ let queued = false;
28
+ let pendingMode = "non-blocking";
29
+ let watchController = null;
30
+ /**
31
+ * Generate types once in the given preflight {@link PreflightMode}. Never
32
+ * throws in dev (logs instead); in production it rethrows so the build fails.
33
+ * This is the un-guarded core — callers should go through {@link runGenerate}
34
+ * so concurrent triggers can't race-write the .d.ts.
35
+ *
36
+ * @param mode - preflight policy for this run. The foreground build passes a
37
+ * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it
38
+ * degrades instantly); the background warehouse watch passes "blocking" so
39
+ * its regenerate actually DESCRIBEs and lands real (non-degraded) types.
40
+ */
41
+ async function generateOnce(mode) {
18
42
  try {
19
43
  const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || "";
20
44
  if (!warehouseId) {
@@ -25,12 +49,114 @@ function appKitTypesPlugin(options) {
25
49
  outFile,
26
50
  queryFolder: watchFolders[0],
27
51
  warehouseId,
28
- noCache: false
52
+ noCache: false,
53
+ mode
29
54
  });
30
55
  } catch (error) {
31
- if (process.env.NODE_ENV === "production") throw error;
32
- logger.error("Error generating types: %O", error);
56
+ const isTypegenError = error instanceof TypegenSyntaxError || error instanceof TypegenFatalError;
57
+ if (process.env.NODE_ENV === "production") {
58
+ if (isTypegenError) error.stack = error.message;
59
+ throw error;
60
+ }
61
+ if (isTypegenError) logger.error("%s", error.message);
62
+ else logger.error("Error generating types: %O", error);
63
+ }
64
+ }
65
+ /**
66
+ * Single-flight wrapper around {@link generateOnce}. The initial build, the
67
+ * .sql watcher, and the DEV warehouse watch all route through here so they can
68
+ * never run typegen concurrently (which would race-write the .d.ts).
69
+ *
70
+ * If a run is already in flight, this does NOT start a second one — it records
71
+ * the requested mode and sets a trailing flag so exactly one more run fires
72
+ * after the current finishes, coalescing any number of overlapping triggers
73
+ * (latest-wins, including the mode: a blocking watch trigger that arrives mid
74
+ * non-blocking foreground run still describes when its trailing run fires).
75
+ *
76
+ * @param mode - preflight policy for this run. Recorded into `pendingMode`,
77
+ * which the drain reads for each generate (latest trigger wins).
78
+ * @returns A promise that resolves when this trigger's work (including any
79
+ * trailing run it scheduled) has completed.
80
+ */
81
+ function runGenerate(mode) {
82
+ pendingMode = mode;
83
+ if (inFlight) {
84
+ queued = true;
85
+ return inFlight;
33
86
  }
87
+ const drain = async () => {
88
+ while (true) {
89
+ queued = false;
90
+ await generateOnce(pendingMode);
91
+ if (!queued) {
92
+ inFlight = null;
93
+ return;
94
+ }
95
+ }
96
+ };
97
+ inFlight = drain();
98
+ return inFlight;
99
+ }
100
+ /**
101
+ * DEV-only: get the warehouse to RUNNING in the background and regenerate with
102
+ * real (non-degraded) types once it is — without blocking dev startup. The
103
+ * foreground build only ever degrades in dev (instant `unknown`/cached types),
104
+ * so this is what lands actual DESCRIBE results in the editor for EVERY
105
+ * reachable warehouse state, not just one that happens to already be warm.
106
+ *
107
+ * Post-probe behaviour by state:
108
+ * - RUNNING → describe right away (the dev foreground degraded, so a running
109
+ * warehouse would otherwise never get real types — this is the case Phase 3
110
+ * restores). `waitUntilRunning` returns immediately for an already-running
111
+ * warehouse, then the blocking regenerate fires.
112
+ * - STARTING → it's already coming up; just wait for RUNNING, then describe.
113
+ * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.
114
+ * - DELETED / DELETING → return (a deleted warehouse can't be started, and
115
+ * blocking typegen would treat it as fatal); leave the degraded types.
116
+ *
117
+ * No-op in production or without a warehouse id. Replaces any previously-armed
118
+ * watch (aborting it first). Fully self-contained: it never throws into the
119
+ * caller and never re-arms itself. The whole lifecycle is abortable via the
120
+ * shared {@link watchController} — its signal is threaded into
121
+ * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the
122
+ * regenerate routes through {@link runGenerate} so it can't race-write the
123
+ * .d.ts with the foreground degrade or a `.sql` re-trigger.
124
+ *
125
+ * The regenerate runs in "blocking" mode (not the foreground's non-blocking)
126
+ * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —
127
+ * the whole point of warming the warehouse in the background.
128
+ */
129
+ function armWarehouseWatch() {
130
+ if (process.env.NODE_ENV === "production") return;
131
+ const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || "";
132
+ if (!warehouseId) return;
133
+ watchController?.abort();
134
+ const controller = new AbortController();
135
+ watchController = controller;
136
+ const { signal } = controller;
137
+ (async () => {
138
+ try {
139
+ const client = new WorkspaceClient({});
140
+ const state = await getWarehouseState(client, warehouseId);
141
+ if (state === "DELETED" || state === "DELETING") return;
142
+ let startedByUs = false;
143
+ if (state === "STOPPED" || state === "STOPPING") try {
144
+ logger.debug("Warehouse is %s; starting it.", state);
145
+ await startWarehouse(client, warehouseId);
146
+ startedByUs = true;
147
+ } catch {
148
+ return;
149
+ }
150
+ if (await waitUntilRunning(client, warehouseId, {
151
+ maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,
152
+ signal,
153
+ treatStoppedAsTransient: startedByUs
154
+ }) === "RUNNING" && !signal.aborted) {
155
+ logger.debug("Warehouse is RUNNING; regenerating types.");
156
+ await runGenerate("blocking");
157
+ }
158
+ } catch {}
159
+ })();
34
160
  }
35
161
  return {
36
162
  name: "appkit-types",
@@ -48,12 +174,20 @@ function appKitTypesPlugin(options) {
48
174
  watchFolders = options?.watchFolders ?? [path.join(process.cwd(), "config", "queries")];
49
175
  },
50
176
  buildStart() {
51
- generate();
177
+ if (process.env.NODE_ENV === "production") return runGenerate("blocking");
178
+ runGenerate("non-blocking");
179
+ armWarehouseWatch();
52
180
  },
53
181
  configureServer(server) {
54
182
  server.watcher.add(watchFolders);
55
183
  server.watcher.on("change", (changedFile) => {
56
- if (watchFolders.some((folder) => changedFile.startsWith(folder)) && changedFile.endsWith(".sql")) generate();
184
+ if (watchFolders.some((folder) => changedFile.startsWith(folder)) && changedFile.endsWith(".sql")) {
185
+ runGenerate("non-blocking");
186
+ armWarehouseWatch();
187
+ }
188
+ });
189
+ server.httpServer?.once("close", () => {
190
+ watchController?.abort();
57
191
  });
58
192
  }
59
193
  };
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n} from \"./index\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /** Folders to watch for changes. */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let watchFolders: string[];\n\n async function generate() {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder: watchFolders[0],\n warehouseId,\n noCache: false,\n });\n } catch (error) {\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n throw error;\n }\n logger.error(\"Error generating types: %O\", error);\n }\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n if (!existsSync(path.join(process.cwd(), \"config\", \"queries\"))) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n watchFolders = options?.watchFolders ?? [\n path.join(process.cwd(), \"config\", \"queries\"),\n ];\n },\n\n buildStart() {\n generate();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n if (isWatchedFile && changedFile.endsWith(\".sql\")) {\n generate();\n }\n });\n },\n };\n}\n"],"mappings":";;;;;;AAUA,MAAM,SAAS,aAAa,6BAA6B;;;;;;;AAkBzD,SAAgB,kBAAkB,SAA4C;CAC5E,IAAI;CACJ,IAAI;CAEJ,eAAe,WAAW;AACxB,MAAI;GACF,MAAM,cAAc,QAAQ,IAAI,2BAA2B;AAE3D,OAAI,CAAC,aAAa;AAChB,WAAO,MAAM,oDAAoD;AACjE;;AAGF,SAAM,uBAAuB;IAC3B;IACA,aAAa,aAAa;IAC1B;IACA,SAAS;IACV,CAAC;WACK,OAAO;AAEd,OAAI,QAAQ,IAAI,aAAa,aAC3B,OAAM;AAER,UAAO,MAAM,8BAA8B,MAAM;;;AAIrD,QAAO;EACL,MAAM;EAEN,QAAQ;AAGN,OAAI,EAFgB,QAAQ,IAAI,2BAA2B,KAEzC;AAChB,WAAO,MAAM,oDAAoD;AACjE,WAAO;;AAGT,OAAI,CAAC,WAAW,KAAK,KAAK,QAAQ,KAAK,EAAE,UAAU,UAAU,CAAC,CAC5D,QAAO;AAGT,UAAO;;EAGT,eAAe,QAAQ;GACrB,MAAM,cAAc,KAAK,QAAQ,OAAO,MAAM,KAAK;AACnD,aAAU,KAAK,QACb,aACA,SAAS,WAAW,UAAU,UAAU,GAAG,uBAC5C;AACD,kBAAe,SAAS,gBAAgB,CACtC,KAAK,KAAK,QAAQ,KAAK,EAAE,UAAU,UAAU,CAC9C;;EAGH,aAAa;AACX,aAAU;;EAGZ,gBAAgB,QAAQ;AACtB,UAAO,QAAQ,IAAI,aAAa;AAEhC,UAAO,QAAQ,GAAG,WAAW,gBAAgB;AAK3C,QAJsB,aAAa,MAAM,WACvC,YAAY,WAAW,OAAO,CAC/B,IAEoB,YAAY,SAAS,OAAO,CAC/C,WAAU;KAEZ;;EAEL"}
1
+ {"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { Plugin } from \"vite\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n TypegenFatalError,\n TypegenSyntaxError,\n} from \"./index\";\nimport type { PreflightMode } from \"./preflight\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * How long the DEV background watcher waits for a STARTING warehouse to reach\n * RUNNING before giving up. Short relative to the CLI's preflight budget: this\n * is a best-effort \"regenerate once the warehouse warms up\" convenience, not a\n * gate, so we'd rather stop polling than hold a detached task open for minutes.\n */\nconst DEV_WAREHOUSE_WATCH_MAX_MS = 60_000;\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /** Folders to watch for changes. */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let watchFolders: string[];\n\n // Single-flight state for runGenerate(). `inFlight` is the promise of the\n // currently-running drain (null when idle); `queued` records that a trigger\n // arrived while a run was active so exactly ONE trailing run fires afterwards\n // (latest-wins — coalesces any number of overlapping triggers into a single\n // rerun). `queued` is read/cleared synchronously inside the drain loop so a\n // trigger landing in any window is caught before the drain exits.\n //\n // `pendingMode` is the mode the next generate should run in (latest-wins, like\n // `queued`): the foreground build runs non-blocking in dev (instant degrade)\n // while the background warehouse watch runs blocking (real DESCRIBEs). A\n // blocking watch trigger that lands while a non-blocking foreground run is in\n // flight therefore still describes when its trailing run fires.\n let inFlight: Promise<void> | null = null;\n let queued = false;\n let pendingMode: PreflightMode = \"non-blocking\";\n\n // The currently-armed DEV background warehouse watch, if any. Aborting it\n // stops a pending waitUntilRunning (server shutdown, or a newer arm replacing\n // an older one).\n let watchController: AbortController | null = null;\n\n /**\n * Generate types once in the given preflight {@link PreflightMode}. Never\n * throws in dev (logs instead); in production it rethrows so the build fails.\n * This is the un-guarded core — callers should go through {@link runGenerate}\n * so concurrent triggers can't race-write the .d.ts.\n *\n * @param mode - preflight policy for this run. The foreground build passes a\n * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it\n * degrades instantly); the background warehouse watch passes \"blocking\" so\n * its regenerate actually DESCRIBEs and lands real (non-degraded) types.\n */\n async function generateOnce(mode: PreflightMode) {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder: watchFolders[0],\n warehouseId,\n noCache: false,\n mode,\n });\n } catch (error) {\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // report in their message. Their stack frames and attached query arrays\n // point into appkit internals and only add noise, so surface just the\n // message — both when failing the prod build and when logging in dev.\n const isTypegenError =\n error instanceof TypegenSyntaxError ||\n error instanceof TypegenFatalError;\n\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n if (isTypegenError) error.stack = error.message;\n throw error;\n }\n\n if (isTypegenError) {\n logger.error(\"%s\", error.message);\n } else {\n logger.error(\"Error generating types: %O\", error);\n }\n }\n }\n\n /**\n * Single-flight wrapper around {@link generateOnce}. The initial build, the\n * .sql watcher, and the DEV warehouse watch all route through here so they can\n * never run typegen concurrently (which would race-write the .d.ts).\n *\n * If a run is already in flight, this does NOT start a second one — it records\n * the requested mode and sets a trailing flag so exactly one more run fires\n * after the current finishes, coalescing any number of overlapping triggers\n * (latest-wins, including the mode: a blocking watch trigger that arrives mid\n * non-blocking foreground run still describes when its trailing run fires).\n *\n * @param mode - preflight policy for this run. Recorded into `pendingMode`,\n * which the drain reads for each generate (latest trigger wins).\n * @returns A promise that resolves when this trigger's work (including any\n * trailing run it scheduled) has completed.\n */\n function runGenerate(mode: PreflightMode): Promise<void> {\n pendingMode = mode;\n\n if (inFlight) {\n // A run is active: remember that another trigger arrived and ride out the\n // current run. One trailing run then covers all coalesced triggers and\n // runs in the latest requested mode (recorded above).\n queued = true;\n return inFlight;\n }\n\n // Drain in a loop rather than recursing after a single queued-check: a\n // trigger can land in the window between generateOnce() resolving and the\n // check, so we re-test `queued` until it's clear. Critically, `inFlight` is\n // cleared synchronously in the SAME tick as the final `queued === false`\n // observation — never deferred to a .finally microtask — so there's no\n // window where a trigger sees `inFlight` set but the drain has already\n // decided to exit. The guard stays held for the whole drain, so concurrent\n // triggers only ever set the flag; they never start a parallel generate.\n const drain = async (): Promise<void> => {\n while (true) {\n queued = false;\n // Snapshot the mode synchronously alongside clearing `queued` so a\n // trigger landing during this generate is observed (via `queued`) on the\n // next loop with its own mode, not silently dropped.\n const runMode = pendingMode;\n await generateOnce(runMode);\n // Synchronous check + clear, atomic w.r.t. other (synchronous) callers.\n if (!queued) {\n inFlight = null;\n return;\n }\n }\n };\n\n inFlight = drain();\n return inFlight;\n }\n\n /**\n * DEV-only: get the warehouse to RUNNING in the background and regenerate with\n * real (non-degraded) types once it is — without blocking dev startup. The\n * foreground build only ever degrades in dev (instant `unknown`/cached types),\n * so this is what lands actual DESCRIBE results in the editor for EVERY\n * reachable warehouse state, not just one that happens to already be warm.\n *\n * Post-probe behaviour by state:\n * - RUNNING → describe right away (the dev foreground degraded, so a running\n * warehouse would otherwise never get real types — this is the case Phase 3\n * restores). `waitUntilRunning` returns immediately for an already-running\n * warehouse, then the blocking regenerate fires.\n * - STARTING → it's already coming up; just wait for RUNNING, then describe.\n * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.\n * - DELETED / DELETING → return (a deleted warehouse can't be started, and\n * blocking typegen would treat it as fatal); leave the degraded types.\n *\n * No-op in production or without a warehouse id. Replaces any previously-armed\n * watch (aborting it first). Fully self-contained: it never throws into the\n * caller and never re-arms itself. The whole lifecycle is abortable via the\n * shared {@link watchController} — its signal is threaded into\n * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the\n * regenerate routes through {@link runGenerate} so it can't race-write the\n * .d.ts with the foreground degrade or a `.sql` re-trigger.\n *\n * The regenerate runs in \"blocking\" mode (not the foreground's non-blocking)\n * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —\n * the whole point of warming the warehouse in the background.\n */\n function armWarehouseWatch(): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n if (!warehouseId) return;\n\n // Supersede any in-flight watch so we never run two concurrently.\n watchController?.abort();\n const controller = new AbortController();\n watchController = controller;\n const { signal } = controller;\n\n void (async () => {\n try {\n const client = new WorkspaceClient({});\n const state = await getWarehouseState(client, warehouseId);\n\n // A deleted/deleting warehouse can't be started and blocking typegen\n // would treat it as fatal — leave the degraded types and stop. Every\n // other state (including RUNNING) proceeds to wait-then-describe so the\n // dev editor gets real types, not just the foreground's degraded ones.\n if (state === \"DELETED\" || state === \"DELETING\") {\n return;\n }\n\n // Stopped/stopping won't reach RUNNING on its own — nudge it. RUNNING and\n // STARTING need no start (RUNNING is already up; STARTING is coming up),\n // so don't issue a redundant one. A failed start is non-fatal: give up\n // silently rather than throw out of the detached task (the developer\n // still has degraded/cached types).\n let startedByUs = false;\n if (state === \"STOPPED\" || state === \"STOPPING\") {\n try {\n logger.debug(\"Warehouse is %s; starting it.\", state);\n await startWarehouse(client, warehouseId);\n startedByUs = true;\n } catch {\n return;\n }\n }\n\n // Wait for RUNNING. For an already-RUNNING warehouse this returns on the\n // first poll; for STARTING/STOPPED it polls (abortably) until the\n // warehouse warms up, a terminal state, or the deadline.\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,\n signal,\n // We just issued the start, so the first poll(s) often still report\n // STOPPED/STOPPING before the start propagates. Poll through those\n // instead of bailing, or the regenerate would never fire. When we\n // didn't start it (RUNNING/STARTING branch), keep the default terminal\n // states.\n treatStoppedAsTransient: startedByUs,\n });\n\n if (final === \"RUNNING\" && !signal.aborted) {\n logger.debug(\"Warehouse is RUNNING; regenerating types.\");\n // Blocking: the warehouse is RUNNING now, so describe it and emit real\n // (non-degraded) types — unlike the foreground dev run, which degraded.\n // Routed through the single-flight guard so it coalesces with the\n // foreground degrade / any `.sql` re-trigger instead of racing them.\n await runGenerate(\"blocking\");\n }\n } catch {\n // Detached background task: any failure (timeout, abort, connectivity,\n // auth) is non-fatal — the developer still has degraded/cached types.\n }\n })();\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n if (!existsSync(path.join(process.cwd(), \"config\", \"queries\"))) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n watchFolders = options?.watchFolders ?? [\n path.join(process.cwd(), \"config\", \"queries\"),\n ];\n },\n\n buildStart() {\n // Production: block the build on this generate (and surface failures).\n // The watch is a dev-only no-op, so just run typegen.\n if (process.env.NODE_ENV === \"production\") {\n return runGenerate(\"blocking\");\n }\n\n // Dev: don't block startup waiting on typegen. The foreground generate runs\n // non-blocking — it skips the warehouse entirely and writes degraded\n // (cached/`unknown`) types instantly. Then arm the warehouse watch so the\n // warehouse gets a one-shot BLOCKING regenerate (real types) in the\n // background for EVERY reachable state: RUNNING describes right away, while\n // STARTING/STOPPED are waited (and started) until they reach RUNNING.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n if (isWatchedFile && changedFile.endsWith(\".sql\")) {\n // Route through the single-flight runner (was fire-and-forget\n // generate(), which could race the initial build / watch). This is a\n // dev-only hook, so degrade instantly (non-blocking), then re-arm the\n // warehouse watch so the edited query is re-described in the background\n // against the running warehouse (or once a still-starting one warms\n // up), landing fresh blocking-described types.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n }\n });\n\n // Tear down any pending warehouse watch when the dev server closes so a\n // long backoff can't keep the process alive after shutdown.\n server.httpServer?.once(\"close\", () => {\n watchController?.abort();\n });\n },\n };\n}\n"],"mappings":";;;;;;;;AAmBA,MAAM,SAAS,aAAa,6BAA6B;;;;;;;AAQzD,MAAM,6BAA6B;;;;;;;AAkBnC,SAAgB,kBAAkB,SAA4C;CAC5E,IAAI;CACJ,IAAI;CAcJ,IAAI,WAAiC;CACrC,IAAI,SAAS;CACb,IAAI,cAA6B;CAKjC,IAAI,kBAA0C;;;;;;;;;;;;CAa9C,eAAe,aAAa,MAAqB;AAC/C,MAAI;GACF,MAAM,cAAc,QAAQ,IAAI,2BAA2B;AAE3D,OAAI,CAAC,aAAa;AAChB,WAAO,MAAM,oDAAoD;AACjE;;AAGF,SAAM,uBAAuB;IAC3B;IACA,aAAa,aAAa;IAC1B;IACA,SAAS;IACT;IACD,CAAC;WACK,OAAO;GAKd,MAAM,iBACJ,iBAAiB,sBACjB,iBAAiB;AAGnB,OAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,QAAI,eAAgB,OAAM,QAAQ,MAAM;AACxC,UAAM;;AAGR,OAAI,eACF,QAAO,MAAM,MAAM,MAAM,QAAQ;OAEjC,QAAO,MAAM,8BAA8B,MAAM;;;;;;;;;;;;;;;;;;;CAqBvD,SAAS,YAAY,MAAoC;AACvD,gBAAc;AAEd,MAAI,UAAU;AAIZ,YAAS;AACT,UAAO;;EAWT,MAAM,QAAQ,YAA2B;AACvC,UAAO,MAAM;AACX,aAAS;AAKT,UAAM,aADU,YACW;AAE3B,QAAI,CAAC,QAAQ;AACX,gBAAW;AACX;;;;AAKN,aAAW,OAAO;AAClB,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCT,SAAS,oBAA0B;AACjC,MAAI,QAAQ,IAAI,aAAa,aAAc;EAE3C,MAAM,cAAc,QAAQ,IAAI,2BAA2B;AAC3D,MAAI,CAAC,YAAa;AAGlB,mBAAiB,OAAO;EACxB,MAAM,aAAa,IAAI,iBAAiB;AACxC,oBAAkB;EAClB,MAAM,EAAE,WAAW;AAEnB,GAAM,YAAY;AAChB,OAAI;IACF,MAAM,SAAS,IAAI,gBAAgB,EAAE,CAAC;IACtC,MAAM,QAAQ,MAAM,kBAAkB,QAAQ,YAAY;AAM1D,QAAI,UAAU,aAAa,UAAU,WACnC;IAQF,IAAI,cAAc;AAClB,QAAI,UAAU,aAAa,UAAU,WACnC,KAAI;AACF,YAAO,MAAM,iCAAiC,MAAM;AACpD,WAAM,eAAe,QAAQ,YAAY;AACzC,mBAAc;YACR;AACN;;AAkBJ,QAXc,MAAM,iBAAiB,QAAQ,aAAa;KACxD,OAAO;KACP;KAMA,yBAAyB;KAC1B,CAAC,KAEY,aAAa,CAAC,OAAO,SAAS;AAC1C,YAAO,MAAM,4CAA4C;AAKzD,WAAM,YAAY,WAAW;;WAEzB;MAIN;;AAGN,QAAO;EACL,MAAM;EAEN,QAAQ;AAGN,OAAI,EAFgB,QAAQ,IAAI,2BAA2B,KAEzC;AAChB,WAAO,MAAM,oDAAoD;AACjE,WAAO;;AAGT,OAAI,CAAC,WAAW,KAAK,KAAK,QAAQ,KAAK,EAAE,UAAU,UAAU,CAAC,CAC5D,QAAO;AAGT,UAAO;;EAGT,eAAe,QAAQ;GACrB,MAAM,cAAc,KAAK,QAAQ,OAAO,MAAM,KAAK;AACnD,aAAU,KAAK,QACb,aACA,SAAS,WAAW,UAAU,UAAU,GAAG,uBAC5C;AACD,kBAAe,SAAS,gBAAgB,CACtC,KAAK,KAAK,QAAQ,KAAK,EAAE,UAAU,UAAU,CAC9C;;EAGH,aAAa;AAGX,OAAI,QAAQ,IAAI,aAAa,aAC3B,QAAO,YAAY,WAAW;AAShC,GAAK,YAAY,eAAe;AAChC,sBAAmB;;EAGrB,gBAAgB,QAAQ;AACtB,UAAO,QAAQ,IAAI,aAAa;AAEhC,UAAO,QAAQ,GAAG,WAAW,gBAAgB;AAK3C,QAJsB,aAAa,MAAM,WACvC,YAAY,WAAW,OAAO,CAC/B,IAEoB,YAAY,SAAS,OAAO,EAAE;AAOjD,KAAK,YAAY,eAAe;AAChC,wBAAmB;;KAErB;AAIF,UAAO,YAAY,KAAK,eAAe;AACrC,qBAAiB,OAAO;KACxB;;EAEL"}
@@ -0,0 +1,116 @@
1
+ //#region src/type-generator/warehouse-status.ts
2
+ /** Backoff bounds for {@link waitUntilRunning}. */
3
+ const INITIAL_POLL_MS = 1e3;
4
+ const MAX_POLL_MS = 15e3;
5
+ /** States from which the warehouse will not transition to RUNNING on its own. */
6
+ const NOT_COMING_UP = new Set([
7
+ "STOPPED",
8
+ "STOPPING",
9
+ "DELETED",
10
+ "DELETING"
11
+ ]);
12
+ /**
13
+ * Terminal states even when {@link waitUntilRunning} is told to treat
14
+ * STOPPED/STOPPING as transient: a deleted (or deleting) warehouse genuinely
15
+ * can't reach RUNNING, so we still resolve with the observed state.
16
+ */
17
+ const NEVER_COMING_UP = new Set(["DELETED", "DELETING"]);
18
+ /**
19
+ * Sleep for `ms`, resolving early if `signal` aborts. The pending timer is
20
+ * always cleared (on resolve and on abort) so a long backoff can't keep the
21
+ * event loop alive after the caller has bailed.
22
+ */
23
+ function delay(ms, signal) {
24
+ return new Promise((resolve) => {
25
+ if (signal?.aborted) {
26
+ resolve();
27
+ return;
28
+ }
29
+ const timer = setTimeout(() => {
30
+ signal?.removeEventListener("abort", onAbort);
31
+ resolve();
32
+ }, ms);
33
+ function onAbort() {
34
+ clearTimeout(timer);
35
+ resolve();
36
+ }
37
+ signal?.addEventListener("abort", onAbort, { once: true });
38
+ });
39
+ }
40
+ /**
41
+ * Fetch the current lifecycle state of a SQL warehouse.
42
+ *
43
+ * Errors from the SDK (auth, bad warehouse id, connectivity) are intentionally
44
+ * NOT caught — the caller decides how to classify and react to them.
45
+ */
46
+ async function getWarehouseState(client, warehouseId) {
47
+ return (await client.warehouses.get({ id: warehouseId })).state;
48
+ }
49
+ /**
50
+ * Initiate a start of a stopped/stopping SQL warehouse.
51
+ *
52
+ * Only KICKS OFF the start: the SDK's `start()` returns a Waiter, but we
53
+ * deliberately do not `.wait()` on it. Blocking on the full cold-start isn't our
54
+ * job here — {@link waitUntilRunning} is the poller that watches the warehouse
55
+ * the rest of the way to RUNNING. We just nudge it out of the stopped state.
56
+ *
57
+ * Errors from the SDK (auth, bad warehouse id, connectivity) are intentionally
58
+ * NOT caught — the caller decides how to classify and react to them.
59
+ */
60
+ async function startWarehouse(client, warehouseId) {
61
+ await client.warehouses.start({ id: warehouseId });
62
+ }
63
+ /**
64
+ * Poll a warehouse until it reaches RUNNING, settles into a state it won't
65
+ * leave on its own, or a deadline elapses.
66
+ *
67
+ * Polling uses exponential backoff: the first wait is ~{@link INITIAL_POLL_MS},
68
+ * doubling on each subsequent poll up to a ~{@link MAX_POLL_MS} cap.
69
+ *
70
+ * Resolution:
71
+ * - Resolves `"RUNNING"` as soon as the warehouse is running.
72
+ * - Resolves with the observed state if it reaches a not-coming-up state
73
+ * (`STOPPED`/`STOPPING`/`DELETED`/`DELETING`) — the caller decides what to do.
74
+ *
75
+ * Set `opts.treatStoppedAsTransient` when the caller has just issued a start and
76
+ * a still-`STOPPED`/`STOPPING` reading is expected to be a stale pre-start blip
77
+ * rather than a settled state. With it on, those two states are polled through
78
+ * (like `STARTING`) until RUNNING, a genuinely terminal `DELETED`/`DELETING`, or
79
+ * the deadline — so an immediate post-start STOPPED reading no longer bails the
80
+ * wait. Off (default), STOPPED/STOPPING remain terminal and resolve as before.
81
+ *
82
+ * Pass `opts.signal` to abort an in-progress wait (e.g. a dev server shutting
83
+ * down): the next deadline/abort check throws an `AbortError`, and a pending
84
+ * backoff sleep resolves immediately rather than holding the process open.
85
+ *
86
+ * @throws Error if `maxMs` elapses before the warehouse reaches RUNNING.
87
+ * @throws Error (`name === "AbortError"`) if `opts.signal` is or becomes aborted.
88
+ */
89
+ async function waitUntilRunning(client, warehouseId, opts) {
90
+ const { maxMs, signal, treatStoppedAsTransient } = opts;
91
+ const start = Date.now();
92
+ let pollMs = opts.pollMs ?? INITIAL_POLL_MS;
93
+ const terminalStates = treatStoppedAsTransient ? NEVER_COMING_UP : NOT_COMING_UP;
94
+ while (true) {
95
+ throwIfAborted(signal);
96
+ const state = await getWarehouseState(client, warehouseId);
97
+ if (state === "RUNNING") return "RUNNING";
98
+ if (terminalStates.has(state)) return state;
99
+ if (Date.now() - start >= maxMs) throw new Error(`Warehouse ${warehouseId} did not reach RUNNING within ${maxMs}ms (last state: ${state})`);
100
+ await delay(pollMs, signal);
101
+ throwIfAborted(signal);
102
+ if (Date.now() - start >= maxMs) throw new Error(`Warehouse ${warehouseId} did not reach RUNNING within ${maxMs}ms (last state: ${state})`);
103
+ pollMs = Math.min(pollMs * 2, MAX_POLL_MS);
104
+ }
105
+ }
106
+ /** Throw a DOMException-style AbortError if the signal has been aborted. */
107
+ function throwIfAborted(signal) {
108
+ if (!signal?.aborted) return;
109
+ const error = /* @__PURE__ */ new Error("The warehouse wait was aborted.");
110
+ error.name = "AbortError";
111
+ throw error;
112
+ }
113
+
114
+ //#endregion
115
+ export { getWarehouseState, startWarehouse, waitUntilRunning };
116
+ //# sourceMappingURL=warehouse-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"warehouse-status.js","names":[],"sources":["../../src/type-generator/warehouse-status.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\n\n/**\n * Lifecycle states a SQL warehouse can report. Mirrors the Databricks SDK\n * `State` union; redeclared here so callers of this module don't need to reach\n * into the SDK's deep type paths.\n */\nexport type WarehouseState =\n | \"RUNNING\"\n | \"STARTING\"\n | \"STOPPED\"\n | \"STOPPING\"\n | \"DELETING\"\n | \"DELETED\";\n\n/** Backoff bounds for {@link waitUntilRunning}. */\nconst INITIAL_POLL_MS = 1000;\nconst MAX_POLL_MS = 15000;\n\n/** States from which the warehouse will not transition to RUNNING on its own. */\nconst NOT_COMING_UP: ReadonlySet<WarehouseState> = new Set<WarehouseState>([\n \"STOPPED\",\n \"STOPPING\",\n \"DELETED\",\n \"DELETING\",\n]);\n\n/**\n * Terminal states even when {@link waitUntilRunning} is told to treat\n * STOPPED/STOPPING as transient: a deleted (or deleting) warehouse genuinely\n * can't reach RUNNING, so we still resolve with the observed state.\n */\nconst NEVER_COMING_UP: ReadonlySet<WarehouseState> = new Set<WarehouseState>([\n \"DELETED\",\n \"DELETING\",\n]);\n\n/**\n * Sleep for `ms`, resolving early if `signal` aborts. The pending timer is\n * always cleared (on resolve and on abort) so a long backoff can't keep the\n * event loop alive after the caller has bailed.\n */\nfunction delay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve();\n return;\n }\n\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n\n function onAbort() {\n clearTimeout(timer);\n resolve();\n }\n\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/**\n * Fetch the current lifecycle state of a SQL warehouse.\n *\n * Errors from the SDK (auth, bad warehouse id, connectivity) are intentionally\n * NOT caught — the caller decides how to classify and react to them.\n */\nexport async function getWarehouseState(\n client: WorkspaceClient,\n warehouseId: string,\n): Promise<WarehouseState> {\n const response = await client.warehouses.get({ id: warehouseId });\n return response.state as WarehouseState;\n}\n\n/**\n * Initiate a start of a stopped/stopping SQL warehouse.\n *\n * Only KICKS OFF the start: the SDK's `start()` returns a Waiter, but we\n * deliberately do not `.wait()` on it. Blocking on the full cold-start isn't our\n * job here — {@link waitUntilRunning} is the poller that watches the warehouse\n * the rest of the way to RUNNING. We just nudge it out of the stopped state.\n *\n * Errors from the SDK (auth, bad warehouse id, connectivity) are intentionally\n * NOT caught — the caller decides how to classify and react to them.\n */\nexport async function startWarehouse(\n client: WorkspaceClient,\n warehouseId: string,\n): Promise<void> {\n await client.warehouses.start({ id: warehouseId });\n}\n\n/**\n * Poll a warehouse until it reaches RUNNING, settles into a state it won't\n * leave on its own, or a deadline elapses.\n *\n * Polling uses exponential backoff: the first wait is ~{@link INITIAL_POLL_MS},\n * doubling on each subsequent poll up to a ~{@link MAX_POLL_MS} cap.\n *\n * Resolution:\n * - Resolves `\"RUNNING\"` as soon as the warehouse is running.\n * - Resolves with the observed state if it reaches a not-coming-up state\n * (`STOPPED`/`STOPPING`/`DELETED`/`DELETING`) — the caller decides what to do.\n *\n * Set `opts.treatStoppedAsTransient` when the caller has just issued a start and\n * a still-`STOPPED`/`STOPPING` reading is expected to be a stale pre-start blip\n * rather than a settled state. With it on, those two states are polled through\n * (like `STARTING`) until RUNNING, a genuinely terminal `DELETED`/`DELETING`, or\n * the deadline — so an immediate post-start STOPPED reading no longer bails the\n * wait. Off (default), STOPPED/STOPPING remain terminal and resolve as before.\n *\n * Pass `opts.signal` to abort an in-progress wait (e.g. a dev server shutting\n * down): the next deadline/abort check throws an `AbortError`, and a pending\n * backoff sleep resolves immediately rather than holding the process open.\n *\n * @throws Error if `maxMs` elapses before the warehouse reaches RUNNING.\n * @throws Error (`name === \"AbortError\"`) if `opts.signal` is or becomes aborted.\n */\nexport async function waitUntilRunning(\n client: WorkspaceClient,\n warehouseId: string,\n opts: {\n maxMs: number;\n pollMs?: number;\n signal?: AbortSignal;\n treatStoppedAsTransient?: boolean;\n },\n): Promise<WarehouseState> {\n const { maxMs, signal, treatStoppedAsTransient } = opts;\n const start = Date.now();\n let pollMs = opts.pollMs ?? INITIAL_POLL_MS;\n\n // Which states end the wait early. When we've just issued a start, STOPPED and\n // STOPPING are expected stale readings, so only DELETED/DELETING stay terminal.\n const terminalStates = treatStoppedAsTransient\n ? NEVER_COMING_UP\n : NOT_COMING_UP;\n\n while (true) {\n throwIfAborted(signal);\n\n const state = await getWarehouseState(client, warehouseId);\n if (state === \"RUNNING\") return \"RUNNING\";\n if (terminalStates.has(state)) return state;\n\n if (Date.now() - start >= maxMs) {\n throw new Error(\n `Warehouse ${warehouseId} did not reach RUNNING within ${maxMs}ms (last state: ${state})`,\n );\n }\n\n await delay(pollMs, signal);\n throwIfAborted(signal);\n\n // Re-check the deadline after sleeping so we don't issue another poll past\n // the budget purely because we napped through it.\n if (Date.now() - start >= maxMs) {\n throw new Error(\n `Warehouse ${warehouseId} did not reach RUNNING within ${maxMs}ms (last state: ${state})`,\n );\n }\n\n pollMs = Math.min(pollMs * 2, MAX_POLL_MS);\n }\n}\n\n/** Throw a DOMException-style AbortError if the signal has been aborted. */\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (!signal?.aborted) return;\n const error = new Error(\"The warehouse wait was aborted.\");\n error.name = \"AbortError\";\n throw error;\n}\n"],"mappings":";;AAgBA,MAAM,kBAAkB;AACxB,MAAM,cAAc;;AAGpB,MAAM,gBAA6C,IAAI,IAAoB;CACzE;CACA;CACA;CACA;CACD,CAAC;;;;;;AAOF,MAAM,kBAA+C,IAAI,IAAoB,CAC3E,WACA,WACD,CAAC;;;;;;AAOF,SAAS,MAAM,IAAY,QAAqC;AAC9D,QAAO,IAAI,SAAS,YAAY;AAC9B,MAAI,QAAQ,SAAS;AACnB,YAAS;AACT;;EAGF,MAAM,QAAQ,iBAAiB;AAC7B,WAAQ,oBAAoB,SAAS,QAAQ;AAC7C,YAAS;KACR,GAAG;EAEN,SAAS,UAAU;AACjB,gBAAa,MAAM;AACnB,YAAS;;AAGX,UAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;GAC1D;;;;;;;;AASJ,eAAsB,kBACpB,QACA,aACyB;AAEzB,SADiB,MAAM,OAAO,WAAW,IAAI,EAAE,IAAI,aAAa,CAAC,EACjD;;;;;;;;;;;;;AAclB,eAAsB,eACpB,QACA,aACe;AACf,OAAM,OAAO,WAAW,MAAM,EAAE,IAAI,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BpD,eAAsB,iBACpB,QACA,aACA,MAMyB;CACzB,MAAM,EAAE,OAAO,QAAQ,4BAA4B;CACnD,MAAM,QAAQ,KAAK,KAAK;CACxB,IAAI,SAAS,KAAK,UAAU;CAI5B,MAAM,iBAAiB,0BACnB,kBACA;AAEJ,QAAO,MAAM;AACX,iBAAe,OAAO;EAEtB,MAAM,QAAQ,MAAM,kBAAkB,QAAQ,YAAY;AAC1D,MAAI,UAAU,UAAW,QAAO;AAChC,MAAI,eAAe,IAAI,MAAM,CAAE,QAAO;AAEtC,MAAI,KAAK,KAAK,GAAG,SAAS,MACxB,OAAM,IAAI,MACR,aAAa,YAAY,gCAAgC,MAAM,kBAAkB,MAAM,GACxF;AAGH,QAAM,MAAM,QAAQ,OAAO;AAC3B,iBAAe,OAAO;AAItB,MAAI,KAAK,KAAK,GAAG,SAAS,MACxB,OAAM,IAAI,MACR,aAAa,YAAY,gCAAgC,MAAM,kBAAkB,MAAM,GACxF;AAGH,WAAS,KAAK,IAAI,SAAS,GAAG,YAAY;;;;AAK9C,SAAS,eAAe,QAA4B;AAClD,KAAI,CAAC,QAAQ,QAAS;CACtB,MAAM,wBAAQ,IAAI,MAAM,kCAAkC;AAC1D,OAAM,OAAO;AACb,OAAM"}
@@ -73,6 +73,19 @@ npx @databricks/appkit generate-types [rootDir] [outFile] [warehouseId]
73
73
 
74
74
  ```
75
75
 
76
+ ### Warehouse readiness and the `--wait` flag[​](#warehouse-readiness-and-the---wait-flag "Direct link to warehouse-readiness-and-the---wait-flag")
77
+
78
+ By default, `generate-types` is **non-blocking**: it never waits on — or fails because of — your SQL warehouse. It writes the best types it can immediately (reusing cached types where the query is unchanged, otherwise `result: unknown`) and then spawns a detached background worker that refreshes the real types once the warehouse is ready. This keeps `npm install` (postinstall) and `npm run dev` (predev) fast and resilient to a cold or briefly-unreachable warehouse. The dev Vite plugin behaves the same way: types appear instantly and refresh in place once the warehouse is live.
79
+
80
+ Pass `--wait` for CI and production builds, where accurate types must be present before the build proceeds:
81
+
82
+ ```bash
83
+ npx @databricks/appkit generate-types --wait
84
+
85
+ ```
86
+
87
+ In blocking mode the generator starts a stopped warehouse, waits (bounded) for it to reach `RUNNING`, and then describes your queries. It fails only when the configured warehouse no longer exists (deleted/deleting), so a transient outage or a cold warehouse degrades gracefully rather than breaking the build. The app template wires this up for you: `postinstall` and `predev` run the non-blocking default, while `prebuild` runs `--wait`.
88
+
76
89
  ## How it works[​](#how-it-works "Direct link to How it works")
77
90
 
78
91
  The type generator: