@databricks/appkit 0.41.5 → 0.41.7

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.
Files changed (35) hide show
  1. package/README.md +3 -3
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/shared/src/schemas/metric-fqn.js +85 -0
  4. package/dist/shared/src/schemas/metric-fqn.js.map +1 -0
  5. package/dist/telemetry/noop.js +2 -1
  6. package/dist/telemetry/noop.js.map +1 -1
  7. package/dist/type-generator/cache.js +33 -1
  8. package/dist/type-generator/cache.js.map +1 -1
  9. package/dist/type-generator/errors.js +100 -0
  10. package/dist/type-generator/errors.js.map +1 -0
  11. package/dist/type-generator/index.js +190 -2
  12. package/dist/type-generator/index.js.map +1 -1
  13. package/dist/type-generator/mv-registry/config.js +131 -0
  14. package/dist/type-generator/mv-registry/config.js.map +1 -0
  15. package/dist/type-generator/mv-registry/describe.js +230 -0
  16. package/dist/type-generator/mv-registry/describe.js.map +1 -0
  17. package/dist/type-generator/mv-registry/metadata.js +40 -0
  18. package/dist/type-generator/mv-registry/metadata.js.map +1 -0
  19. package/dist/type-generator/mv-registry/render-types.js +118 -0
  20. package/dist/type-generator/mv-registry/render-types.js.map +1 -0
  21. package/dist/type-generator/mv-registry/sync.js +96 -0
  22. package/dist/type-generator/mv-registry/sync.js.map +1 -0
  23. package/dist/type-generator/query-registry.js +126 -88
  24. package/dist/type-generator/query-registry.js.map +1 -1
  25. package/dist/type-generator/statement-result.js +116 -0
  26. package/dist/type-generator/statement-result.js.map +1 -0
  27. package/dist/type-generator/types.js.map +1 -1
  28. package/dist/type-generator/vite-plugin.d.ts +12 -0
  29. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  30. package/dist/type-generator/vite-plugin.js +11 -5
  31. package/dist/type-generator/vite-plugin.js.map +1 -1
  32. package/docs/development/type-generation.md +4 -0
  33. package/docs/plugins/analytics.md +15 -0
  34. package/package.json +19 -18
  35. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"./migration\";\nimport type { PreflightMode } from \"./preflight\";\nimport { generateQueriesFromDescribe } from \"./query-registry\";\nimport { generateServingTypes as generateServingTypesImpl } from \"./serving/generator\";\nimport type { QueryFatalError, QuerySchema, QuerySyntaxError } from \"./types\";\n\ndotenv.config();\n\nconst logger = createLogger(\"type-generator\");\n\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\n}\n\nfunction formatFailureRows(\n label: string,\n queries: TypegenFailure[],\n color: (value: string) => string,\n) {\n if (queries.length === 0) return [];\n\n // Group by message so a shared failure — e.g. a warehouse-level fatal that\n // hits every query identically — prints once instead of repeating per row.\n const byMessage = new Map<string, string[]>();\n for (const { name, message } of queries) {\n const names = byMessage.get(message);\n if (names) names.push(name);\n else byMessage.set(message, [name]);\n }\n\n const maxNameLen = Math.max(...queries.map((query) => query.name.length));\n const tag = color(label.padEnd(7));\n const rows: string[] = [];\n for (const [message, names] of byMessage) {\n // Unique message → keep the compact one-line `tag name message` form.\n if (names.length === 1) {\n rows.push(\n ` ${tag} ${pc.bold(names[0].padEnd(maxNameLen))} ${pc.dim(message)}`,\n );\n continue;\n }\n // Shared message → print it once, then list the affected query names.\n rows.push(\n ` ${tag} ${pc.dim(message)} ${pc.dim(`(${names.length} ${plural(names.length, \"query\", \"queries\")})`)}`,\n );\n rows.push(\n ` ${names.map((name) => pc.bold(name)).join(pc.dim(\", \"))}`,\n );\n }\n return rows;\n}\n\nfunction formatTypegenFailureMessage(options: {\n syntaxErrors: QuerySyntaxError[];\n fatalErrors?: QueryFatalError[];\n warehouseId?: string;\n title: string;\n causes: string[];\n nextStep: string;\n}) {\n const { syntaxErrors, fatalErrors = [], warehouseId, title } = options;\n const total = syntaxErrors.length + fatalErrors.length;\n const separator = pc.dim(\"─\".repeat(60));\n const warehouse = warehouseId\n ? ` against ${pc.dim(`warehouse ${warehouseId}`)}`\n : \"\";\n\n return [\n ` ${pc.bold(pc.red(\"Type generation failed\"))}`,\n ` ${separator}`,\n ` ${title}: ${total} ${plural(total, \"query\", \"queries\")} could not be described${warehouse}.`,\n ` AppKit wrote generated types with ${pc.bold(\"result: unknown\")} for the failed ${plural(total, \"query\", \"queries\")}.`,\n \"\",\n ...formatFailureRows(\"SQL ERR\", syntaxErrors, pc.red),\n ...(syntaxErrors.length > 0 && fatalErrors.length > 0 ? [\"\"] : []),\n ...formatFailureRows(\"FATAL\", fatalErrors, pc.red),\n \"\",\n ` ${pc.bold(\"Common causes\")}`,\n ...options.causes.map((cause) => ` - ${cause}`),\n \"\",\n ` ${pc.bold(\"Next step\")}`,\n ` ${options.nextStep}`,\n ].join(\"\\n\");\n}\n\n/**\n * Thrown when one or more queries fail `DESCRIBE QUERY` against a *reachable*\n * warehouse — i.e. genuine SQL errors (bad table, syntax, incompatible type),\n * as opposed to a connectivity failure (warehouse unreachable), which degrades\n * silently. Whether this is fatal is the caller's decision: the Vite plugin and\n * CLI fail the build in production and warn-only in development.\n */\nexport class TypegenSyntaxError extends Error {\n readonly queries: QuerySyntaxError[];\n readonly fatalQueries: QueryFatalError[];\n\n constructor(\n queries: QuerySyntaxError[],\n warehouseId?: string,\n fatalQueries: QueryFatalError[] = [],\n ) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: queries,\n fatalErrors: fatalQueries,\n warehouseId,\n title: \"DESCRIBE QUERY failed\",\n causes: [\n \"SQL syntax errors\",\n \"missing tables or views\",\n \"warehouse format incompatibilities\",\n ],\n nextStep: warehouseId\n ? `Run each SQL ERR query directly in a Databricks SQL editor against warehouse ${pc.bold(warehouseId)}.`\n : \"Run each SQL ERR query directly in a Databricks SQL editor.\",\n }),\n );\n this.name = \"TypegenSyntaxError\";\n this.queries = queries;\n this.fatalQueries = fatalQueries;\n }\n}\n\n/**\n * Thrown when DESCRIBE QUERY could not be requested because of a non-SQL fatal\n * setup/request problem, such as missing permissions, invalid warehouse IDs, or\n * malformed SDK configuration. Like TypegenSyntaxError, this is thrown only\n * after the declaration file has been written with `result: unknown` entries.\n */\nexport class TypegenFatalError extends Error {\n readonly queries: QueryFatalError[];\n\n constructor(queries: QueryFatalError[], warehouseId?: string) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: [],\n fatalErrors: queries,\n warehouseId,\n title: \"DESCRIBE QUERY could not be requested\",\n causes: [\n \"missing warehouse permissions\",\n \"invalid warehouse ID\",\n \"authentication failure\",\n \"SDK configuration errors\",\n ],\n nextStep: warehouseId\n ? `Verify access to warehouse ${pc.bold(warehouseId)} and rerun type generation.`\n : \"Verify warehouse access and rerun type generation.\",\n }),\n );\n this.name = \"TypegenFatalError\";\n this.queries = queries;\n }\n}\n\n/**\n * Generate type declarations for QueryRegistry\n * Create the d.ts file from the plugin routes and query schemas\n * @param querySchemas - the list of query schemas\n * @returns - the type declarations as a string\n */\nfunction generateTypeDeclarations(querySchemas: QuerySchema[] = []): string {\n const queryEntries = querySchemas\n .map(({ name, type }) => {\n const indentedType = type\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : ` ${line}`))\n .join(\"\\n\");\n return ` ${name}: ${indentedType}`;\n })\n .join(\";\\n\");\n\n const querySection = queryEntries ? `\\n${queryEntries};\\n ` : \"\";\n\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\nimport type { SQLTypeMarker, SQLStringMarker, SQLNumberMarker, SQLBooleanMarker, SQLBinaryMarker, SQLDateMarker, SQLTimestampMarker } from \"@databricks/appkit-ui/js\";\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface QueryRegistry {${querySection}}\n}\n`;\n}\n\n/**\n * Entry point for generating type declarations from all imported files\n * @param options - the options for the generation\n * @param options.entryPoint - the entry point file\n * @param options.outFile - the output file\n * @param options.querySchemaFile - optional path to query schema file (e.g. config/queries/schema.ts)\n */\nexport async function generateFromEntryPoint(options: {\n outFile: string;\n queryFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n mode?: PreflightMode;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n } = options;\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n let fatalErrors: QueryFatalError[] = [];\n if (queryFolder) {\n const result = await generateQueriesFromDescribe(queryFolder, warehouseId, {\n noCache,\n mode,\n });\n queryRegistry = result.schemas;\n syntaxErrors = result.syntaxErrors ?? [];\n fatalErrors = result.fatalErrors ?? [];\n }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n\n // One-time migration: remove old generated file and patch project configs\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Types are always written above — including `result: unknown` for any query\n // that could not be described. Connectivity failures pass silently so a\n // transient warehouse outage never blocks a build; genuine SQL errors and\n // non-connectivity fatal request failures surface after the file write.\n if (syntaxErrors.length > 0) {\n throw new TypegenSyntaxError(syntaxErrors, warehouseId, fatalErrors);\n }\n if (fatalErrors.length > 0) {\n throw new TypegenFatalError(fatalErrors, warehouseId);\n }\n\n logger.debug(\"Type generation complete!\");\n}\n\n// Rolldown tree-shaking only preserves \"own exports\" (locally defined) — not re-exports.\n// A local binding ensures the serving vite plugin's import keeps this in the dependency graph,\n// mirroring how generateFromEntryPoint (also defined here) is preserved via the analytics vite plugin.\nexport const generateServingTypes = generateServingTypesImpl;\n\n/** Directory name for generated AppKit type declaration files. */\nexport const TYPES_DIR = \"appkit-types\";\n/** Default filename for analytics query type declarations. */\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\n/** Default filename for serving endpoint type declarations. */\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport dotenv from \"dotenv\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n isRevivableMetricCacheEntry,\n loadCache,\n type MetricCacheEntry,\n metricCacheHash,\n saveCache,\n} from \"./cache\";\nimport { getErrorDiagnostic, isConnectivityError } from \"./errors\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"./migration\";\nimport { readMetricConfig, resolveMetricConfig } from \"./mv-registry/config\";\nimport { createWorkspaceDescribeFetcher } from \"./mv-registry/describe\";\nimport { generateMetricsMetadataJson } from \"./mv-registry/metadata\";\nimport { generateMetricTypeDeclarations } from \"./mv-registry/render-types\";\nimport { emptyMetricSchema, syncMetrics } from \"./mv-registry/sync\";\nimport type {\n DescribeFetcher,\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n} from \"./mv-registry/types\";\nimport { decidePreflight, type PreflightMode } from \"./preflight\";\nimport { generateQueriesFromDescribe } from \"./query-registry\";\nimport { generateServingTypes as generateServingTypesImpl } from \"./serving/generator\";\nimport type { QueryFatalError, QuerySchema, QuerySyntaxError } from \"./types\";\nimport {\n getWarehouseState,\n startWarehouse,\n type WarehouseState,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\ndotenv.config();\n\nconst logger = createLogger(\"type-generator\");\n\n/**\n * Upper bound (~5 min) on how long the metric path's `blocking`-mode preflight\n * waits for a warehouse to reach RUNNING. Mirrors the query path's (unexported)\n * `PREFLIGHT_WAIT_MAX_MS` in query-registry.ts.\n */\nconst MV_PREFLIGHT_WAIT_MAX_MS = 300_000;\n\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\n}\n\nfunction formatFailureRows(\n label: string,\n queries: TypegenFailure[],\n color: (value: string) => string,\n) {\n if (queries.length === 0) return [];\n\n // Group by message so a shared failure — e.g. a warehouse-level fatal that\n // hits every query identically — prints once instead of repeating per row.\n const byMessage = new Map<string, string[]>();\n for (const { name, message } of queries) {\n const names = byMessage.get(message);\n if (names) names.push(name);\n else byMessage.set(message, [name]);\n }\n\n const maxNameLen = Math.max(...queries.map((query) => query.name.length));\n const tag = color(label.padEnd(7));\n const rows: string[] = [];\n for (const [message, names] of byMessage) {\n // Unique message → keep the compact one-line `tag name message` form.\n if (names.length === 1) {\n rows.push(\n ` ${tag} ${pc.bold(names[0].padEnd(maxNameLen))} ${pc.dim(message)}`,\n );\n continue;\n }\n // Shared message → print it once, then list the affected query names.\n rows.push(\n ` ${tag} ${pc.dim(message)} ${pc.dim(`(${names.length} ${plural(names.length, \"query\", \"queries\")})`)}`,\n );\n rows.push(\n ` ${names.map((name) => pc.bold(name)).join(pc.dim(\", \"))}`,\n );\n }\n return rows;\n}\n\nfunction formatTypegenFailureMessage(options: {\n syntaxErrors: QuerySyntaxError[];\n fatalErrors?: QueryFatalError[];\n warehouseId?: string;\n title: string;\n causes: string[];\n nextStep: string;\n}) {\n const { syntaxErrors, fatalErrors = [], warehouseId, title } = options;\n const total = syntaxErrors.length + fatalErrors.length;\n const separator = pc.dim(\"─\".repeat(60));\n const warehouse = warehouseId\n ? ` against ${pc.dim(`warehouse ${warehouseId}`)}`\n : \"\";\n\n return [\n ` ${pc.bold(pc.red(\"Type generation failed\"))}`,\n ` ${separator}`,\n ` ${title}: ${total} ${plural(total, \"query\", \"queries\")} could not be described${warehouse}.`,\n ` AppKit wrote generated types with ${pc.bold(\"result: unknown\")} for the failed ${plural(total, \"query\", \"queries\")}.`,\n \"\",\n ...formatFailureRows(\"SQL ERR\", syntaxErrors, pc.red),\n ...(syntaxErrors.length > 0 && fatalErrors.length > 0 ? [\"\"] : []),\n ...formatFailureRows(\"FATAL\", fatalErrors, pc.red),\n \"\",\n ` ${pc.bold(\"Common causes\")}`,\n ...options.causes.map((cause) => ` - ${cause}`),\n \"\",\n ` ${pc.bold(\"Next step\")}`,\n ` ${options.nextStep}`,\n ].join(\"\\n\");\n}\n\n/**\n * Thrown when one or more queries fail `DESCRIBE QUERY` against a *reachable*\n * warehouse — i.e. genuine SQL errors (bad table, syntax, incompatible type),\n * as opposed to a connectivity failure (warehouse unreachable), which degrades\n * silently. Whether this is fatal is the caller's decision: the Vite plugin and\n * CLI fail the build in production and warn-only in development.\n */\nexport class TypegenSyntaxError extends Error {\n readonly queries: QuerySyntaxError[];\n readonly fatalQueries: QueryFatalError[];\n\n constructor(\n queries: QuerySyntaxError[],\n warehouseId?: string,\n fatalQueries: QueryFatalError[] = [],\n ) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: queries,\n fatalErrors: fatalQueries,\n warehouseId,\n title: \"DESCRIBE QUERY failed\",\n causes: [\n \"SQL syntax errors\",\n \"missing tables or views\",\n \"warehouse format incompatibilities\",\n ],\n nextStep: warehouseId\n ? `Run each SQL ERR query directly in a Databricks SQL editor against warehouse ${pc.bold(warehouseId)}.`\n : \"Run each SQL ERR query directly in a Databricks SQL editor.\",\n }),\n );\n this.name = \"TypegenSyntaxError\";\n this.queries = queries;\n this.fatalQueries = fatalQueries;\n }\n}\n\n/**\n * Thrown when DESCRIBE QUERY could not be requested because of a non-SQL fatal\n * setup/request problem, such as missing permissions, invalid warehouse IDs, or\n * malformed SDK configuration. Like TypegenSyntaxError, this is thrown only\n * after the declaration file has been written with `result: unknown` entries.\n */\nexport class TypegenFatalError extends Error {\n readonly queries: QueryFatalError[];\n\n constructor(queries: QueryFatalError[], warehouseId?: string) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: [],\n fatalErrors: queries,\n warehouseId,\n title: \"DESCRIBE QUERY could not be requested\",\n causes: [\n \"missing warehouse permissions\",\n \"invalid warehouse ID\",\n \"authentication failure\",\n \"SDK configuration errors\",\n ],\n nextStep: warehouseId\n ? `Verify access to warehouse ${pc.bold(warehouseId)} and rerun type generation.`\n : \"Verify warehouse access and rerun type generation.\",\n }),\n );\n this.name = \"TypegenFatalError\";\n this.queries = queries;\n }\n}\n\n/**\n * Generate type declarations for QueryRegistry\n * Create the d.ts file from the plugin routes and query schemas\n * @param querySchemas - the list of query schemas\n * @returns - the type declarations as a string\n */\nfunction generateTypeDeclarations(querySchemas: QuerySchema[] = []): string {\n const queryEntries = querySchemas\n .map(({ name, type }) => {\n const indentedType = type\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : ` ${line}`))\n .join(\"\\n\");\n return ` ${name}: ${indentedType}`;\n })\n .join(\";\\n\");\n\n const querySection = queryEntries ? `\\n${queryEntries};\\n ` : \"\";\n\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\nimport type { SQLTypeMarker, SQLStringMarker, SQLNumberMarker, SQLBooleanMarker, SQLBinaryMarker, SQLDateMarker, SQLTimestampMarker } from \"@databricks/appkit-ui/js\";\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface QueryRegistry {${querySection}}\n}\n`;\n}\n\n/**\n * Status-only probe for the metric-view gate in {@link generateFromEntryPoint}:\n * what state is the warehouse in right now?\n *\n * Uses {@link getWarehouseState} (`warehouses.get`) — a read-only GET that can\n * never start the warehouse — unlike the metric DESCRIBE statements it guards,\n * whose execution auto-starts a stopped warehouse and waits on it.\n *\n * Returns the observed state so the gate can distinguish a transient\n * not-running state (STOPPED/STARTING/... → degraded entries that retry) from a\n * terminal one (DELETED/DELETING → degraded entries pinned sticky). Takes the\n * lazy client *getter* (not a client) so the probe also absorbs client\n * construction failure. A connectivity blip returns `undefined`, which the gate\n * reads as transient not-running; a deterministic failure (auth, bad id) is\n * re-thrown so the gate can classify it fatal rather than silently degrading.\n */\nasync function probeWarehouseState(\n getClient: () => WorkspaceClient,\n warehouseId: string,\n): Promise<WarehouseState | undefined> {\n try {\n return await getWarehouseState(getClient(), warehouseId);\n } catch (err) {\n // Connectivity blip → undefined (gate degrades, retries next pass). A\n // deterministic failure (auth, bad warehouse id, client construction) must\n // not masquerade as not-running — re-throw so the gate pins it fatal, the\n // same split the query path's preflight makes.\n if (isConnectivityError(err)) return undefined;\n throw err;\n }\n}\n\n/**\n * Entry point for generating type declarations from all imported files\n * @param options - the options for the generation\n * @param options.entryPoint - the entry point file\n * @param options.outFile - the output file\n * @param options.noCache - skip the typegen cache entirely: every query is\n * re-described, and the metric path ignores its cached schemas (every\n * configured key becomes describe-needed) and overwrites the cache's\n * `metrics` section with this pass's results.\n * @param options.mode - preflight policy (see {@link PreflightMode}), default\n * `\"non-blocking\"`. For queries, `\"non-blocking\"` never touches the\n * warehouse. For metric views it makes one status-only probe and DESCRIBEs\n * only when the warehouse is already RUNNING, otherwise emits permissive\n * degraded types immediately. `\"blocking\"` waits for / starts the warehouse\n * first, failing the build only for a deleted/deleting one.\n * @param options.mvOutFile - optional output file for the MetricRegistry\n * augmentation. Defaults to a sibling `metric.d.ts` file under the same\n * directory as `outFile`. Skipped entirely if `metric-views.json` is absent.\n * @param options.mvMetadataOutFile - optional output file for the\n * build-time semantic metadata JSON bundle (`metrics.metadata.json`).\n * Defaults to a sibling of `mvOutFile`. Skipped entirely if\n * `metric-views.json` is absent.\n * @param options.metricFetcher - optional DescribeFetcher used by\n * {@link syncMetrics} (tests inject a mock; production lazily builds a\n * default WorkspaceClient-backed one). An injected fetcher always runs: it\n * hits no warehouse, so it bypasses both the non-blocking gate and the\n * blocking preflight.\n */\nexport async function generateFromEntryPoint(options: {\n outFile: string;\n queryFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n mode?: PreflightMode;\n mvOutFile?: string;\n mvMetadataOutFile?: string;\n metricFetcher?: DescribeFetcher;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n mvOutFile,\n mvMetadataOutFile,\n metricFetcher,\n } = options;\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n let fatalErrors: QueryFatalError[] = [];\n if (queryFolder) {\n const result = await generateQueriesFromDescribe(queryFolder, warehouseId, {\n noCache,\n mode,\n });\n queryRegistry = result.schemas;\n syntaxErrors = result.syntaxErrors ?? [];\n fatalErrors = result.fatalErrors ?? [];\n }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n\n // Metric-view types: only emit when metric-views.json exists. The path is\n // purely additive — apps that never adopt metric views must not produce\n // empty noise.\n if (queryFolder) {\n const mvConfig = await readMetricConfig(queryFolder);\n if (mvConfig) {\n const resolution = resolveMetricConfig(mvConfig);\n\n // Metric schemas persist in the shared typegen cache as a `metrics`\n // section (sibling of `queries`, same file/version), keyed by metric key\n // with md5(\"<source>|<lane>\") as the change detector. Loaded strictly\n // AFTER the query path's own load → mutate → save cycle, so the single\n // metric-side save below can never clobber a query entry.\n const cache = await loadCache();\n\n // The section is consumed through a null-prototype copy: metric keys\n // are user-controlled config input and \"__proto__\" passes the metric\n // key regex — on a plain object, writing it would hit the\n // Object.prototype setter (mutating the object's prototype and silently\n // dropping the entry) instead of storing data. A null prototype also\n // keeps partition reads from resolving inherited names (\"constructor\",\n // \"toString\", ...) as phantom entries.\n const mvCacheSection: Record<string, MetricCacheEntry> =\n Object.create(null);\n if (!noCache && cache.metrics) {\n for (const key of Object.keys(cache.metrics)) {\n mvCacheSection[key] = cache.metrics[key];\n }\n }\n\n // Partition BEFORE any gate/preflight decision: a hit (structurally valid\n // entry, hash match, not retry-flagged) is served from cache no matter\n // what the warehouse is doing — a degraded pass falls back to\n // last-known-good schemas, exactly like queries degrade to cached types.\n // Only the remainder (new, edited, retry-flagged, or unrevivable entries)\n // is eligible for DESCRIBE, so a fully-warm pass makes zero warehouse\n // calls and constructs zero clients.\n const hitSchemas = new Map<string, MetricSchema>();\n const describeNeeded: typeof resolution.entries = [];\n // Degraded cached schemas pinned `retry: false` are sticky failures: they\n // serve their permissive schema like any hit, but are collected here for\n // the single notice below so the misconfiguration isn't silently hidden.\n const stickyDegradedHits: string[] = [];\n for (const entry of resolution.entries) {\n const prior = mvCacheSection[entry.key];\n if (\n prior !== undefined &&\n isRevivableMetricCacheEntry(prior) &&\n prior.hash === metricCacheHash(entry.source, entry.lane) &&\n !prior.retry\n ) {\n hitSchemas.set(entry.key, prior.schema);\n if (prior.schema.degraded === true) {\n stickyDegradedHits.push(entry.key);\n }\n } else {\n describeNeeded.push(entry);\n }\n }\n\n if (stickyDegradedHits.length > 0) {\n logger.warn(\n \"cached failure for %s — fix the entry in metric-views.json or run with --no-cache to retry.\",\n stickyDegradedHits.join(\", \"),\n );\n }\n\n // At most ONE WorkspaceClient per pass for the whole metric path: the\n // status probe, the blocking preflight, and the default DESCRIBE fetcher\n // share this lazily-created instance, so a pass that never contacts the\n // warehouse constructs zero clients.\n let mvClient: WorkspaceClient | undefined;\n const getMvClient = (): WorkspaceClient => {\n mvClient ??= new WorkspaceClient({});\n return mvClient;\n };\n\n // Blocking-mode preflight: ensure the warehouse is running before the\n // DESCRIBE batch (probe → decide → wait / start+wait; only\n // DELETED/DELETING is fatal). Deliberately split from the query path's\n // preflight — metric views may bind a different warehouse in future. Two\n // softenings vs the query preflight: a failed probe and a timed-out wait\n // are NOT fatal here — we fall through to syncMetrics, which classifies a\n // still-not-ready warehouse as degraded rather than failing the build.\n let preflightFatalMessage: string | undefined;\n if (\n mode === \"blocking\" &&\n metricFetcher === undefined &&\n describeNeeded.length > 0\n ) {\n try {\n const state = await getWarehouseState(getMvClient(), warehouseId);\n const decision = decidePreflight(state, mode);\n if (decision === \"fatal\") {\n preflightFatalMessage = `warehouse ${warehouseId} is ${state}`;\n } else if (decision === \"startWaitProceed\") {\n // treatStoppedAsTransient rides out the stale pre-start\n // STOPPED/STOPPING reading, same as the query preflight.\n await startWarehouse(getMvClient(), warehouseId);\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n treatStoppedAsTransient: true,\n });\n if (settled !== \"RUNNING\") {\n // With treatStoppedAsTransient, a non-RUNNING resolve is\n // exactly DELETED/DELETING — the warehouse was deleted while\n // we waited. Fatal, same as catching it at decision time.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n }\n } else if (decision === \"waitThenProceed\") {\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n });\n if (settled === \"DELETED\" || settled === \"DELETING\") {\n // Deleted mid-wait: fatal. A STOPPED/STOPPING resolve (this\n // wait runs without treatStoppedAsTransient) stays a soft\n // fall-through — a stopped warehouse is startable, so it\n // degrades and converges rather than failing the build.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n }\n }\n } catch (err) {\n // Connectivity blip: fall through to syncMetrics, whose DESCRIBEs\n // degrade a not-ready / unreachable warehouse rather than throwing. A\n // deterministic failure (auth, bad warehouse id, a timed-out start)\n // is fatal — surface it instead of stalling ~5 min against a\n // not-ready warehouse, mirroring the query path's preflight catch.\n if (!isConnectivityError(err)) {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n }\n }\n\n // Honor the non-blocking preflight contract (#406) for metric DESCRIBEs:\n // a `DESCRIBE TABLE EXTENDED ... AS JSON` waits up to 30s per key and\n // auto-starts a stopped warehouse — exactly what \"non-blocking\" promises\n // not to do. So one status-only probe (which can't start the warehouse)\n // decides whether to DESCRIBE now or emit degraded artifacts for a later\n // blocking run; it keeps the observed state so the skip can tell a\n // transient not-running warehouse from a terminal DELETED/DELETING one.\n let gateState: WarehouseState | undefined;\n let describeNow =\n metricFetcher !== undefined ||\n mode !== \"non-blocking\" ||\n describeNeeded.length === 0;\n if (!describeNow) {\n try {\n gateState = await probeWarehouseState(getMvClient, warehouseId);\n } catch (err) {\n // probeWarehouseState only throws on a deterministic failure (auth,\n // bad warehouse id) — a connectivity blip already returned undefined.\n // Pin it fatal through the same path as a fatal blocking preflight.\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n describeNow = gateState === \"RUNNING\";\n }\n\n let described: MetricSchema[];\n let failures: MetricSyncFailure[] = [];\n // True when this pass skipped DESCRIBE for a reason that can never\n // self-converge — a deleted/deleting warehouse (fatal preflight or gate\n // skip). The write site pins those degraded outcomes sticky.\n let terminalSkip = false;\n if (preflightFatalMessage !== undefined) {\n // Fatal preflight (deleted/deleting warehouse): fail like the query\n // path — skip DESCRIBE, emit degraded schemas so both artifacts are\n // still written, and record one fatal error per describe-needed key\n // (cache hits are unaffected). The end-of-run throw below surfaces them\n // after the writes. Terminal, so these entries are pinned sticky.\n described = describeNeeded.map(emptyMetricSchema);\n terminalSkip = true;\n for (const entry of describeNeeded) {\n fatalErrors.push({ name: entry.key, message: preflightFatalMessage });\n }\n } else if (describeNeeded.length === 0) {\n // Nothing left to describe — every configured key was a cache hit.\n // syncMetrics would be a no-op (and building its fetcher would\n // construct a client for nothing); artifacts regenerate from cache.\n described = [];\n } else if (describeNow) {\n const fetcher =\n metricFetcher ??\n createWorkspaceDescribeFetcher(getMvClient(), warehouseId);\n ({ schemas: described, failures } = await syncMetrics(\n { entries: describeNeeded },\n fetcher,\n ));\n\n // Surface DESCRIBE failures loudly: a misconfigured metric-views.json\n // would otherwise silently ship an empty entry that the runtime\n // fail-closed gate 503s in production. syncMetrics is log-free; this\n // caller is the single owner of failure logging.\n if (failures.length > 0) {\n for (const f of failures) {\n logger.warn(\n \"metric sync failed for %s (%s): %s\",\n f.key,\n f.source,\n f.reason,\n );\n }\n }\n\n // Degraded-but-not-failed keys: the warehouse answered with a\n // non-terminal state (stopped / cold-starting), so their schemas are\n // unknown — not errors. One summary line, no per-key warns; failed\n // keys are excluded (the warn loop above already reported them).\n const failedKeys = new Set(failures.map((f) => f.key));\n const degradedKeys = described\n .filter((s) => s.degraded && !failedKeys.has(s.key))\n .map((s) => s.key);\n if (degradedKeys.length > 0) {\n logger.info(\n \"Warehouse %s did not return schemas for %d metric view(s) (%s) — wrote degraded metric types (permissive); they will refresh once the warehouse is available.\",\n warehouseId,\n degradedKeys.length,\n degradedKeys.join(\", \"),\n );\n }\n } else {\n // Un-probed DESCRIBEs deliberately skipped, not failures: emit each\n // describe-needed key as a degraded schema (permissive types) so both\n // artifacts exist; cache hits keep serving last-known-good. A transient\n // state refreshes on a later RUNNING pass; a DELETED/DELETING probe is\n // terminal, so those keys are pinned sticky below.\n described = describeNeeded.map(emptyMetricSchema);\n terminalSkip = gateState === \"DELETED\" || gateState === \"DELETING\";\n logger.info(\n \"Warehouse %s is not running — wrote degraded metric types (permissive) for %d metric view(s) (%s); they will refresh once the warehouse is available.\",\n warehouseId,\n describeNeeded.length,\n describeNeeded.map((e) => e.key).join(\", \"),\n );\n }\n\n // Persist outcomes for exactly the keys this pass owned (the\n // describe-needed set); hits were partitioned out above and are never\n // rewritten, so a warehouse-down pass keeps last-known-good entries. A\n // successful DESCRIBE caches `retry: false`; a degraded outcome caches\n // `retry: true` only when re-describing could later succeed (non-terminal\n // state or transient failure), else sticky `retry: false`. One save per\n // pass; with `noCache` the section started empty, so it's overwritten.\n const failureByKey = new Map<string, MetricSyncFailure>();\n for (const failure of failures) {\n failureByKey.set(failure.key, failure);\n }\n for (let i = 0; i < describeNeeded.length; i++) {\n // syncMetrics (and both .map(emptyMetricSchema) branches) return\n // one schema per entry in entry order, so described[i] always\n // belongs to describeNeeded[i].\n const entry = describeNeeded[i];\n const failure = failureByKey.get(entry.key);\n mvCacheSection[entry.key] = {\n hash: metricCacheHash(entry.source, entry.lane),\n schema: described[i],\n retry:\n described[i].degraded === true &&\n !terminalSkip &&\n (failure === undefined || failure.transient === true),\n };\n }\n\n // Prune entries whose key is no longer configured, so a removed metric\n // doesn't haunt the cache file forever.\n const configuredKeys = new Set(resolution.entries.map((e) => e.key));\n let prunedCount = 0;\n for (const key of Object.keys(mvCacheSection)) {\n if (!configuredKeys.has(key)) {\n delete mvCacheSection[key];\n prunedCount++;\n }\n }\n\n // Save when this pass produced outcomes, bypassed the cache, or pruned\n // — a warm pass over a shrunk config has nothing to describe but must\n // still shrink the file.\n if (describeNeeded.length > 0 || noCache || prunedCount > 0) {\n cache.metrics = mvCacheSection;\n await saveCache(cache);\n }\n\n // Merge cached hits with fresh results back into config order\n // (resolution.entries order — the renderers sort internally where\n // determinism matters).\n const describedByKey = new Map<string, MetricSchema>();\n for (const schema of described) {\n describedByKey.set(schema.key, schema);\n }\n const mvSchemas = resolution.entries.map((entry) => {\n const schema =\n hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);\n if (schema !== undefined) return schema;\n // Defensive: every entry is either a cache hit or describe-needed (and\n // every describe-needed entry yields exactly one schema above), so this\n // should be unreachable. If the invariant ever breaks, warn loudly but\n // still emit a permissive degraded schema — the metric path never\n // crashes a build over a single entry.\n logger.warn(\n \"no schema resolved for metric key %s — emitting degraded types (should not happen)\",\n entry.key,\n );\n return emptyMetricSchema(entry);\n });\n\n const mvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n const mvDeclarations = generateMetricTypeDeclarations(mvSchemas);\n await fs.mkdir(path.dirname(mvFile), { recursive: true });\n await fs.writeFile(mvFile, mvDeclarations, \"utf-8\");\n\n // Emit the semantic-metadata JSON bundle alongside the .d.ts. The hook\n // imports this artifact (via a registration call from the consuming\n // app) and exposes the per-metric subset on its return value.\n const mvMetadataFile =\n mvMetadataOutFile ??\n path.join(path.dirname(mvFile), METRIC_METADATA_FILE);\n const metadataJson = generateMetricsMetadataJson(mvSchemas);\n await fs.mkdir(path.dirname(mvMetadataFile), { recursive: true });\n await fs.writeFile(mvMetadataFile, metadataJson, \"utf-8\");\n\n logger.debug(\n \"Wrote MetricRegistry augmentation + metadata bundle for %d metric(s)%s\",\n mvSchemas.length,\n failures.length > 0 ? ` (${failures.length} failure(s))` : \"\",\n );\n }\n }\n\n // One-time migration: remove old generated file and patch project configs\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Types are always written above — including `result: unknown` for any query\n // that could not be described. Connectivity failures pass silently so a\n // transient warehouse outage never blocks a build; genuine SQL errors and\n // non-connectivity fatal request failures surface after the file write.\n if (syntaxErrors.length > 0) {\n throw new TypegenSyntaxError(syntaxErrors, warehouseId, fatalErrors);\n }\n if (fatalErrors.length > 0) {\n throw new TypegenFatalError(fatalErrors, warehouseId);\n }\n\n logger.debug(\"Type generation complete!\");\n}\n\n// Rolldown tree-shaking only preserves \"own exports\" (locally defined) — not re-exports.\n// A local binding ensures the serving vite plugin's import keeps this in the dependency graph,\n// mirroring how generateFromEntryPoint (also defined here) is preserved via the analytics vite plugin.\nexport const generateServingTypes = generateServingTypesImpl;\n\n// Re-export the mv-registry types so consumers (CLI, the type-generator\n// .d.ts shim in `packages/shared`) can pick them up from this entry point —\n// the .d.ts shim documents these as part of the package's public surface.\nexport type {\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n};\n\n/** Directory name for generated AppKit type declaration files. */\nexport const TYPES_DIR = \"appkit-types\";\n/** Default filename for analytics query type declarations. */\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\n/** Default filename for serving endpoint type declarations. */\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\n/** Default filename for metric-view registry type declarations. */\nexport const METRIC_TYPES_FILE = \"metric.d.ts\";\n/**\n * Default filename for the build-time semantic-metadata JSON bundle, sibling of\n * {@link METRIC_TYPES_FILE}. Shape is `Record<metricKey, { measures,\n * dimensions }>` (UC FQN and execution lane are server-side concerns, kept out\n * of this client-shipped artifact). The consuming app imports it at build time\n * and registers it via `@databricks/appkit-ui/format`'s\n * `registerMetricsMetadata()`, so the React hook returns per-metric `metadata`\n * without a second network round-trip.\n */\nexport const METRIC_METADATA_FILE = \"metrics.metadata.json\";\n"],"mappings":"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"}
@@ -0,0 +1,131 @@
1
+ import { MAX_UC_OBJECT_NAME_LENGTH, UC_FQN_PATTERN } from "../../shared/src/schemas/metric-fqn.js";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ //#region src/type-generator/mv-registry/config.ts
6
+ const MV_CONFIG_FILE = "metric-views.json";
7
+ /**
8
+ * Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog
9
+ * limit. Enforced by {@link resolveMetricConfig}.
10
+ */
11
+ const MAX_METRIC_VIEWS = 200;
12
+ /** Per-segment cap = UC's object-name length limit (255). */
13
+ const MAX_FQN_SEGMENT_LENGTH = MAX_UC_OBJECT_NAME_LENGTH;
14
+ /** Whole-FQN cap: three max-length segments plus the two separating dots. */
15
+ const MAX_FQN_LENGTH = MAX_FQN_SEGMENT_LENGTH * 3 + 2;
16
+ /** A metric view FQN is exactly catalog.schema.metric_view. */
17
+ const FQN_SEGMENT_NAMES = [
18
+ "catalog",
19
+ "schema",
20
+ "metric_view"
21
+ ];
22
+ const FQN_SEGMENT_COUNT = FQN_SEGMENT_NAMES.length;
23
+ /**
24
+ * Locale-independent comparator (UTF-16 code-unit order) shared by BOTH artifact
25
+ * key orderings. Plain `sort()` is locale-sensitive, so keys could order
26
+ * differently across environments and invalidate the cache hash — this keeps the
27
+ * ordering stable everywhere.
28
+ */
29
+ function compareKeys(a, b) {
30
+ return a < b ? -1 : a > b ? 1 : 0;
31
+ }
32
+ /**
33
+ * Read {@link MV_CONFIG_FILE} from a queries folder.
34
+ *
35
+ * Returns `null` if the file does not exist (the metric-view path is
36
+ * additive — apps without metric-views.json must not be penalized). There is
37
+ * deliberately no fallback to the legacy `metric.json` filename.
38
+ *
39
+ * Throws on JSON parse errors so misconfiguration surfaces loudly.
40
+ */
41
+ async function readMetricConfig(queryFolder) {
42
+ const metricPath = path.join(queryFolder, MV_CONFIG_FILE);
43
+ let raw;
44
+ try {
45
+ raw = await fs.readFile(metricPath, "utf8");
46
+ } catch (err) {
47
+ if (err.code === "ENOENT") return null;
48
+ throw err;
49
+ }
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(raw);
53
+ } catch (err) {
54
+ throw new Error(`Failed to parse metric-views.json at ${metricPath}: ${err.message}`);
55
+ }
56
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`Invalid metric-views.json at ${metricPath}: expected an object with a 'metricViews' map.`);
57
+ return parsed;
58
+ }
59
+ /**
60
+ * Validate a key against the JSON Schema's metricKey pattern. Kept
61
+ * lightweight — the shared Zod schema ({@link metricSourceSchema}) is the
62
+ * canonical contract for IDE/CI; this regex is identical to its
63
+ * {@link metricKeySchema}.
64
+ */
65
+ function isValidMetricKey(key) {
66
+ return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
67
+ }
68
+ /**
69
+ * Total predicate: is `fqn` a well-formed three-part UC metric view FQN?
70
+ *
71
+ * Well-formed = exactly three non-empty, dot-separated segments, each a valid
72
+ * Unity Catalog object name per the shared {@link UC_FQN_PATTERN} (the single
73
+ * source of truth, also used by the canonical Zod schema). Used as the
74
+ * defense-in-depth re-check at the describe fetcher seam; {@link resolveMetricConfig}
75
+ * runs the same checks but with specific, staged error messages.
76
+ *
77
+ * @note Segment length ({@link MAX_FQN_SEGMENT_LENGTH}) is NOT checked here —
78
+ * an over-long but otherwise legal name is still "valid shape". The length cap
79
+ * is a separate concern enforced (with its own message) in resolveMetricConfig.
80
+ */
81
+ function isValidFqn(fqn) {
82
+ const segments = fqn.split(".");
83
+ if (segments.length !== FQN_SEGMENT_COUNT) return false;
84
+ return segments.every((segment) => UC_FQN_PATTERN.test(segment));
85
+ }
86
+ /**
87
+ * Field allowlists enforced by {@link resolveMetricConfig}.
88
+ */
89
+ const ALLOWED_TOP_LEVEL_FIELDS = new Set(["$schema", "metricViews"]);
90
+ const ALLOWED_ENTRY_FIELDS = new Set(["source", "executor"]);
91
+ /**
92
+ * Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.
93
+ */
94
+ function resolveMetricConfig(config) {
95
+ for (const field of Object.keys(config)) if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) throw new Error(`Invalid top-level field "${field}" in metric-views.json: only '$schema' and 'metricViews' are allowed.`);
96
+ const metricViews = config.metricViews === void 0 ? {} : config.metricViews;
97
+ if (typeof metricViews !== "object" || metricViews === null || Array.isArray(metricViews)) throw new Error(`Invalid 'metricViews' in metric-views.json: expected an object map of metric entries.`);
98
+ const entries = [];
99
+ const sortedKeys = Object.keys(metricViews).sort(compareKeys);
100
+ if (sortedKeys.length > MAX_METRIC_VIEWS) throw new Error(`Invalid 'metricViews' in metric-views.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`);
101
+ for (const key of sortedKeys) {
102
+ if (!isValidMetricKey(key)) throw new Error(`Invalid metric key "${key}" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`);
103
+ const entry = metricViews[key];
104
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`Invalid metric entry "${key}": expected an object with a 'source' field.`);
105
+ for (const field of Object.keys(entry)) if (!ALLOWED_ENTRY_FIELDS.has(field)) throw new Error(`Invalid field "${field}" on metric entry "${key}": only 'source' and 'executor' are allowed at v1.`);
106
+ if (typeof entry.source !== "string" || entry.source.trim() === "") throw new Error(`Invalid metric entry "${key}": 'source' must be a non-empty string.`);
107
+ if (entry.source.length > MAX_FQN_LENGTH) throw new Error(`Invalid metric source for "${key}": FQN is ${entry.source.length} characters, exceeding the maximum of ${MAX_FQN_LENGTH}.`);
108
+ const segments = entry.source.split(".");
109
+ if (segments.length !== FQN_SEGMENT_COUNT) throw new Error(`Invalid metric source "${entry.source}" for "${key}": expected a three-part UC FQN <catalog>.<schema>.<metric_view> (got ${segments.length} dot-separated part${segments.length === 1 ? "" : "s"}). A catalog, schema, or metric view name cannot itself contain a dot.`);
110
+ for (let i = 0; i < segments.length; i++) {
111
+ const segment = segments[i];
112
+ const segmentName = FQN_SEGMENT_NAMES[i];
113
+ if (segment.length === 0) throw new Error(`Invalid metric source "${entry.source}" for "${key}": the ${segmentName} part is empty. A three-part UC FQN needs a non-empty name in each position: <catalog>.<schema>.<metric_view>.`);
114
+ if (segment.length > MAX_FQN_SEGMENT_LENGTH) throw new Error(`Invalid metric source for "${key}": the ${segmentName} segment is ${segment.length} characters, exceeding the maximum of ${MAX_FQN_SEGMENT_LENGTH} per segment.`);
115
+ if (!UC_FQN_PATTERN.test(segment)) throw new Error(`Invalid metric source "${entry.source}" for "${key}": the ${segmentName} part "${segment}" contains a character Unity Catalog does not allow in an object name (no spaces, '/', or control characters).`);
116
+ }
117
+ const executor = entry.executor;
118
+ if (executor !== void 0 && executor !== "app_service_principal" && executor !== "user") throw new Error(`Invalid executor "${String(executor)}" on metric entry "${key}": must be "app_service_principal" or "user".`);
119
+ const lane = executor === "user" ? "obo" : "sp";
120
+ entries.push({
121
+ key,
122
+ source: entry.source,
123
+ lane
124
+ });
125
+ }
126
+ return { entries };
127
+ }
128
+
129
+ //#endregion
130
+ export { compareKeys, isValidFqn, readMetricConfig, resolveMetricConfig };
131
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":[],"sources":["../../../src/type-generator/mv-registry/config.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n// Plain, zod-free value imports — single source of truth for the UC object-name\n// grammar. The Zod schema (packages/shared/src/schemas/metric-source.ts) imports\n// the SAME UC_FQN_PATTERN for its `source` .regex(...), so the runtime and the\n// canonical schema validate identically without the type-generator pulling the\n// shared Zod schema package into its runtime path (locked dependency-graph\n// ruling — see the comment in ../cache.ts). The relative specifier resolves the\n// shared source directly and drags in no zod.\nimport {\n MAX_UC_OBJECT_NAME_LENGTH,\n UC_FQN_PATTERN,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport type {\n MetricConfigResolution,\n MetricLane,\n MetricSourceConfig,\n ResolvedMetricEntry,\n} from \"./types\";\n\nconst MV_CONFIG_FILE = \"metric-views.json\";\n\n/**\n * Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog\n * limit. Enforced by {@link resolveMetricConfig}.\n */\nconst MAX_METRIC_VIEWS = 200;\n/** Per-segment cap = UC's object-name length limit (255). */\nconst MAX_FQN_SEGMENT_LENGTH = MAX_UC_OBJECT_NAME_LENGTH;\n/** Whole-FQN cap: three max-length segments plus the two separating dots. */\nconst MAX_FQN_LENGTH = MAX_FQN_SEGMENT_LENGTH * 3 + 2;\n/** A metric view FQN is exactly catalog.schema.metric_view. */\nconst FQN_SEGMENT_NAMES = [\"catalog\", \"schema\", \"metric_view\"] as const;\nconst FQN_SEGMENT_COUNT = FQN_SEGMENT_NAMES.length;\n\n/**\n * Locale-independent comparator (UTF-16 code-unit order) shared by BOTH artifact\n * key orderings. Plain `sort()` is locale-sensitive, so keys could order\n * differently across environments and invalidate the cache hash — this keeps the\n * ordering stable everywhere.\n */\nexport function compareKeys(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Read {@link MV_CONFIG_FILE} from a queries folder.\n *\n * Returns `null` if the file does not exist (the metric-view path is\n * additive — apps without metric-views.json must not be penalized). There is\n * deliberately no fallback to the legacy `metric.json` filename.\n *\n * Throws on JSON parse errors so misconfiguration surfaces loudly.\n */\nexport async function readMetricConfig(\n queryFolder: string,\n): Promise<MetricSourceConfig | null> {\n const metricPath = path.join(queryFolder, MV_CONFIG_FILE);\n let raw: string;\n try {\n raw = await fs.readFile(metricPath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `Failed to parse metric-views.json at ${metricPath}: ${(err as Error).message}`,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\n `Invalid metric-views.json at ${metricPath}: expected an object with a 'metricViews' map.`,\n );\n }\n\n return parsed as MetricSourceConfig;\n}\n\n/**\n * Validate a key against the JSON Schema's metricKey pattern. Kept\n * lightweight — the shared Zod schema ({@link metricSourceSchema}) is the\n * canonical contract for IDE/CI; this regex is identical to its\n * {@link metricKeySchema}.\n */\nfunction isValidMetricKey(key: string): boolean {\n return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);\n}\n\n/**\n * Total predicate: is `fqn` a well-formed three-part UC metric view FQN?\n *\n * Well-formed = exactly three non-empty, dot-separated segments, each a valid\n * Unity Catalog object name per the shared {@link UC_FQN_PATTERN} (the single\n * source of truth, also used by the canonical Zod schema). Used as the\n * defense-in-depth re-check at the describe fetcher seam; {@link resolveMetricConfig}\n * runs the same checks but with specific, staged error messages.\n *\n * @note Segment length ({@link MAX_FQN_SEGMENT_LENGTH}) is NOT checked here —\n * an over-long but otherwise legal name is still \"valid shape\". The length cap\n * is a separate concern enforced (with its own message) in resolveMetricConfig.\n */\nexport function isValidFqn(fqn: string): boolean {\n const segments = fqn.split(\".\");\n if (segments.length !== FQN_SEGMENT_COUNT) {\n return false;\n }\n return segments.every((segment) => UC_FQN_PATTERN.test(segment));\n}\n\n/**\n * Field allowlists enforced by {@link resolveMetricConfig}.\n */\nconst ALLOWED_TOP_LEVEL_FIELDS = new Set([\"$schema\", \"metricViews\"]);\nconst ALLOWED_ENTRY_FIELDS = new Set([\"source\", \"executor\"]);\n\n/**\n * Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.\n */\nexport function resolveMetricConfig(\n config: MetricSourceConfig,\n): MetricConfigResolution {\n for (const field of Object.keys(config)) {\n if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) {\n throw new Error(\n `Invalid top-level field \"${field}\" in metric-views.json: only '$schema' and 'metricViews' are allowed.`,\n );\n }\n }\n\n // Default to {} only when metricViews is genuinely absent. A `null` must fall\n // through to the type check below and throw — the canonical Zod schema rejects\n // null.\n const metricViews =\n config.metricViews === undefined ? {} : config.metricViews;\n if (\n typeof metricViews !== \"object\" ||\n metricViews === null ||\n Array.isArray(metricViews)\n ) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: expected an object map of metric entries.`,\n );\n }\n\n const entries: ResolvedMetricEntry[] = [];\n const sortedKeys = Object.keys(metricViews).sort(compareKeys);\n if (sortedKeys.length > MAX_METRIC_VIEWS) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`,\n );\n }\n for (const key of sortedKeys) {\n if (!isValidMetricKey(key)) {\n throw new Error(\n `Invalid metric key \"${key}\" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`,\n );\n }\n\n const entry = metricViews[key];\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new Error(\n `Invalid metric entry \"${key}\": expected an object with a 'source' field.`,\n );\n }\n\n for (const field of Object.keys(entry)) {\n if (!ALLOWED_ENTRY_FIELDS.has(field)) {\n throw new Error(\n `Invalid field \"${field}\" on metric entry \"${key}\": only 'source' and 'executor' are allowed at v1.`,\n );\n }\n }\n\n if (typeof entry.source !== \"string\" || entry.source.trim() === \"\") {\n throw new Error(\n `Invalid metric entry \"${key}\": 'source' must be a non-empty string.`,\n );\n }\n\n if (entry.source.length > MAX_FQN_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": FQN is ${entry.source.length} characters, exceeding the maximum of ${MAX_FQN_LENGTH}.`,\n );\n }\n\n // Staged, specific validation against the UC object-name grammar\n // (UC_FQN_PATTERN — shared with the canonical Zod schema). Reported in\n // order of increasing specificity so the message names the exact problem.\n const segments = entry.source.split(\".\");\n\n // Arity: exactly catalog.schema.metric_view. A wrong part count almost\n // always means a name contains a dot — which the dotted `source` cannot\n // express, since every dot is a segment boundary.\n if (segments.length !== FQN_SEGMENT_COUNT) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view> (got ${segments.length} dot-separated part${segments.length === 1 ? \"\" : \"s\"}). A catalog, schema, or metric view name cannot itself contain a dot.`,\n );\n }\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n const segmentName = FQN_SEGMENT_NAMES[i];\n\n // Empty part: a leading/trailing/double dot (e.g. \"a..c\", \".b.c\").\n if (segment.length === 0) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part is empty. A three-part UC FQN needs a non-empty name in each position: <catalog>.<schema>.<metric_view>.`,\n );\n }\n\n // Length cap (UC: object names are at most 255 characters).\n if (segment.length > MAX_FQN_SEGMENT_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": the ${segmentName} segment is ${segment.length} characters, exceeding the maximum of ${MAX_FQN_SEGMENT_LENGTH} per segment.`,\n );\n }\n\n // Character set: must be a valid UC object name (the FQN is always\n // backtick-quoted before it reaches SQL, so UC's *delimited* identifier\n // rules apply — anything but space, '/', and control characters).\n if (!UC_FQN_PATTERN.test(segment)) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part \"${segment}\" contains a character Unity Catalog does not allow in an object name (no spaces, '/', or control characters).`,\n );\n }\n }\n\n const executor = entry.executor;\n if (\n executor !== undefined &&\n executor !== \"app_service_principal\" &&\n executor !== \"user\"\n ) {\n throw new Error(\n `Invalid executor \"${String(executor)}\" on metric entry \"${key}\": must be \"app_service_principal\" or \"user\".`,\n );\n }\n\n const lane: MetricLane = executor === \"user\" ? \"obo\" : \"sp\";\n\n entries.push({ key, source: entry.source, lane });\n }\n\n return { entries };\n}\n"],"mappings":"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"}
@@ -0,0 +1,230 @@
1
+ import { isValidFqn } from "./config.js";
2
+ import { describeAdaptive } from "../statement-result.js";
3
+
4
+ //#region src/type-generator/mv-registry/describe.ts
5
+ /**
6
+ * Parse the JSON payload returned by DESCRIBE TABLE EXTENDED ... AS JSON.
7
+ *
8
+ * The Statement Execution API returns a single string cell — this normalizer
9
+ * unwraps it. Handles both the production (real warehouse) shape and the
10
+ * shape produced by mocked test responses.
11
+ */
12
+ function parseDescribeTableExtendedJson(response) {
13
+ if (response.status?.state === "FAILED") {
14
+ const msg = response.status.error?.message ?? "DESCRIBE failed";
15
+ throw new Error(`DESCRIBE TABLE EXTENDED failed: ${msg}`);
16
+ }
17
+ const rows = response.result?.data_array ?? [];
18
+ if (rows.length === 0) throw new Error("DESCRIBE TABLE EXTENDED returned no rows. Verify the FQN points to a metric view.");
19
+ const cell = rows[0]?.[0];
20
+ if (typeof cell !== "string") throw new Error("DESCRIBE TABLE EXTENDED first cell was not a JSON string. Confirm the AS JSON suffix is supported.");
21
+ try {
22
+ return JSON.parse(cell);
23
+ } catch (err) {
24
+ throw new Error(`Failed to parse DESCRIBE TABLE EXTENDED JSON: ${err.message}`);
25
+ }
26
+ }
27
+ /**
28
+ * Pure function: turn the parsed DESCRIBE JSON into structured column metadata.
29
+ *
30
+ * Tolerant of multiple JSON shapes (the field may be `columns` or
31
+ * `schema.fields`, type may be a string or `{ name }` object, the measure
32
+ * marker may be `is_measure` or under `metadata.is_measure`).
33
+ */
34
+ function extractMetricColumns(parsed) {
35
+ if (!parsed || typeof parsed !== "object") return [];
36
+ const root = parsed;
37
+ const columnsCandidate = root.columns ?? (root.schema && typeof root.schema === "object" ? root.schema.fields : void 0);
38
+ if (!Array.isArray(columnsCandidate)) return [];
39
+ const columns = [];
40
+ for (const raw of columnsCandidate) {
41
+ if (!raw || typeof raw !== "object") continue;
42
+ const obj = raw;
43
+ const name = typeof obj.name === "string" ? obj.name : typeof obj.column_name === "string" ? obj.column_name : void 0;
44
+ if (!name) continue;
45
+ const typeRaw = obj.type ?? obj.data_type ?? obj.type_name;
46
+ let type = "STRING";
47
+ if (typeof typeRaw === "string") type = typeRaw;
48
+ else if (typeRaw && typeof typeRaw === "object") {
49
+ const inner = typeRaw.name;
50
+ if (typeof inner === "string") type = inner;
51
+ }
52
+ let isMeasure = false;
53
+ if (typeof obj.is_measure === "boolean") isMeasure = obj.is_measure;
54
+ else if (obj.metadata && typeof obj.metadata === "object" && typeof obj.metadata.is_measure === "boolean") isMeasure = obj.metadata.is_measure;
55
+ else if (obj.kind === "measure" || obj.role === "measure") isMeasure = true;
56
+ const description = typeof obj.comment === "string" ? obj.comment : typeof obj.description === "string" ? obj.description : void 0;
57
+ const displayName = extractStringFromAny(obj, ["display_name", "displayName"]);
58
+ const format = extractFormatString(obj);
59
+ const timeGrains = isMeasure ? void 0 : inferTimeGrains(type);
60
+ columns.push({
61
+ name,
62
+ type,
63
+ isMeasure,
64
+ description,
65
+ ...displayName ? { displayName } : {},
66
+ ...format ? { format } : {},
67
+ ...timeGrains ? { timeGrains } : {}
68
+ });
69
+ }
70
+ return columns;
71
+ }
72
+ /**
73
+ * Read a non-empty string attribute from a DESCRIBE column entry, tolerating
74
+ * the multiple shapes UC has shipped for this metadata over time.
75
+ */
76
+ function extractStringFromAny(obj, candidates) {
77
+ for (const key of candidates) {
78
+ const direct = obj[key];
79
+ if (typeof direct === "string" && direct.trim().length > 0) return direct;
80
+ const meta = obj.metadata;
81
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
82
+ const nested = meta[key];
83
+ if (typeof nested === "string" && nested.trim().length > 0) return nested;
84
+ }
85
+ }
86
+ }
87
+ /**
88
+ * Read the column's `format` attribute from a DESCRIBE entry and return a
89
+ * printf-like format string suitable for `formatValue` and `toD3Format`.
90
+ */
91
+ function extractFormatString(obj) {
92
+ for (const key of ["format", "format_spec"]) {
93
+ const direct = obj[key];
94
+ const fromDirect = formatStringFromValue(direct);
95
+ if (fromDirect) return fromDirect;
96
+ const meta = obj.metadata;
97
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
98
+ const nested = meta[key];
99
+ const fromMeta = formatStringFromValue(nested);
100
+ if (fromMeta) return fromMeta;
101
+ }
102
+ }
103
+ }
104
+ function formatStringFromValue(value) {
105
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
106
+ if (value && typeof value === "object" && !Array.isArray(value)) return translateStructuredFormat(value);
107
+ }
108
+ /**
109
+ * Translate the structured `format` object emitted by DESCRIBE TABLE EXTENDED
110
+ * AS JSON into a printf-like format string.
111
+ */
112
+ function translateStructuredFormat(spec) {
113
+ if (spec.currency && typeof spec.currency === "object") return currencyFormatString(spec.currency);
114
+ if (spec.percent && typeof spec.percent === "object") return percentFormatString(spec.percent);
115
+ if (spec.number && typeof spec.number === "object") return numberFormatString(spec.number);
116
+ }
117
+ function currencyFormatString(c) {
118
+ const places = readDecimalPlaces(c) ?? 2;
119
+ const codeRaw = c.currency_code;
120
+ return `${currencySymbol(typeof codeRaw === "string" && codeRaw.trim().length > 0 ? codeRaw.toUpperCase() : "USD")}#,##0${fractionalSuffix(places)}`;
121
+ }
122
+ function percentFormatString(p) {
123
+ return `0${fractionalSuffix(readDecimalPlaces(p) ?? 0)}%`;
124
+ }
125
+ function numberFormatString(n) {
126
+ return `#,##0${fractionalSuffix(readDecimalPlaces(n) ?? 0)}`;
127
+ }
128
+ function fractionalSuffix(places) {
129
+ return places > 0 ? `.${"0".repeat(places)}` : "";
130
+ }
131
+ /**
132
+ * Maximum decimal places honored from a format spec.
133
+ */
134
+ const MAX_DECIMAL_PLACES = 100;
135
+ function readDecimalPlaces(obj) {
136
+ const dp = obj.decimal_places;
137
+ if (typeof dp === "number" && Number.isFinite(dp) && dp >= 0) return Math.min(Math.floor(dp), MAX_DECIMAL_PLACES);
138
+ if (dp && typeof dp === "object" && !Array.isArray(dp)) {
139
+ const places = dp.places;
140
+ if (typeof places === "number" && Number.isFinite(places) && places >= 0) return Math.min(Math.floor(places), MAX_DECIMAL_PLACES);
141
+ }
142
+ }
143
+ /**
144
+ * Map ISO currency codes to their conventional prefix symbol. Unknown codes
145
+ * fall back to the literal code + space.
146
+ */
147
+ const CURRENCY_SYMBOLS = {
148
+ USD: "$",
149
+ EUR: "€",
150
+ GBP: "£",
151
+ JPY: "¥",
152
+ CNY: "¥",
153
+ INR: "₹",
154
+ BRL: "R$"
155
+ };
156
+ function currencySymbol(code) {
157
+ return CURRENCY_SYMBOLS[code] ?? `${code} `;
158
+ }
159
+ /**
160
+ * Infer the standard set of valid time grains for a dimension based on its
161
+ * SQL data type.
162
+ */
163
+ function inferTimeGrains(type) {
164
+ const normalized = type.toLowerCase().replace(/\(.*\)$/, "").trim();
165
+ if (normalized === "timestamp" || normalized === "timestamp_ltz" || normalized === "timestamp_ntz") return [
166
+ "day",
167
+ "hour",
168
+ "minute",
169
+ "month",
170
+ "quarter",
171
+ "week",
172
+ "year"
173
+ ];
174
+ if (normalized === "date") return [
175
+ "day",
176
+ "month",
177
+ "quarter",
178
+ "week",
179
+ "year"
180
+ ];
181
+ }
182
+ /**
183
+ * Quote a dot-separated FQN for safe interpolation into a Spark/Databricks SQL
184
+ * statement.
185
+ *
186
+ * Each dot-split segment is wrapped in backtick-quoted-identifier syntax. The
187
+ * one character that can break out of a backtick-quoted identifier is the
188
+ * backtick itself, escaped by doubling (`` ` `` → `` `` ``) — so every backtick
189
+ * inside a segment is doubled before the segment is wrapped. Control characters
190
+ * and newlines have no valid escape inside a quoted identifier, so a segment
191
+ * containing one is rejected outright.
192
+ *
193
+ * This is a pure, standalone escaper: it is intentionally independent of FQN
194
+ * naming validation ({@link isValidFqn}). Naming validation decides whether an
195
+ * FQN is an acceptable metric source; this function only guarantees that
196
+ * whatever it is handed cannot break out of the quoted identifier it produces.
197
+ *
198
+ * An ordinary identifier is unchanged apart from the wrapping backticks:
199
+ * `catalog.schema.view` → `` `catalog`.`schema`.`view` ``.
200
+ *
201
+ * @param fqn - Dot-separated identifier (e.g. `catalog.schema.view`).
202
+ * @returns The backtick-quoted, escaped identifier ready for interpolation.
203
+ * @throws If any segment contains a control character or newline.
204
+ */
205
+ function quoteFqnForSql(fqn) {
206
+ const CONTROL_OR_NEWLINE = /\p{Cc}/u;
207
+ return fqn.split(".").map((segment) => {
208
+ if (CONTROL_OR_NEWLINE.test(segment)) throw new Error(`Cannot quote FQN segment "${segment}" for SQL: it contains a control character or newline, which has no valid escape inside a backtick-quoted identifier.`);
209
+ return `\`${segment.replace(/`/g, "``")}\``;
210
+ }).join(".");
211
+ }
212
+ /**
213
+ * Build a DescribeFetcher from a real WorkspaceClient + warehouseId.
214
+ */
215
+ function createWorkspaceDescribeFetcher(client, warehouseId) {
216
+ const describeFormat = {};
217
+ return async (fqn) => {
218
+ /**
219
+ * Defense-in-depth: every caller passes a source that already cleared
220
+ * {@link resolveMetricConfig}, but this fetcher is an exported seam — re-check
221
+ * before interpolating into SQL.
222
+ */
223
+ if (!isValidFqn(fqn)) throw new Error(`Invalid metric source "${fqn}": expected a three-part UC FQN <catalog>.<schema>.<metric_view>.`);
224
+ return describeAdaptive(client, `DESCRIBE TABLE EXTENDED ${quoteFqnForSql(fqn)} AS JSON`, warehouseId, describeFormat);
225
+ };
226
+ }
227
+
228
+ //#endregion
229
+ export { createWorkspaceDescribeFetcher, extractMetricColumns, parseDescribeTableExtendedJson };
230
+ //# sourceMappingURL=describe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe.js","names":[],"sources":["../../../src/type-generator/mv-registry/describe.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport { type DescribeFormatMemo, describeAdaptive } from \"../statement-result\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport { isValidFqn } from \"./config\";\nimport type { DescribeFetcher, MetricColumnMetadata } from \"./types\";\n\n/**\n * Parse the JSON payload returned by DESCRIBE TABLE EXTENDED ... AS JSON.\n *\n * The Statement Execution API returns a single string cell — this normalizer\n * unwraps it. Handles both the production (real warehouse) shape and the\n * shape produced by mocked test responses.\n */\nexport function parseDescribeTableExtendedJson(\n response: DatabricksStatementExecutionResponse,\n): unknown {\n if (response.status?.state === \"FAILED\") {\n const msg = response.status.error?.message ?? \"DESCRIBE failed\";\n throw new Error(`DESCRIBE TABLE EXTENDED failed: ${msg}`);\n }\n\n const rows = response.result?.data_array ?? [];\n if (rows.length === 0) {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED returned no rows. Verify the FQN points to a metric view.\",\n );\n }\n\n const cell = rows[0]?.[0];\n if (typeof cell !== \"string\") {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED first cell was not a JSON string. Confirm the AS JSON suffix is supported.\",\n );\n }\n\n try {\n return JSON.parse(cell);\n } catch (err) {\n throw new Error(\n `Failed to parse DESCRIBE TABLE EXTENDED JSON: ${(err as Error).message}`,\n );\n }\n}\n\n/**\n * Pure function: turn the parsed DESCRIBE JSON into structured column metadata.\n *\n * Tolerant of multiple JSON shapes (the field may be `columns` or\n * `schema.fields`, type may be a string or `{ name }` object, the measure\n * marker may be `is_measure` or under `metadata.is_measure`).\n */\nexport function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] {\n if (!parsed || typeof parsed !== \"object\") {\n return [];\n }\n\n const root = parsed as Record<string, unknown>;\n const columnsCandidate = (root.columns ??\n (root.schema && typeof root.schema === \"object\"\n ? (root.schema as Record<string, unknown>).fields\n : undefined)) as unknown;\n\n if (!Array.isArray(columnsCandidate)) {\n return [];\n }\n\n const columns: MetricColumnMetadata[] = [];\n for (const raw of columnsCandidate) {\n if (!raw || typeof raw !== \"object\") continue;\n const obj = raw as Record<string, unknown>;\n const name =\n typeof obj.name === \"string\"\n ? obj.name\n : typeof obj.column_name === \"string\"\n ? obj.column_name\n : undefined;\n if (!name) continue;\n\n const typeRaw = obj.type ?? obj.data_type ?? obj.type_name;\n let type = \"STRING\";\n if (typeof typeRaw === \"string\") {\n type = typeRaw;\n } else if (typeRaw && typeof typeRaw === \"object\") {\n const inner = (typeRaw as Record<string, unknown>).name;\n if (typeof inner === \"string\") type = inner;\n }\n\n let isMeasure = false;\n if (typeof obj.is_measure === \"boolean\") {\n isMeasure = obj.is_measure;\n } else if (\n obj.metadata &&\n typeof obj.metadata === \"object\" &&\n typeof (obj.metadata as Record<string, unknown>).is_measure === \"boolean\"\n ) {\n isMeasure = (obj.metadata as Record<string, unknown>)\n .is_measure as boolean;\n } else if (obj.kind === \"measure\" || obj.role === \"measure\") {\n isMeasure = true;\n }\n\n const description =\n typeof obj.comment === \"string\"\n ? obj.comment\n : typeof obj.description === \"string\"\n ? obj.description\n : undefined;\n\n const displayName = extractStringFromAny(obj, [\n \"display_name\",\n \"displayName\",\n ]);\n const format = extractFormatString(obj);\n\n // Time-grain inference is type-driven, not YAML-attribute-driven.\n const timeGrains = isMeasure ? undefined : inferTimeGrains(type);\n\n columns.push({\n name,\n type,\n isMeasure,\n description,\n ...(displayName ? { displayName } : {}),\n ...(format ? { format } : {}),\n ...(timeGrains ? { timeGrains } : {}),\n });\n }\n\n return columns;\n}\n\n/**\n * Read a non-empty string attribute from a DESCRIBE column entry, tolerating\n * the multiple shapes UC has shipped for this metadata over time.\n */\nfunction extractStringFromAny(\n obj: Record<string, unknown>,\n candidates: readonly string[],\n): string | undefined {\n for (const key of candidates) {\n const direct = obj[key];\n if (typeof direct === \"string\" && direct.trim().length > 0) {\n return direct;\n }\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n if (typeof nested === \"string\" && nested.trim().length > 0) {\n return nested;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Read the column's `format` attribute from a DESCRIBE entry and return a\n * printf-like format string suitable for `formatValue` and `toD3Format`.\n */\nfunction extractFormatString(obj: Record<string, unknown>): string | undefined {\n for (const key of [\"format\", \"format_spec\"]) {\n const direct = obj[key];\n const fromDirect = formatStringFromValue(direct);\n if (fromDirect) return fromDirect;\n\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n const fromMeta = formatStringFromValue(nested);\n if (fromMeta) return fromMeta;\n }\n }\n return undefined;\n}\n\nfunction formatStringFromValue(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return translateStructuredFormat(value as Record<string, unknown>);\n }\n return undefined;\n}\n\n/**\n * Translate the structured `format` object emitted by DESCRIBE TABLE EXTENDED\n * AS JSON into a printf-like format string.\n */\nfunction translateStructuredFormat(\n spec: Record<string, unknown>,\n): string | undefined {\n if (spec.currency && typeof spec.currency === \"object\") {\n return currencyFormatString(spec.currency as Record<string, unknown>);\n }\n if (spec.percent && typeof spec.percent === \"object\") {\n return percentFormatString(spec.percent as Record<string, unknown>);\n }\n if (spec.number && typeof spec.number === \"object\") {\n return numberFormatString(spec.number as Record<string, unknown>);\n }\n return undefined;\n}\n\nfunction currencyFormatString(c: Record<string, unknown>): string {\n const places = readDecimalPlaces(c) ?? 2;\n const codeRaw = c.currency_code;\n const code =\n typeof codeRaw === \"string\" && codeRaw.trim().length > 0\n ? codeRaw.toUpperCase()\n : \"USD\";\n const symbol = currencySymbol(code);\n return `${symbol}#,##0${fractionalSuffix(places)}`;\n}\n\nfunction percentFormatString(p: Record<string, unknown>): string {\n const places = readDecimalPlaces(p) ?? 0;\n return `0${fractionalSuffix(places)}%`;\n}\n\nfunction numberFormatString(n: Record<string, unknown>): string {\n const places = readDecimalPlaces(n) ?? 0;\n return `#,##0${fractionalSuffix(places)}`;\n}\n\nfunction fractionalSuffix(places: number): string {\n return places > 0 ? `.${\"0\".repeat(places)}` : \"\";\n}\n\n/**\n * Maximum decimal places honored from a format spec.\n */\nconst MAX_DECIMAL_PLACES = 100;\n\nfunction readDecimalPlaces(obj: Record<string, unknown>): number | undefined {\n const dp = obj.decimal_places;\n if (typeof dp === \"number\" && Number.isFinite(dp) && dp >= 0) {\n return Math.min(Math.floor(dp), MAX_DECIMAL_PLACES);\n }\n if (dp && typeof dp === \"object\" && !Array.isArray(dp)) {\n const places = (dp as Record<string, unknown>).places;\n if (typeof places === \"number\" && Number.isFinite(places) && places >= 0) {\n return Math.min(Math.floor(places), MAX_DECIMAL_PLACES);\n }\n }\n return undefined;\n}\n\n/**\n * Map ISO currency codes to their conventional prefix symbol. Unknown codes\n * fall back to the literal code + space.\n */\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n USD: \"$\",\n EUR: \"€\",\n GBP: \"£\",\n JPY: \"¥\",\n CNY: \"¥\",\n INR: \"₹\",\n BRL: \"R$\",\n};\n\nfunction currencySymbol(code: string): string {\n return CURRENCY_SYMBOLS[code] ?? `${code} `;\n}\n\n/**\n * Infer the standard set of valid time grains for a dimension based on its\n * SQL data type.\n */\nfunction inferTimeGrains(type: string): string[] | undefined {\n const normalized = type\n .toLowerCase()\n .replace(/\\(.*\\)$/, \"\")\n .trim();\n if (\n normalized === \"timestamp\" ||\n normalized === \"timestamp_ltz\" ||\n normalized === \"timestamp_ntz\"\n ) {\n return [\"day\", \"hour\", \"minute\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n if (normalized === \"date\") {\n return [\"day\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n return undefined;\n}\n\n/**\n * Quote a dot-separated FQN for safe interpolation into a Spark/Databricks SQL\n * statement.\n *\n * Each dot-split segment is wrapped in backtick-quoted-identifier syntax. The\n * one character that can break out of a backtick-quoted identifier is the\n * backtick itself, escaped by doubling (`` ` `` → `` `` ``) — so every backtick\n * inside a segment is doubled before the segment is wrapped. Control characters\n * and newlines have no valid escape inside a quoted identifier, so a segment\n * containing one is rejected outright.\n *\n * This is a pure, standalone escaper: it is intentionally independent of FQN\n * naming validation ({@link isValidFqn}). Naming validation decides whether an\n * FQN is an acceptable metric source; this function only guarantees that\n * whatever it is handed cannot break out of the quoted identifier it produces.\n *\n * An ordinary identifier is unchanged apart from the wrapping backticks:\n * `catalog.schema.view` → `` `catalog`.`schema`.`view` ``.\n *\n * @param fqn - Dot-separated identifier (e.g. `catalog.schema.view`).\n * @returns The backtick-quoted, escaped identifier ready for interpolation.\n * @throws If any segment contains a control character or newline.\n */\nexport function quoteFqnForSql(fqn: string): string {\n // Reject anything that cannot be represented inside a backtick-quoted\n // identifier. \\p{Cc} is the Unicode \"control\" category, which covers C0\n // (incl. \\n, \\r, \\t), DEL, and C1 — i.e. every control character/newline.\n const CONTROL_OR_NEWLINE = /\\p{Cc}/u;\n return fqn\n .split(\".\")\n .map((segment) => {\n if (CONTROL_OR_NEWLINE.test(segment)) {\n throw new Error(\n `Cannot quote FQN segment \"${segment}\" for SQL: it contains a control character or newline, which has no valid escape inside a backtick-quoted identifier.`,\n );\n }\n // Double every backtick — the only break-out from a backtick-quoted\n // identifier — then wrap the whole segment in backticks.\n return `\\`${segment.replace(/`/g, \"``\")}\\``;\n })\n .join(\".\");\n}\n\n/**\n * Build a DescribeFetcher from a real WorkspaceClient + warehouseId.\n */\nexport function createWorkspaceDescribeFetcher(\n client: WorkspaceClient,\n warehouseId: string,\n): DescribeFetcher {\n // One format probe per fetcher (= per typegen run): the first DESCRIBE\n // discovers the warehouse's working format, every later one reuses it.\n const describeFormat: DescribeFormatMemo = {};\n return async (fqn: string) => {\n /**\n * Defense-in-depth: every caller passes a source that already cleared\n * {@link resolveMetricConfig}, but this fetcher is an exported seam — re-check\n * before interpolating into SQL.\n */\n if (!isValidFqn(fqn)) {\n throw new Error(\n `Invalid metric source \"${fqn}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view>.`,\n );\n }\n // Escape + quote every segment before interpolation. isValidFqn already\n // rejects backticks/control chars for metric sources, so this is\n // belt-and-suspenders for the SQL-injection seam — and keeps the quoting\n // independent of the naming rule.\n const quotedFqn = quoteFqnForSql(fqn);\n return describeAdaptive(\n client,\n `DESCRIBE TABLE EXTENDED ${quotedFqn} AS JSON`,\n warehouseId,\n describeFormat,\n );\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { compareKeys } from "./config.js";
2
+
3
+ //#region src/type-generator/mv-registry/metadata.ts
4
+ /**
5
+ * Converts a list of metric schemas into a JSON metadata bundle.
6
+ */
7
+ function buildMetricsMetadataBundle(schemas) {
8
+ const bundle = Object.create(null);
9
+ const sortedSchemas = [...schemas].sort((a, b) => compareKeys(a.key, b.key));
10
+ for (const schema of sortedSchemas) {
11
+ const measures = Object.create(null);
12
+ for (const m of schema.measures) measures[m.name] = buildColumnMetadata(m);
13
+ const dimensions = Object.create(null);
14
+ for (const d of schema.dimensions) dimensions[d.name] = buildColumnMetadata(d);
15
+ bundle[schema.key] = {
16
+ measures,
17
+ dimensions
18
+ };
19
+ }
20
+ return bundle;
21
+ }
22
+ function buildColumnMetadata(col) {
23
+ const entry = { type: col.type };
24
+ if (col.displayName) entry.display_name = col.displayName;
25
+ if (col.format) entry.format = col.format;
26
+ if (col.description) entry.description = col.description;
27
+ if (!col.isMeasure && col.timeGrains && col.timeGrains.length > 0) entry.time_grain = [...col.timeGrains];
28
+ return entry;
29
+ }
30
+ /**
31
+ * Serialize the metadata bundle to a stable, human-readable JSON string.
32
+ */
33
+ function generateMetricsMetadataJson(schemas) {
34
+ const bundle = buildMetricsMetadataBundle(schemas);
35
+ return `${JSON.stringify(bundle, null, 2)}\n`;
36
+ }
37
+
38
+ //#endregion
39
+ export { generateMetricsMetadataJson };
40
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","names":[],"sources":["../../../src/type-generator/mv-registry/metadata.ts"],"sourcesContent":["import { compareKeys } from \"./config\";\nimport type { MetricColumnMetadata, MetricSchema } from \"./types\";\n\ninterface MetricColumnSemanticMetadata {\n type: string;\n display_name?: string;\n format?: string;\n description?: string;\n /** Only emitted on dimension entries that resolved to a TIMESTAMP* or DATE SQL type. */\n time_grain?: readonly string[];\n}\n\ninterface MetricSemanticMetadataEntry {\n measures: Record<string, MetricColumnSemanticMetadata>;\n dimensions: Record<string, MetricColumnSemanticMetadata>;\n}\n\ntype MetricsMetadataBundle = Record<string, MetricSemanticMetadataEntry>;\n\n/**\n * Converts a list of metric schemas into a JSON metadata bundle.\n */\nexport function buildMetricsMetadataBundle(\n schemas: MetricSchema[],\n): MetricsMetadataBundle {\n // Null-prototype maps: metric keys and column names are controlled outside\n // this package, and \"__proto__\" is legal input.\n const bundle: MetricsMetadataBundle = Object.create(null);\n const sortedSchemas = [...schemas].sort((a, b) => compareKeys(a.key, b.key));\n\n for (const schema of sortedSchemas) {\n const measures: Record<string, MetricColumnSemanticMetadata> =\n Object.create(null);\n for (const m of schema.measures) {\n measures[m.name] = buildColumnMetadata(m);\n }\n\n const dimensions: Record<string, MetricColumnSemanticMetadata> =\n Object.create(null);\n for (const d of schema.dimensions) {\n dimensions[d.name] = buildColumnMetadata(d);\n }\n\n bundle[schema.key] = {\n measures,\n dimensions,\n };\n }\n\n return bundle;\n}\n\nfunction buildColumnMetadata(\n col: MetricColumnMetadata,\n): MetricColumnSemanticMetadata {\n const entry: MetricColumnSemanticMetadata = { type: col.type };\n if (col.displayName) entry.display_name = col.displayName;\n if (col.format) entry.format = col.format;\n if (col.description) entry.description = col.description;\n if (!col.isMeasure && col.timeGrains && col.timeGrains.length > 0) {\n entry.time_grain = [...col.timeGrains];\n }\n return entry;\n}\n\n/**\n * Serialize the metadata bundle to a stable, human-readable JSON string.\n */\nexport function generateMetricsMetadataJson(schemas: MetricSchema[]): string {\n const bundle = buildMetricsMetadataBundle(schemas);\n return `${JSON.stringify(bundle, null, 2)}\\n`;\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,2BACd,SACuB;CAGvB,MAAM,SAAgC,OAAO,OAAO,KAAK;CACzD,MAAM,gBAAgB,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,MAAM,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC;AAE5E,MAAK,MAAM,UAAU,eAAe;EAClC,MAAM,WACJ,OAAO,OAAO,KAAK;AACrB,OAAK,MAAM,KAAK,OAAO,SACrB,UAAS,EAAE,QAAQ,oBAAoB,EAAE;EAG3C,MAAM,aACJ,OAAO,OAAO,KAAK;AACrB,OAAK,MAAM,KAAK,OAAO,WACrB,YAAW,EAAE,QAAQ,oBAAoB,EAAE;AAG7C,SAAO,OAAO,OAAO;GACnB;GACA;GACD;;AAGH,QAAO;;AAGT,SAAS,oBACP,KAC8B;CAC9B,MAAM,QAAsC,EAAE,MAAM,IAAI,MAAM;AAC9D,KAAI,IAAI,YAAa,OAAM,eAAe,IAAI;AAC9C,KAAI,IAAI,OAAQ,OAAM,SAAS,IAAI;AACnC,KAAI,IAAI,YAAa,OAAM,cAAc,IAAI;AAC7C,KAAI,CAAC,IAAI,aAAa,IAAI,cAAc,IAAI,WAAW,SAAS,EAC9D,OAAM,aAAa,CAAC,GAAG,IAAI,WAAW;AAExC,QAAO;;;;;AAMT,SAAgB,4BAA4B,SAAiC;CAC3E,MAAM,SAAS,2BAA2B,QAAQ;AAClD,QAAO,GAAG,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC"}