@databricks/appkit 0.51.0 → 0.53.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.
Files changed (99) hide show
  1. package/CLAUDE.md +3 -0
  2. package/dist/agents/databricks.d.ts +2 -3
  3. package/dist/agents/databricks.d.ts.map +1 -1
  4. package/dist/agents/databricks.js +5 -4
  5. package/dist/agents/databricks.js.map +1 -1
  6. package/dist/agents/supervisor-api.d.ts.map +1 -1
  7. package/dist/agents/supervisor-api.js +3 -1
  8. package/dist/agents/supervisor-api.js.map +1 -1
  9. package/dist/appkit/package.js +1 -1
  10. package/dist/cache/index.d.ts.map +1 -1
  11. package/dist/cache/index.js +4 -2
  12. package/dist/cache/index.js.map +1 -1
  13. package/dist/connectors/files/client.js +2 -1
  14. package/dist/connectors/files/client.js.map +1 -1
  15. package/dist/connectors/genie/client.d.ts +2 -4
  16. package/dist/connectors/genie/client.js +2 -3
  17. package/dist/connectors/genie/client.js.map +1 -1
  18. package/dist/connectors/jobs/client.d.ts +2 -2
  19. package/dist/connectors/jobs/client.js +2 -1
  20. package/dist/connectors/jobs/client.js.map +1 -1
  21. package/dist/connectors/serving/client.d.ts +1 -1
  22. package/dist/connectors/serving/client.js +2 -1
  23. package/dist/connectors/serving/client.js.map +1 -1
  24. package/dist/connectors/sql-warehouse/client.js +2 -1
  25. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  26. package/dist/connectors/sql-warehouse/defaults.js.map +1 -1
  27. package/dist/connectors/sql-warehouse/warehouse-status-emitter.js.map +1 -1
  28. package/dist/connectors/vector-search/client.js.map +1 -1
  29. package/dist/context/client-options.js.map +1 -1
  30. package/dist/context/execution-context.d.ts +1 -1
  31. package/dist/context/service-context.d.ts +2 -1
  32. package/dist/context/service-context.d.ts.map +1 -1
  33. package/dist/context/service-context.js +8 -5
  34. package/dist/context/service-context.js.map +1 -1
  35. package/dist/core/appkit.d.ts +2 -1
  36. package/dist/core/appkit.d.ts.map +1 -1
  37. package/dist/core/appkit.js.map +1 -1
  38. package/dist/index.d.ts +6 -1
  39. package/dist/index.js +4 -1
  40. package/dist/internal-telemetry/reporter.js.map +1 -1
  41. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  42. package/dist/plugins/analytics/analytics.js.map +1 -1
  43. package/dist/plugins/analytics/query.js.map +1 -1
  44. package/dist/plugins/analytics/result-delivery.js.map +1 -1
  45. package/dist/plugins/files/plugin.js +4 -3
  46. package/dist/plugins/files/plugin.js.map +1 -1
  47. package/dist/plugins/files/types.d.ts +2 -1
  48. package/dist/plugins/files/types.d.ts.map +1 -1
  49. package/dist/plugins/jobs/plugin.js.map +1 -1
  50. package/dist/plugins/jobs/types.d.ts +2 -1
  51. package/dist/plugins/jobs/types.d.ts.map +1 -1
  52. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  53. package/dist/plugins/lakebase/lakebase.js +5 -4
  54. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  55. package/dist/shared/src/schemas/manifest.d.ts +2 -2
  56. package/dist/stream/arrow-stream-processor.js.map +1 -1
  57. package/dist/type-generator/errors.js +59 -1
  58. package/dist/type-generator/errors.js.map +1 -1
  59. package/dist/type-generator/index.js +105 -22
  60. package/dist/type-generator/index.js.map +1 -1
  61. package/dist/type-generator/mv-registry/describe.js.map +1 -1
  62. package/dist/type-generator/query-registry.js +60 -21
  63. package/dist/type-generator/query-registry.js.map +1 -1
  64. package/dist/type-generator/serving/fetcher.js +2 -1
  65. package/dist/type-generator/serving/fetcher.js.map +1 -1
  66. package/dist/type-generator/serving/generator.js +3 -2
  67. package/dist/type-generator/serving/generator.js.map +1 -1
  68. package/dist/type-generator/statement-result.js.map +1 -1
  69. package/dist/type-generator/types.js.map +1 -1
  70. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  71. package/dist/type-generator/vite-plugin.js +3 -2
  72. package/dist/type-generator/vite-plugin.js.map +1 -1
  73. package/dist/type-generator/warehouse-status.js.map +1 -1
  74. package/dist/workspace-client/client.js +57 -0
  75. package/dist/workspace-client/client.js.map +1 -0
  76. package/dist/workspace-client/errors.d.ts +2 -0
  77. package/dist/workspace-client/errors.js +3 -0
  78. package/dist/workspace-client/factory.d.ts +19 -0
  79. package/dist/workspace-client/factory.d.ts.map +1 -0
  80. package/dist/workspace-client/factory.js +24 -0
  81. package/dist/workspace-client/factory.js.map +1 -0
  82. package/dist/workspace-client/index.d.ts +4 -0
  83. package/dist/workspace-client/index.js +5 -0
  84. package/dist/workspace-client/legacy.d.ts +29 -0
  85. package/dist/workspace-client/legacy.d.ts.map +1 -0
  86. package/dist/workspace-client/legacy.js +23 -0
  87. package/dist/workspace-client/legacy.js.map +1 -0
  88. package/dist/workspace-client/types.d.ts +49 -0
  89. package/dist/workspace-client/types.d.ts.map +1 -0
  90. package/docs/api/appkit/Class.DatabricksAdapter.md +2 -3
  91. package/docs/api/appkit/Function.createApp.md +9 -9
  92. package/docs/api/appkit/Function.createWorkspaceClient.md +28 -0
  93. package/docs/api/appkit/Interface.WorkspaceClient.md +119 -0
  94. package/docs/api/appkit/Interface.WorkspaceClientOptions.md +47 -0
  95. package/docs/api/appkit.md +3 -0
  96. package/docs/development/type-generation.md +17 -2
  97. package/llms.txt +3 -0
  98. package/package.json +1 -1
  99. package/sbom.cdx.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import { createLogger } from "../logging/logger.js";
2
+ import { createWorkspaceClient } from "../workspace-client/factory.js";
3
+ import "../workspace-client/index.js";
2
4
  import { isRevivableMetricCacheEntry, loadCache, metricCacheHash, saveCache } from "./cache.js";
3
- import { getErrorDiagnostic, isConnectivityError } from "./errors.js";
5
+ import { classifyBlockingFailure, classifyEnvironmentalCause, getErrorDiagnostic, isConnectivityError } from "./errors.js";
4
6
  import { migrateProjectConfig, removeOldGeneratedTypes, resolveProjectRoot } from "./migration.js";
5
7
  import { readMetricConfig, resolveMetricConfig } from "./mv-registry/config.js";
6
8
  import { createWorkspaceDescribeFetcher } from "./mv-registry/describe.js";
@@ -10,10 +12,10 @@ import { decidePreflight } from "./preflight.js";
10
12
  import { getWarehouseState, startWarehouse, waitUntilRunning } from "./warehouse-status.js";
11
13
  import { generateQueriesFromDescribe } from "./query-registry.js";
12
14
  import { generateServingTypes as generateServingTypes$1 } from "./serving/generator.js";
13
- import { WorkspaceClient } from "@databricks/sdk-experimental";
14
15
  import pc from "picocolors";
15
16
  import fs from "node:fs/promises";
16
17
  import path from "node:path";
18
+ import { existsSync } from "node:fs";
17
19
  import dotenv from "dotenv";
18
20
 
19
21
  //#region src/type-generator/index.ts
@@ -25,9 +27,32 @@ const logger = createLogger("type-generator");
25
27
  * `PREFLIGHT_WAIT_MAX_MS` in query-registry.ts.
26
28
  */
27
29
  const MV_PREFLIGHT_WAIT_MAX_MS = 3e5;
30
+ /**
31
+ * Generate a loud warning message for environmental failures with committed types present.
32
+ * @param cause - coarse cause label: "auth" (401/403), "unreachable" (connectivity), or "unavailable" (other)
33
+ */
34
+ function determineWarningMessage(cause, warehouseId) {
35
+ return `AppKit typegen: using committed types — warehouse ${warehouseId} ${cause === "auth" ? "auth blocked" : cause === "unreachable" ? "warehouse unreachable" : "warehouse unavailable"}; please check warehouse status and retry`;
36
+ }
28
37
  function plural(count, singular, pluralForm = `${singular}s`) {
29
38
  return count === 1 ? singular : pluralForm;
30
39
  }
40
+ /**
41
+ * Check if committed type artifacts exist (at least one of the requested surfaces).
42
+ * Serving types are excluded (gitignored, never part of the gate).
43
+ * Returns true if either the analytics or metric-views committed .d.ts file exists.
44
+ */
45
+ function hasCommittedTypes(analyticsOutFile, metricViewsOutFile) {
46
+ const hasAnalytics = existsSync(analyticsOutFile);
47
+ const hasMetrics = metricViewsOutFile !== void 0 && existsSync(metricViewsOutFile);
48
+ return hasAnalytics || hasMetrics;
49
+ }
50
+ function isQueryDegraded(schema) {
51
+ return schema.degraded === true;
52
+ }
53
+ function hasAnyDegradedMetrics(schemas) {
54
+ return schemas.some((s) => s.degraded === true);
55
+ }
31
56
  function formatFailureRows(label, queries, color) {
32
57
  if (queries.length === 0) return [];
33
58
  const byMessage = /* @__PURE__ */ new Map();
@@ -202,6 +227,8 @@ async function generateFromEntryPoint(options) {
202
227
  let queryRegistry = [];
203
228
  let syntaxErrors = [];
204
229
  let fatalErrors = [];
230
+ let hadEnvironmentalFailure = false;
231
+ let environmentalCause;
205
232
  if (queryFolder) {
206
233
  const result = await generateQueriesFromDescribe(queryFolder, warehouseId, {
207
234
  noCache,
@@ -210,10 +237,19 @@ async function generateFromEntryPoint(options) {
210
237
  queryRegistry = result.schemas;
211
238
  syntaxErrors = result.syntaxErrors ?? [];
212
239
  fatalErrors = result.fatalErrors ?? [];
240
+ hadEnvironmentalFailure = hadEnvironmentalFailure || (result.hadEnvironmentalFailure ?? false);
241
+ environmentalCause = environmentalCause ?? result.environmentalCause ?? void 0;
213
242
  }
214
243
  const typeDeclarations = generateTypeDeclarations(queryRegistry);
215
- await fs.mkdir(path.dirname(outFile), { recursive: true });
216
- await fs.writeFile(outFile, typeDeclarations, "utf-8");
244
+ const hasAnyDegradedQuery = queryRegistry.some(isQueryDegraded);
245
+ if (mode === "blocking" && hasAnyDegradedQuery) {
246
+ hadEnvironmentalFailure = true;
247
+ environmentalCause = environmentalCause ?? "unavailable";
248
+ }
249
+ if (mode !== "blocking" || !hasAnyDegradedQuery) {
250
+ await fs.mkdir(path.dirname(outFile), { recursive: true });
251
+ await fs.writeFile(outFile, typeDeclarations, "utf-8");
252
+ }
217
253
  if (metricViewsFolder) {
218
254
  const mvFile = mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);
219
255
  let mvResult;
@@ -224,7 +260,8 @@ async function generateFromEntryPoint(options) {
224
260
  metricOutFile: mvFile,
225
261
  cache: !noCache,
226
262
  metricFetcher,
227
- mode
263
+ mode,
264
+ suppressDegradedWrite: mode === "blocking"
228
265
  });
229
266
  } catch (configError) {
230
267
  throw new TypegenFatalError([{
@@ -233,15 +270,27 @@ async function generateFromEntryPoint(options) {
233
270
  }], warehouseId);
234
271
  }
235
272
  for (const fe of mvResult.fatalErrors) fatalErrors.push(fe);
236
- if (mode === "blocking") for (const failure of mvResult.failures) fatalErrors.push({
237
- name: failure.key,
238
- message: `metric view ${failure.key} (${failure.source}) could not be described: ${failure.reason}`
239
- });
273
+ hadEnvironmentalFailure = hadEnvironmentalFailure || (mvResult.hadEnvironmentalFailure ?? false);
274
+ environmentalCause = environmentalCause ?? mvResult.environmentalCause ?? void 0;
275
+ if (mode === "blocking") for (const failure of mvResult.failures) {
276
+ if (failure.transient) continue;
277
+ fatalErrors.push({
278
+ name: failure.key,
279
+ message: `metric view ${failure.key} (${failure.source}) could not be described: ${failure.reason}`
280
+ });
281
+ }
240
282
  }
241
283
  await removeOldGeneratedTypes(projectRoot, "appKitTypes.d.ts");
242
284
  await migrateProjectConfig(projectRoot);
243
285
  if (syntaxErrors.length > 0) throw new TypegenSyntaxError(syntaxErrors, warehouseId, fatalErrors);
244
286
  if (fatalErrors.length > 0) throw new TypegenFatalError(fatalErrors, warehouseId);
287
+ if (mode === "blocking" && hadEnvironmentalFailure) if (hasCommittedTypes(outFile, options.mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE))) {
288
+ const warningMessage = determineWarningMessage(environmentalCause ?? "unavailable", warehouseId);
289
+ logger.warn(warningMessage);
290
+ } else throw new TypegenFatalError([{
291
+ name: "type-generator",
292
+ message: `Warehouse ${warehouseId} could not be reached and no committed types exist. Run 'npx @databricks/appkit generate-types --wait' locally and commit the generated .d.ts files.`
293
+ }], warehouseId);
245
294
  logger.debug("Type generation complete!");
246
295
  }
247
296
  /**
@@ -256,10 +305,14 @@ async function generateFromEntryPoint(options) {
256
305
  * @param options.metricOutFile - output path for the MetricRegistry `.d.ts`.
257
306
  * @param options.cache - cache toggle, default ON. Only `cache === false` disables it (so `undefined`/`true` keep caching).
258
307
  * @param options.metricFetcher - optional injected {@link DescribeFetcher}
259
- * @param options.mode - preflight/gate policy, default `"describe-now"`.
308
+ * @param options.mode - preflight/gate policy, default `"describe-now"`. When set to `"blocking"`,
309
+ * metric-view .d.ts writes are suppressed if any metric is degraded (to preserve committed files).
310
+ * @param options.suppressDegradedWrite - when true (only in `mode === "blocking"` context), skip
311
+ * the metricOutFile write if any metric schema has `degraded === true`. Used to prevent
312
+ * overwriting committed .d.ts files with degraded types in blocking mode.
260
313
  */
261
314
  async function syncMetricViewsTypes(options) {
262
- const { metricViewsFolder, warehouseId, metricOutFile, cache: cacheEnabled, metricFetcher, mode = "describe-now" } = options;
315
+ const { metricViewsFolder, warehouseId, metricOutFile, cache: cacheEnabled, metricFetcher, mode = "describe-now", suppressDegradedWrite } = options;
263
316
  const noCache = cacheEnabled === false;
264
317
  const mvConfig = await readMetricConfig(metricViewsFolder);
265
318
  if (!mvConfig) return {
@@ -282,27 +335,43 @@ async function syncMetricViewsTypes(options) {
282
335
  }
283
336
  let mvClient;
284
337
  const getMvClient = () => {
285
- mvClient ??= new WorkspaceClient({});
338
+ mvClient ??= createWorkspaceClient();
286
339
  return mvClient;
287
340
  };
288
341
  let preflightFatalMessage;
342
+ let hadEnvironmentalFailure = false;
343
+ let environmentalCause;
289
344
  if (mode === "blocking" && metricFetcher === void 0 && describeNeeded.length > 0) try {
290
345
  const state = await getWarehouseState(getMvClient(), warehouseId);
291
346
  const decision = decidePreflight(state, mode);
292
- if (decision === "fatal") preflightFatalMessage = `warehouse ${warehouseId} is ${state}`;
293
- else if (decision === "startWaitProceed") {
347
+ if (decision === "fatal") {
348
+ preflightFatalMessage = `warehouse ${warehouseId} is ${state}`;
349
+ hadEnvironmentalFailure = true;
350
+ environmentalCause = "unavailable";
351
+ } else if (decision === "startWaitProceed") {
294
352
  await startWarehouse(getMvClient(), warehouseId);
295
353
  const settled = await waitUntilRunning(getMvClient(), warehouseId, {
296
354
  maxMs: MV_PREFLIGHT_WAIT_MAX_MS,
297
355
  treatStoppedAsTransient: true
298
356
  });
299
- if (settled !== "RUNNING") preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;
357
+ if (settled !== "RUNNING") {
358
+ preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;
359
+ hadEnvironmentalFailure = true;
360
+ }
300
361
  } else if (decision === "waitThenProceed") {
301
362
  const settled = await waitUntilRunning(getMvClient(), warehouseId, { maxMs: MV_PREFLIGHT_WAIT_MAX_MS });
302
- if (settled === "DELETED" || settled === "DELETING") preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;
363
+ if (settled === "DELETED" || settled === "DELETING") {
364
+ preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;
365
+ hadEnvironmentalFailure = true;
366
+ }
303
367
  }
304
368
  } catch (err) {
305
- if (!isConnectivityError(err)) preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;
369
+ if (!isConnectivityError(err)) if (classifyBlockingFailure(err) === "deterministic") preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;
370
+ else {
371
+ preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;
372
+ hadEnvironmentalFailure = true;
373
+ environmentalCause = classifyEnvironmentalCause(err);
374
+ }
306
375
  }
307
376
  let gateState;
308
377
  let describeNow = metricFetcher !== void 0 || mode !== "non-blocking" || describeNeeded.length === 0;
@@ -318,7 +387,7 @@ async function syncMetricViewsTypes(options) {
318
387
  let failures = [];
319
388
  if (preflightFatalMessage !== void 0) {
320
389
  described = describeNeeded.map(emptyMetricSchema);
321
- for (const entry of describeNeeded) fatalErrors.push({
390
+ if (!hadEnvironmentalFailure) for (const entry of describeNeeded) fatalErrors.push({
322
391
  name: entry.key,
323
392
  message: preflightFatalMessage
324
393
  });
@@ -327,12 +396,22 @@ async function syncMetricViewsTypes(options) {
327
396
  const fetcher = metricFetcher ?? createWorkspaceDescribeFetcher(getMvClient(), warehouseId);
328
397
  ({schemas: described, failures} = await syncMetrics({ entries: describeNeeded }, fetcher));
329
398
  if (failures.length > 0) for (const f of failures) logger.warn("metric sync failed for %s (%s): %s", f.key, f.source, f.reason);
399
+ if (mode === "blocking" && failures.some((failure) => failure.transient)) {
400
+ hadEnvironmentalFailure = true;
401
+ environmentalCause = environmentalCause ?? "unreachable";
402
+ }
330
403
  const failedKeys = new Set(failures.map((f) => f.key));
331
404
  const degradedKeys = described.filter((s) => s.degraded && !failedKeys.has(s.key)).map((s) => s.key);
332
- if (degradedKeys.length > 0) logger.info("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.", warehouseId, degradedKeys.length, degradedKeys.join(", "));
405
+ if (degradedKeys.length > 0) {
406
+ logger.info("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.", warehouseId, degradedKeys.length, degradedKeys.join(", "));
407
+ hadEnvironmentalFailure = true;
408
+ environmentalCause = environmentalCause ?? "unavailable";
409
+ }
333
410
  } else {
334
411
  described = describeNeeded.map(emptyMetricSchema);
335
412
  logger.info("Warehouse %s is not running — wrote degraded metric types (permissive) for %d metric view(s) (%s); they will refresh once the warehouse is available.", warehouseId, describeNeeded.length, describeNeeded.map((e) => e.key).join(", "));
413
+ hadEnvironmentalFailure = true;
414
+ environmentalCause = environmentalCause ?? "unavailable";
336
415
  }
337
416
  for (let i = 0; i < describeNeeded.length; i++) {
338
417
  const entry = describeNeeded[i];
@@ -364,15 +443,19 @@ async function syncMetricViewsTypes(options) {
364
443
  logger.warn("no schema resolved for metric key %s — emitting degraded types (should not happen)", entry.key);
365
444
  return emptyMetricSchema(entry);
366
445
  });
367
- await fs.mkdir(path.dirname(metricOutFile), { recursive: true });
368
- await fs.writeFile(metricOutFile, generateMetricTypeDeclarations(schemas), "utf-8");
446
+ if (!suppressDegradedWrite || !hasAnyDegradedMetrics(schemas)) {
447
+ await fs.mkdir(path.dirname(metricOutFile), { recursive: true });
448
+ await fs.writeFile(metricOutFile, generateMetricTypeDeclarations(schemas), "utf-8");
449
+ }
369
450
  logger.debug("Wrote MetricRegistry augmentation for %d metric(s)%s", schemas.length, failures.length > 0 ? ` (${failures.length} failure(s))` : "");
370
451
  return {
371
452
  metricOutFile,
372
453
  schemas,
373
454
  failures,
374
455
  fatalErrors,
375
- noConfig: false
456
+ noConfig: false,
457
+ hadEnvironmentalFailure: mode === "blocking" ? hadEnvironmentalFailure : void 0,
458
+ environmentalCause: mode === "blocking" && hadEnvironmentalFailure ? environmentalCause : void 0
376
459
  };
377
460
  }
378
461
  const generateServingTypes = generateServingTypes$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 { 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 { 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 Views 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 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 *\n * Uses {@link getWarehouseState} (`warehouses.get`) —\n * a read-only GET that can never start the warehouse\n *\n * Takes the lazy client *getter* so the probe also absorbs client construction failure.\n * A connectivity blip returns `undefined`, which the gate reads as transient not-running;\n * a deterministic failure (auth, bad id) is re-thrown so the gate can classify it\n * 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.metricViewsFolder - folder that holds `definitions.json`\n * (`<root>/config/metric-views`). Optional and independent of `queryFolder`:\n * metric-view types generate whenever this folder holds a config, even if the\n * app has no `config/queries`. When omitted it defaults to a sibling\n * `metric-views` directory of `queryFolder` (so query-only callers keep\n * working); when neither is given, the metric path is skipped.\n * @param options.mvOutFile - optional output file for the MetricRegistry\n * augmentation. Defaults to a sibling `metric-views.d.ts` file under the same\n * directory as `outFile`. Skipped entirely if `definitions.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 metricViewsFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n mode?: PreflightMode;\n mvOutFile?: string;\n metricFetcher?: DescribeFetcher;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n mvOutFile,\n metricFetcher,\n } = options;\n\n // Metric config lives in `config/metric-views/`, a sibling of the queries\n // folder. Prefer the explicit option; otherwise derive the sibling of\n // `queryFolder` so callers that pass only `queryFolder` keep emitting metric\n // types. Undefined when neither is given → the metric path stays dormant.\n const metricViewsFolder =\n options.metricViewsFolder ??\n (queryFolder ? path.resolve(queryFolder, \"..\", \"metric-views\") : undefined);\n\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: emit whenever a metric-views folder is resolved (gated\n // on the metric config's own dir, NOT the queries folder — an app can declare\n // metric views without any `.sql` queries). `syncMetricViewsTypes` still\n // returns `noConfig` when the folder holds no `definitions.json`.\n if (metricViewsFolder) {\n const mvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n\n let mvResult: SyncMetricViewsTypesResult;\n try {\n mvResult = await syncMetricViewsTypes({\n metricViewsFolder,\n warehouseId,\n metricOutFile: mvFile,\n cache: !noCache,\n metricFetcher,\n mode,\n });\n } catch (configError) {\n // syncMetricViewsTypes only throws for a malformed definitions.json — re-throw as a message-only TypegenFatalError.\n throw new TypegenFatalError(\n [\n {\n name: \"config/metric-views/definitions.json\",\n message: getErrorDiagnostic(configError),\n },\n ],\n warehouseId,\n );\n }\n\n // Deleted/deleting-warehouse fatal preflight (blocking mode only);\n // empty (no-op) when definitions.json is absent or in non-blocking mode.\n for (const fe of mvResult.fatalErrors) {\n fatalErrors.push(fe);\n }\n\n // Blocking (`--wait` / prod Vite) escalates per-key DESCRIBE failures — a bad or unreachable source, i.e. a config error\n // to build failures so the end-of-run throw fails after the writes.\n if (mode === \"blocking\") {\n for (const failure of mvResult.failures) {\n fatalErrors.push({\n name: failure.key,\n message: `metric view ${failure.key} (${failure.source}) could not be described: ${failure.reason}`,\n });\n }\n }\n }\n\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Types are always written above — including `result: unknown` for any Metric View that could not be described.\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/**\n * Result of a {@link syncMetricViewsTypes} run, returned to the caller (the CLI\n * directly, or {@link generateFromEntryPoint} which delegates to it) so it can\n * report what happened and decide its exit code.\n */\nexport interface SyncMetricViewsTypesResult {\n metricOutFile?: string;\n schemas: MetricSchema[];\n failures: MetricSyncFailure[];\n /**\n * `true` when no `definitions.json` was found in the metric-views folder, so\n * nothing was synced.\n */\n noConfig: boolean;\n /**\n * Per-key fatal preflight errors (empty except in the `blocking`-mode\n * deleted/deleting-warehouse and deterministic-preflight-failure cases). The\n * artifacts are still written; {@link generateFromEntryPoint} surfaces these\n * by throwing {@link TypegenFatalError} after the writes. A `\"describe-now\"`\n * run sets no blocking preflight, so for that mode this is always empty.\n */\n fatalErrors: Array<{ name: string; message: string }>;\n}\n\n/**\n * Unified metric-view type-generation pipeline behind {@link\n * generateFromEntryPoint}'s metric section (which forwards its\n * `\"non-blocking\"`/`\"blocking\"` mode). Also directly callable with the default\n * `\"describe-now\"` mode for a focused, always-converge metric refresh.\n *\n *\n * @param options.metricViewsFolder - folder that holds `definitions.json` (`<root>/config/metric-views`).\n * @param options.warehouseId - SQL warehouse used for `DESCRIBE TABLE EXTENDED`.\n * @param options.metricOutFile - output path for the MetricRegistry `.d.ts`.\n * @param options.cache - cache toggle, default ON. Only `cache === false` disables it (so `undefined`/`true` keep caching).\n * @param options.metricFetcher - optional injected {@link DescribeFetcher}\n * @param options.mode - preflight/gate policy, default `\"describe-now\"`.\n */\nexport async function syncMetricViewsTypes(options: {\n metricViewsFolder: string;\n warehouseId: string;\n metricOutFile: string;\n cache?: boolean;\n metricFetcher?: DescribeFetcher;\n mode?: \"describe-now\" | \"non-blocking\" | \"blocking\";\n}): Promise<SyncMetricViewsTypesResult> {\n const {\n metricViewsFolder,\n warehouseId,\n metricOutFile,\n cache: cacheEnabled,\n metricFetcher,\n mode = \"describe-now\",\n } = options;\n\n // Only `cache === false` disables caching; `undefined`/`true` keep it on.\n const noCache = cacheEnabled === false;\n\n const mvConfig = await readMetricConfig(metricViewsFolder);\n if (!mvConfig) {\n // No definitions.json — additive path stays dormant. The CLI turns this\n // into a friendly \"nothing to sync\" message and exits 0;\n // generateFromEntryPoint simply ignores `noConfig`.\n return { schemas: [], failures: [], fatalErrors: [], noConfig: true };\n }\n\n const resolution = resolveMetricConfig(mvConfig);\n\n const fatalErrors: Array<{ name: string; message: string }> = [];\n\n // Load the shared typegen cache and copy its `metrics` section into a null-prototype map.\n const cache = await loadCache();\n const mvCacheSection: Record<string, MetricCacheEntry> = 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 (a structurally valid,\n // hash-matching, NON-degraded cached entry) is served from cache no matter\n // what the warehouse is doing. The cache only ever holds successful describes\n // (a degraded outcome is never persisted — see the write block below), so the\n // `degraded !== true` guard is normally moot; it also defends against a stale\n // degraded entry left by an older writer, which re-describes instead of\n // serving. Everything else (new, edited, unrevivable, or degraded) is eligible\n // for DESCRIBE, so a fully-warm pass makes zero warehouse calls and constructs\n // zero clients. Mirrors the query path: only a good result is cache-servable.\n const hitSchemas = new Map<string, MetricSchema>();\n const describeNeeded: typeof resolution.entries = [];\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.schema.degraded !== true\n ) {\n hitSchemas.set(entry.key, prior.schema);\n } else {\n describeNeeded.push(entry);\n }\n }\n\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 MV DESCRIBE\n // batch (probe → decide → wait / start+wait; only DELETED/DELETING is fatal). Two softenings vs the query preflight: a failed probe and a timed-out wait are NOT fatal here — we fall through to syncMetrics, which classifies a still-not-ready warehouse as degraded rather than failing the build. Skipped for `describe-now`/`non-blocking` (only `mode === \"blocking\"` enters here).\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 STOPPED/STOPPING\n // 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 exactly\n // DELETED/DELETING — the warehouse was deleted while we waited.\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.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n }\n }\n } catch (err) {\n // Connectivity blip: fall through to syncMetrics, whose DESCRIBEs degrade\n // a not-ready / unreachable warehouse rather than throwing.\n if (!isConnectivityError(err)) {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n }\n }\n\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 preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n describeNow = gateState === \"RUNNING\";\n }\n\n let described: MetricSchema[];\n let failures: MetricSyncFailure[] = [];\n if (preflightFatalMessage !== undefined) {\n // Fatal preflight (deleted/deleting warehouse): fail like the query path —\n // skip DESCRIBE, emit degraded schemas so both artifacts are still written,\n // and record one fatal error per describe-needed key (cache hits are\n // unaffected). The caller surfaces them after the writes. The degraded\n // schemas are not cached (see the write block), so a later pass re-probes.\n described = describeNeeded.map(emptyMetricSchema);\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 construct a\n // 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 definitions.json would\n // otherwise silently ship an empty entry that the runtime fail-closed gate\n // 503s in production. syncMetrics is log-free; this caller is the single\n // 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 non-terminal\n // state (stopped / cold-starting), so their schemas are unknown.\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 so both artifacts exist; cache\n // hits keep serving last-known-good.\n described = describeNeeded.map(emptyMetricSchema);\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 // Cache only successful schema results for describe-needed keys; remove stale cache for degraded ones.\n for (let i = 0; i < describeNeeded.length; i++) {\n // syncMetrics return one schema per entry in entry order, so described[i] always belongs to describeNeeded[i].\n const entry = describeNeeded[i];\n if (described[i].degraded === true) {\n delete mvCacheSection[entry.key];\n continue;\n }\n mvCacheSection[entry.key] = {\n hash: metricCacheHash(entry.source, entry.lane),\n schema: described[i],\n // Vestigial, mirrors the query path's only cache write (always false): a\n // persisted entry is by construction a good result, so it never needs a\n // re-describe flag. Kept for on-disk shape compatibility with existing\n // version-3 caches (isRevivableMetricCacheEntry gates on a boolean).\n retry: false,\n };\n }\n\n // Prune entries whose key is no longer configured\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 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 const describedByKey = new Map<string, MetricSchema>();\n for (const schema of described) {\n describedByKey.set(schema.key, schema);\n }\n const schemas = resolution.entries.map((entry) => {\n const schema = hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);\n if (schema !== undefined) return schema;\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 await fs.mkdir(path.dirname(metricOutFile), { recursive: true });\n await fs.writeFile(\n metricOutFile,\n generateMetricTypeDeclarations(schemas),\n \"utf-8\",\n );\n\n logger.debug(\n \"Wrote MetricRegistry augmentation for %d metric(s)%s\",\n schemas.length,\n failures.length > 0 ? ` (${failures.length} failure(s))` : \"\",\n );\n\n return {\n metricOutFile,\n schemas,\n failures,\n fatalErrors,\n noConfig: false,\n };\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\nexport const TYPES_DIR = \"appkit-types\";\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\nexport const METRIC_TYPES_FILE = \"metric-views.d.ts\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport 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 createWorkspaceClient,\n type WorkspaceClient,\n} from \"../workspace-client\";\nimport {\n isRevivableMetricCacheEntry,\n loadCache,\n type MetricCacheEntry,\n metricCacheHash,\n saveCache,\n} from \"./cache\";\nimport {\n classifyBlockingFailure,\n classifyEnvironmentalCause,\n getErrorDiagnostic,\n isConnectivityError,\n} 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 { 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 Views 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\n/**\n * Generate a loud warning message for environmental failures with committed types present.\n * @param cause - coarse cause label: \"auth\" (401/403), \"unreachable\" (connectivity), or \"unavailable\" (other)\n */\nfunction determineWarningMessage(\n cause: \"auth\" | \"unreachable\" | \"unavailable\",\n warehouseId: string,\n): string {\n const causeLabel =\n cause === \"auth\"\n ? \"auth blocked\"\n : cause === \"unreachable\"\n ? \"warehouse unreachable\"\n : \"warehouse unavailable\";\n // Use a stable prefix for greppability and CI log matching.\n return `AppKit typegen: using committed types — warehouse ${warehouseId} ${causeLabel}; please check warehouse status and retry`;\n}\n\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\n}\n\n/**\n * Check if committed type artifacts exist (at least one of the requested surfaces).\n * Serving types are excluded (gitignored, never part of the gate).\n * Returns true if either the analytics or metric-views committed .d.ts file exists.\n */\nfunction hasCommittedTypes(\n analyticsOutFile: string,\n metricViewsOutFile: string | undefined,\n): boolean {\n const hasAnalytics = existsSync(analyticsOutFile);\n const hasMetrics =\n metricViewsOutFile !== undefined && existsSync(metricViewsOutFile);\n return hasAnalytics || hasMetrics;\n}\n\nfunction isQueryDegraded(schema: QuerySchema): boolean {\n return schema.degraded === true;\n}\n\nfunction hasAnyDegradedMetrics(schemas: MetricSchema[]): boolean {\n return schemas.some((s) => s.degraded === true);\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 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 *\n * Uses {@link getWarehouseState} (`warehouses.get`) —\n * a read-only GET that can never start the warehouse\n *\n * Takes the lazy client *getter* so the probe also absorbs client construction failure.\n * A connectivity blip returns `undefined`, which the gate reads as transient not-running;\n * a deterministic failure (auth, bad id) is re-thrown so the gate can classify it\n * 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.metricViewsFolder - folder that holds `definitions.json`\n * (`<root>/config/metric-views`). Optional and independent of `queryFolder`:\n * metric-view types generate whenever this folder holds a config, even if the\n * app has no `config/queries`. When omitted it defaults to a sibling\n * `metric-views` directory of `queryFolder` (so query-only callers keep\n * working); when neither is given, the metric path is skipped.\n * @param options.mvOutFile - optional output file for the MetricRegistry\n * augmentation. Defaults to a sibling `metric-views.d.ts` file under the same\n * directory as `outFile`. Skipped entirely if `definitions.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 metricViewsFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n mode?: PreflightMode;\n mvOutFile?: string;\n metricFetcher?: DescribeFetcher;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n mvOutFile,\n metricFetcher,\n } = options;\n\n // Metric config lives in `config/metric-views/`, a sibling of the queries\n // folder. Prefer the explicit option; otherwise derive the sibling of\n // `queryFolder` so callers that pass only `queryFolder` keep emitting metric\n // types. Undefined when neither is given → the metric path stays dormant.\n const metricViewsFolder =\n options.metricViewsFolder ??\n (queryFolder ? path.resolve(queryFolder, \"..\", \"metric-views\") : undefined);\n\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n // Deterministic fatal errors only (404/400).\n let fatalErrors: QueryFatalError[] = [];\n // Track whether an environmental failure occurred in blocking mode.\n let hadEnvironmentalFailure = false;\n // Track the coarse cause of the environmental failure for the warning message.\n let environmentalCause: \"auth\" | \"unreachable\" | \"unavailable\" | undefined;\n\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 hadEnvironmentalFailure =\n hadEnvironmentalFailure || (result.hadEnvironmentalFailure ?? false);\n environmentalCause =\n environmentalCause ?? result.environmentalCause ?? undefined;\n }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n // In blocking mode, never overwrite committed types with a schema explicitly\n // marked degraded. Leave the committed .d.ts as the fallback of record.\n // Non-blocking mode always writes.\n const hasAnyDegradedQuery = queryRegistry.some(isQueryDegraded);\n if (mode === \"blocking\" && hasAnyDegradedQuery) {\n // A degraded schema always participates in the committed-types gate. Keep\n // this invariant next to write suppression so a new producer cannot update\n // one decision without the other.\n hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unavailable\";\n }\n const shouldWriteQueries = mode !== \"blocking\" || !hasAnyDegradedQuery;\n\n if (shouldWriteQueries) {\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n }\n\n // Metric-view types: emit whenever a metric-views folder is resolved (gated\n // on the metric config's own dir, NOT the queries folder — an app can declare\n // metric views without any `.sql` queries). `syncMetricViewsTypes` still\n // returns `noConfig` when the folder holds no `definitions.json`.\n if (metricViewsFolder) {\n const mvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n\n let mvResult: SyncMetricViewsTypesResult;\n try {\n mvResult = await syncMetricViewsTypes({\n metricViewsFolder,\n warehouseId,\n metricOutFile: mvFile,\n cache: !noCache,\n metricFetcher,\n mode,\n // In blocking mode, never overwrite committed metric types with a\n // degraded result — including on runs that go on to throw, so a failing\n // build leaves the committed .d.ts intact. Non-blocking always writes.\n suppressDegradedWrite: mode === \"blocking\",\n });\n } catch (configError) {\n // syncMetricViewsTypes only throws for a malformed definitions.json — re-throw as a message-only TypegenFatalError.\n throw new TypegenFatalError(\n [\n {\n name: \"config/metric-views/definitions.json\",\n message: getErrorDiagnostic(configError),\n },\n ],\n warehouseId,\n );\n }\n\n // Deleted/deleting-warehouse fatal preflight (blocking mode only);\n // empty (no-op) when definitions.json is absent or in non-blocking mode.\n // Only deterministic fatals are recorded in fatalErrors.\n for (const fe of mvResult.fatalErrors) {\n fatalErrors.push(fe);\n }\n\n // Thread through the environmental failure flag and cause.\n hadEnvironmentalFailure =\n hadEnvironmentalFailure || (mvResult.hadEnvironmentalFailure ?? false);\n environmentalCause =\n environmentalCause ?? mvResult.environmentalCause ?? undefined;\n\n // Blocking (`--wait` / prod Vite) escalates only deterministic per-key\n // DESCRIBE failures. Transient connectivity failures are already recorded\n // as environmental by syncMetricViewsTypes and fall through to the\n // committed-types gate below.\n if (mode === \"blocking\") {\n for (const failure of mvResult.failures) {\n if (failure.transient) continue;\n fatalErrors.push({\n name: failure.key,\n message: `metric view ${failure.key} (${failure.source}) could not be described: ${failure.reason}`,\n });\n }\n }\n }\n\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Deterministic failures (SQL syntax errors or 404/400 HTTP) always crash regardless of mode.\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 // Environmental failures (in blocking mode) trigger the has-types gate.\n if (mode === \"blocking\" && hadEnvironmentalFailure) {\n // Determine resolved metric-views file for the has-types check.\n const resolvedMvFile =\n options.mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n\n const hasTypes = hasCommittedTypes(outFile, resolvedMvFile);\n\n if (hasTypes) {\n // Committed types present: emit loud warning and exit 0.\n const warningMessage = determineWarningMessage(\n environmentalCause ?? \"unavailable\",\n warehouseId,\n );\n logger.warn(warningMessage);\n } else {\n // No committed types: crash with a generic message.\n throw new TypegenFatalError(\n [\n {\n name: \"type-generator\",\n message: `Warehouse ${warehouseId} could not be reached and no committed types exist. Run 'npx @databricks/appkit generate-types --wait' locally and commit the generated .d.ts files.`,\n },\n ],\n warehouseId,\n );\n }\n }\n\n logger.debug(\"Type generation complete!\");\n}\n\n/**\n * Result of a {@link syncMetricViewsTypes} run, returned to the caller (the CLI\n * directly, or {@link generateFromEntryPoint} which delegates to it) so it can\n * report what happened and decide its exit code.\n */\nexport interface SyncMetricViewsTypesResult {\n metricOutFile?: string;\n schemas: MetricSchema[];\n failures: MetricSyncFailure[];\n /**\n * `true` when no `definitions.json` was found in the metric-views folder, so\n * nothing was synced.\n */\n noConfig: boolean;\n /**\n * Per-key fatal preflight errors (empty except in the `blocking`-mode\n * deleted/deleting-warehouse and deterministic-preflight-failure cases).\n * {@link generateFromEntryPoint} surfaces these by throwing\n * {@link TypegenFatalError}; when the run also degraded, `suppressDegradedWrite`\n * means no artifact was written and the committed types stand. A `\"describe-now\"`\n * run sets no blocking preflight, so for that mode this is always empty.\n * ONLY contains deterministic failures (404/400).\n */\n fatalErrors: Array<{ name: string; message: string }>;\n /**\n * `true` when an environmental failure occurred in blocking mode (auth, connectivity,\n * DELETED/DELETING, wait-timeout, or other unrecognized failures). Used by\n * {@link generateFromEntryPoint} to decide whether to apply the has-types gate.\n * Does not directly cause a throw — the gate decides that. Always false in\n * non-blocking or describe-now mode.\n */\n hadEnvironmentalFailure?: boolean;\n /**\n * Coarse cause label for the environmental failure, one of \"auth\" (401/403),\n * \"unreachable\" (connectivity), or \"unavailable\" (other). Only set when\n * hadEnvironmentalFailure is true; used by the warning message.\n */\n environmentalCause?: \"auth\" | \"unreachable\" | \"unavailable\";\n}\n\n/**\n * Unified metric-view type-generation pipeline behind {@link\n * generateFromEntryPoint}'s metric section (which forwards its\n * `\"non-blocking\"`/`\"blocking\"` mode). Also directly callable with the default\n * `\"describe-now\"` mode for a focused, always-converge metric refresh.\n *\n *\n * @param options.metricViewsFolder - folder that holds `definitions.json` (`<root>/config/metric-views`).\n * @param options.warehouseId - SQL warehouse used for `DESCRIBE TABLE EXTENDED`.\n * @param options.metricOutFile - output path for the MetricRegistry `.d.ts`.\n * @param options.cache - cache toggle, default ON. Only `cache === false` disables it (so `undefined`/`true` keep caching).\n * @param options.metricFetcher - optional injected {@link DescribeFetcher}\n * @param options.mode - preflight/gate policy, default `\"describe-now\"`. When set to `\"blocking\"`,\n * metric-view .d.ts writes are suppressed if any metric is degraded (to preserve committed files).\n * @param options.suppressDegradedWrite - when true (only in `mode === \"blocking\"` context), skip\n * the metricOutFile write if any metric schema has `degraded === true`. Used to prevent\n * overwriting committed .d.ts files with degraded types in blocking mode.\n */\nexport async function syncMetricViewsTypes(options: {\n metricViewsFolder: string;\n warehouseId: string;\n metricOutFile: string;\n cache?: boolean;\n metricFetcher?: DescribeFetcher;\n mode?: \"describe-now\" | \"non-blocking\" | \"blocking\";\n suppressDegradedWrite?: boolean;\n}): Promise<SyncMetricViewsTypesResult> {\n const {\n metricViewsFolder,\n warehouseId,\n metricOutFile,\n cache: cacheEnabled,\n metricFetcher,\n mode = \"describe-now\",\n suppressDegradedWrite,\n } = options;\n\n // Only `cache === false` disables caching; `undefined`/`true` keep it on.\n const noCache = cacheEnabled === false;\n\n const mvConfig = await readMetricConfig(metricViewsFolder);\n if (!mvConfig) {\n // No definitions.json — additive path stays dormant. The CLI turns this\n // into a friendly \"nothing to sync\" message and exits 0;\n // generateFromEntryPoint simply ignores `noConfig`.\n return { schemas: [], failures: [], fatalErrors: [], noConfig: true };\n }\n\n const resolution = resolveMetricConfig(mvConfig);\n\n const fatalErrors: Array<{ name: string; message: string }> = [];\n\n // Load the shared typegen cache and copy its `metrics` section into a null-prototype map.\n const cache = await loadCache();\n const mvCacheSection: Record<string, MetricCacheEntry> = 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 (a structurally valid,\n // hash-matching, NON-degraded cached entry) is served from cache no matter\n // what the warehouse is doing. The cache only ever holds successful describes\n // (a degraded outcome is never persisted — see the write block below), so the\n // `degraded !== true` guard is normally moot; it also defends against a stale\n // degraded entry left by an older writer, which re-describes instead of\n // serving. Everything else (new, edited, unrevivable, or degraded) is eligible\n // for DESCRIBE, so a fully-warm pass makes zero warehouse calls and constructs\n // zero clients. Mirrors the query path: only a good result is cache-servable.\n const hitSchemas = new Map<string, MetricSchema>();\n const describeNeeded: typeof resolution.entries = [];\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.schema.degraded !== true\n ) {\n hitSchemas.set(entry.key, prior.schema);\n } else {\n describeNeeded.push(entry);\n }\n }\n\n let mvClient: WorkspaceClient | undefined;\n const getMvClient = (): WorkspaceClient => {\n mvClient ??= createWorkspaceClient();\n return mvClient;\n };\n\n // Blocking-mode preflight: ensure the warehouse is running before the MV DESCRIBE\n // batch (probe → decide → wait / start+wait; only DELETED/DELETING is fatal). Two softenings vs the query preflight: a failed probe and a timed-out wait are NOT fatal here — we fall through to syncMetrics, which classifies a still-not-ready warehouse as degraded rather than failing the build. Skipped for `describe-now`/`non-blocking` (only `mode === \"blocking\"` enters here).\n let preflightFatalMessage: string | undefined;\n let hadEnvironmentalFailure = false;\n let environmentalCause: \"auth\" | \"unreachable\" | \"unavailable\" | 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 // State-based DELETED/DELETING is environmental, not deterministic.\n hadEnvironmentalFailure = true;\n environmentalCause = \"unavailable\";\n } else if (decision === \"startWaitProceed\") {\n // treatStoppedAsTransient rides out the stale pre-start STOPPED/STOPPING\n // 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 exactly\n // DELETED/DELETING — the warehouse was deleted while we waited.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n hadEnvironmentalFailure = true;\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. Environmental (state-based).\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n hadEnvironmentalFailure = true;\n }\n }\n } catch (err) {\n // Connectivity blip: fall through to syncMetrics, whose DESCRIBEs degrade\n // a not-ready / unreachable warehouse rather than throwing.\n if (!isConnectivityError(err)) {\n // Classify: deterministic (404/400) or environmental (auth, etc).\n const classification = classifyBlockingFailure(err);\n if (classification === \"deterministic\") {\n // Keep as fatal preflight for deterministic errors (404/400).\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n } else {\n // Environmental: set preflightFatalMessage so DESCRIBE is skipped, but\n // mark hadEnvironmentalFailure so the gate handles it later (not added\n // to fatalErrors).\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n hadEnvironmentalFailure = true;\n environmentalCause = classifyEnvironmentalCause(err);\n }\n }\n }\n }\n\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 preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n describeNow = gateState === \"RUNNING\";\n }\n\n let described: MetricSchema[];\n let failures: MetricSyncFailure[] = [];\n if (preflightFatalMessage !== undefined) {\n // Fatal preflight (deleted/deleting warehouse or deterministic error):\n // skip DESCRIBE, emit degraded schemas so both artifacts are still written,\n // and record one fatal error per describe-needed key (cache hits are\n // unaffected) ONLY if it's a deterministic error. Environmental failures\n // degrade silently. The degraded schemas are not cached (see the write\n // block), so a later pass re-probes.\n described = describeNeeded.map(emptyMetricSchema);\n // Only deterministic fatals (404/400) record errors; environmental failures\n // degrade silently for the has-types gate to handle.\n if (!hadEnvironmentalFailure) {\n for (const entry of describeNeeded) {\n fatalErrors.push({ name: entry.key, message: preflightFatalMessage });\n }\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 construct a\n // 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 definitions.json would\n // otherwise silently ship an empty entry that the runtime fail-closed gate\n // 503s in production. syncMetrics is log-free; this caller is the single\n // 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 // A rejected DESCRIBE with a connectivity signal is expected to recover on\n // a later pass. In blocking mode, route it through the same committed-types\n // gate as preflight outages instead of treating it as a configuration error.\n if (mode === \"blocking\" && failures.some((failure) => failure.transient)) {\n hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unreachable\";\n }\n\n // Degraded-but-not-failed keys: the warehouse answered with a non-terminal\n // state (stopped / cold-starting), so their schemas are unknown.\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 hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unavailable\";\n }\n } else {\n // Un-probed DESCRIBEs deliberately skipped, not failures: emit each\n // describe-needed key as a degraded schema so both artifacts exist; cache\n // hits keep serving last-known-good. This is an environmental failure path.\n described = describeNeeded.map(emptyMetricSchema);\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 hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unavailable\";\n }\n\n // Cache only successful schema results for describe-needed keys; remove stale cache for degraded ones.\n for (let i = 0; i < describeNeeded.length; i++) {\n // syncMetrics return one schema per entry in entry order, so described[i] always belongs to describeNeeded[i].\n const entry = describeNeeded[i];\n if (described[i].degraded === true) {\n delete mvCacheSection[entry.key];\n continue;\n }\n mvCacheSection[entry.key] = {\n hash: metricCacheHash(entry.source, entry.lane),\n schema: described[i],\n // Vestigial, mirrors the query path's only cache write (always false): a\n // persisted entry is by construction a good result, so it never needs a\n // re-describe flag. Kept for on-disk shape compatibility with existing\n // version-3 caches (isRevivableMetricCacheEntry gates on a boolean).\n retry: false,\n };\n }\n\n // Prune entries whose key is no longer configured\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 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 const describedByKey = new Map<string, MetricSchema>();\n for (const schema of described) {\n describedByKey.set(schema.key, schema);\n }\n const schemas = resolution.entries.map((entry) => {\n const schema = hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);\n if (schema !== undefined) return schema;\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 // Same anti-clobber rule as the query path: when suppressDegradedWrite is set\n // (blocking mode), skip the write if any metric degraded, preserving the\n // committed metric-views.d.ts. Non-blocking mode always writes.\n const shouldWriteMetrics =\n !suppressDegradedWrite || !hasAnyDegradedMetrics(schemas);\n\n if (shouldWriteMetrics) {\n await fs.mkdir(path.dirname(metricOutFile), { recursive: true });\n await fs.writeFile(\n metricOutFile,\n generateMetricTypeDeclarations(schemas),\n \"utf-8\",\n );\n }\n\n logger.debug(\n \"Wrote MetricRegistry augmentation for %d metric(s)%s\",\n schemas.length,\n failures.length > 0 ? ` (${failures.length} failure(s))` : \"\",\n );\n\n return {\n metricOutFile,\n schemas,\n failures,\n fatalErrors,\n noConfig: false,\n hadEnvironmentalFailure:\n mode === \"blocking\" ? hadEnvironmentalFailure : undefined,\n environmentalCause:\n mode === \"blocking\" && hadEnvironmentalFailure\n ? environmentalCause\n : undefined,\n };\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\nexport const TYPES_DIR = \"appkit-types\";\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\nexport const METRIC_TYPES_FILE = \"metric-views.d.ts\";\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"describe.js","names":[],"sources":["../../../src/type-generator/mv-registry/describe.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\n// Grammar + SQL-quoting for metric-view FQNs live together in the shared,\n// zod-free leaf so the type-generator and the analytics runtime validate and\n// escape against one source of truth (see the module doc in metric-fqn.ts).\nimport {\n isValidFqn,\n quoteFqnForSql,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport { type DescribeFormatMemo, describeAdaptive } from \"../statement-result\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport type { DescribeFetcher, MetricColumnMetadata } from \"./types\";\n\n/**\n * Parse the JSON payload returned by DESCRIBE TABLE EXTENDED ... AS JSON.\n *\n * The Statement Execution API returns a single string cell — this normalizer\n * unwraps it. Handles both the production (real warehouse) shape and the\n * shape produced by mocked test responses.\n */\nexport function parseDescribeTableExtendedJson(\n response: DatabricksStatementExecutionResponse,\n): unknown {\n if (response.status?.state === \"FAILED\") {\n const msg = response.status.error?.message ?? \"DESCRIBE failed\";\n throw new Error(`DESCRIBE TABLE EXTENDED failed: ${msg}`);\n }\n\n const rows = response.result?.data_array ?? [];\n if (rows.length === 0) {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED returned no rows. Verify the FQN points to a metric view.\",\n );\n }\n\n const cell = rows[0]?.[0];\n if (typeof cell !== \"string\") {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED first cell was not a JSON string. Confirm the AS JSON suffix is supported.\",\n );\n }\n\n try {\n return JSON.parse(cell);\n } catch (err) {\n throw new Error(\n `Failed to parse DESCRIBE TABLE EXTENDED JSON: ${(err as Error).message}`,\n );\n }\n}\n\n/**\n * Pure function: turn the parsed DESCRIBE JSON into structured column metadata.\n *\n * Tolerant of multiple JSON shapes (the field may be `columns` or\n * `schema.fields`, type may be a string or `{ name }` object, the measure\n * marker may be `is_measure` or under `metadata.is_measure`).\n */\nexport function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] {\n if (!parsed || typeof parsed !== \"object\") {\n return [];\n }\n\n const root = parsed as Record<string, unknown>;\n const columnsCandidate = (root.columns ??\n (root.schema && typeof root.schema === \"object\"\n ? (root.schema as Record<string, unknown>).fields\n : undefined)) as unknown;\n\n if (!Array.isArray(columnsCandidate)) {\n return [];\n }\n\n const columns: MetricColumnMetadata[] = [];\n for (const raw of columnsCandidate) {\n if (!raw || typeof raw !== \"object\") continue;\n const obj = raw as Record<string, unknown>;\n const name =\n typeof obj.name === \"string\"\n ? obj.name\n : typeof obj.column_name === \"string\"\n ? obj.column_name\n : undefined;\n if (!name) continue;\n\n const typeRaw = obj.type ?? obj.data_type ?? obj.type_name;\n let type = \"STRING\";\n if (typeof typeRaw === \"string\") {\n type = typeRaw;\n } else if (typeRaw && typeof typeRaw === \"object\") {\n const inner = (typeRaw as Record<string, unknown>).name;\n if (typeof inner === \"string\") type = inner;\n }\n\n let isMeasure = false;\n if (typeof obj.is_measure === \"boolean\") {\n isMeasure = obj.is_measure;\n } else if (\n obj.metadata &&\n typeof obj.metadata === \"object\" &&\n typeof (obj.metadata as Record<string, unknown>).is_measure === \"boolean\"\n ) {\n isMeasure = (obj.metadata as Record<string, unknown>)\n .is_measure as boolean;\n } else if (obj.kind === \"measure\" || obj.role === \"measure\") {\n isMeasure = true;\n }\n\n const description =\n typeof obj.comment === \"string\"\n ? obj.comment\n : typeof obj.description === \"string\"\n ? obj.description\n : undefined;\n\n const displayName = extractStringFromAny(obj, [\n \"display_name\",\n \"displayName\",\n ]);\n const format = extractFormatString(obj);\n\n // Time-grain inference is type-driven, not YAML-attribute-driven.\n const timeGrains = isMeasure ? undefined : inferTimeGrains(type);\n\n columns.push({\n name,\n type,\n isMeasure,\n description,\n ...(displayName ? { displayName } : {}),\n ...(format ? { format } : {}),\n ...(timeGrains ? { timeGrains } : {}),\n });\n }\n\n return columns;\n}\n\n/**\n * Read a non-empty string attribute from a DESCRIBE column entry, tolerating\n * the multiple shapes UC has shipped for this metadata over time.\n */\nfunction extractStringFromAny(\n obj: Record<string, unknown>,\n candidates: readonly string[],\n): string | undefined {\n for (const key of candidates) {\n const direct = obj[key];\n if (typeof direct === \"string\" && direct.trim().length > 0) {\n return direct;\n }\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n if (typeof nested === \"string\" && nested.trim().length > 0) {\n return nested;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Read the column's `format` attribute from a DESCRIBE entry and return a\n * printf-like format string suitable for `formatValue` and `toD3Format`.\n */\nfunction extractFormatString(obj: Record<string, unknown>): string | undefined {\n for (const key of [\"format\", \"format_spec\"]) {\n const direct = obj[key];\n const fromDirect = formatStringFromValue(direct);\n if (fromDirect) return fromDirect;\n\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n const fromMeta = formatStringFromValue(nested);\n if (fromMeta) return fromMeta;\n }\n }\n return undefined;\n}\n\nfunction formatStringFromValue(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return translateStructuredFormat(value as Record<string, unknown>);\n }\n return undefined;\n}\n\n/**\n * Translate the structured `format` object emitted by DESCRIBE TABLE EXTENDED\n * AS JSON into a printf-like format string.\n */\nfunction translateStructuredFormat(\n spec: Record<string, unknown>,\n): string | undefined {\n if (spec.currency && typeof spec.currency === \"object\") {\n return currencyFormatString(spec.currency as Record<string, unknown>);\n }\n if (spec.percent && typeof spec.percent === \"object\") {\n return percentFormatString(spec.percent as Record<string, unknown>);\n }\n if (spec.number && typeof spec.number === \"object\") {\n return numberFormatString(spec.number as Record<string, unknown>);\n }\n return undefined;\n}\n\nfunction currencyFormatString(c: Record<string, unknown>): string {\n const places = readDecimalPlaces(c) ?? 2;\n const codeRaw = c.currency_code;\n const code =\n typeof codeRaw === \"string\" && codeRaw.trim().length > 0\n ? codeRaw.toUpperCase()\n : \"USD\";\n const symbol = currencySymbol(code);\n return `${symbol}#,##0${fractionalSuffix(places)}`;\n}\n\nfunction percentFormatString(p: Record<string, unknown>): string {\n const places = readDecimalPlaces(p) ?? 0;\n return `0${fractionalSuffix(places)}%`;\n}\n\nfunction numberFormatString(n: Record<string, unknown>): string {\n const places = readDecimalPlaces(n) ?? 0;\n return `#,##0${fractionalSuffix(places)}`;\n}\n\nfunction fractionalSuffix(places: number): string {\n return places > 0 ? `.${\"0\".repeat(places)}` : \"\";\n}\n\n/**\n * Maximum decimal places honored from a format spec.\n */\nconst MAX_DECIMAL_PLACES = 100;\n\nfunction readDecimalPlaces(obj: Record<string, unknown>): number | undefined {\n const dp = obj.decimal_places;\n if (typeof dp === \"number\" && Number.isFinite(dp) && dp >= 0) {\n return Math.min(Math.floor(dp), MAX_DECIMAL_PLACES);\n }\n if (dp && typeof dp === \"object\" && !Array.isArray(dp)) {\n const places = (dp as Record<string, unknown>).places;\n if (typeof places === \"number\" && Number.isFinite(places) && places >= 0) {\n return Math.min(Math.floor(places), MAX_DECIMAL_PLACES);\n }\n }\n return undefined;\n}\n\n/**\n * Map ISO currency codes to their conventional prefix symbol. Unknown codes\n * fall back to the literal code + space.\n */\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n USD: \"$\",\n EUR: \"€\",\n GBP: \"£\",\n JPY: \"¥\",\n CNY: \"¥\",\n INR: \"₹\",\n BRL: \"R$\",\n};\n\nfunction currencySymbol(code: string): string {\n return CURRENCY_SYMBOLS[code] ?? `${code} `;\n}\n\n/**\n * Infer the standard set of valid time grains for a dimension based on its\n * SQL data type.\n */\nfunction inferTimeGrains(type: string): string[] | undefined {\n const normalized = type\n .toLowerCase()\n .replace(/\\(.*\\)$/, \"\")\n .trim();\n if (\n normalized === \"timestamp\" ||\n normalized === \"timestamp_ltz\" ||\n normalized === \"timestamp_ntz\"\n ) {\n return [\"day\", \"hour\", \"minute\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n if (normalized === \"date\") {\n return [\"day\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n return undefined;\n}\n\n/**\n * Build a DescribeFetcher from a real WorkspaceClient + warehouseId.\n */\nexport function createWorkspaceDescribeFetcher(\n client: WorkspaceClient,\n warehouseId: string,\n): DescribeFetcher {\n // One format probe per fetcher (= per typegen run): the first DESCRIBE\n // discovers the warehouse's working format, every later one reuses it.\n const describeFormat: DescribeFormatMemo = {};\n return async (fqn: string) => {\n /**\n * Defense-in-depth: every caller passes a source that already cleared\n * {@link resolveMetricConfig}, but this fetcher is an exported seam — re-check\n * before interpolating into SQL.\n */\n if (!isValidFqn(fqn)) {\n throw new Error(\n `Invalid metric source \"${fqn}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view>.`,\n );\n }\n // Escape + quote every segment before interpolation. isValidFqn already\n // rejects backticks/control chars for metric sources, so this is\n // belt-and-suspenders for the SQL-injection seam — and keeps the quoting\n // independent of the naming rule.\n const quotedFqn = quoteFqnForSql(fqn);\n return describeAdaptive(\n client,\n `DESCRIBE TABLE EXTENDED ${quotedFqn} AS JSON`,\n warehouseId,\n describeFormat,\n );\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"describe.js","names":[],"sources":["../../../src/type-generator/mv-registry/describe.ts"],"sourcesContent":["// Grammar + SQL-quoting for metric-view FQNs live together in the shared,\n// zod-free leaf so the type-generator and the analytics runtime validate and\n// escape against one source of truth (see the module doc in metric-fqn.ts).\nimport {\n isValidFqn,\n quoteFqnForSql,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\nimport { type DescribeFormatMemo, describeAdaptive } from \"../statement-result\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport type { DescribeFetcher, MetricColumnMetadata } from \"./types\";\n\n/**\n * Parse the JSON payload returned by DESCRIBE TABLE EXTENDED ... AS JSON.\n *\n * The Statement Execution API returns a single string cell — this normalizer\n * unwraps it. Handles both the production (real warehouse) shape and the\n * shape produced by mocked test responses.\n */\nexport function parseDescribeTableExtendedJson(\n response: DatabricksStatementExecutionResponse,\n): unknown {\n if (response.status?.state === \"FAILED\") {\n const msg = response.status.error?.message ?? \"DESCRIBE failed\";\n throw new Error(`DESCRIBE TABLE EXTENDED failed: ${msg}`);\n }\n\n const rows = response.result?.data_array ?? [];\n if (rows.length === 0) {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED returned no rows. Verify the FQN points to a metric view.\",\n );\n }\n\n const cell = rows[0]?.[0];\n if (typeof cell !== \"string\") {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED first cell was not a JSON string. Confirm the AS JSON suffix is supported.\",\n );\n }\n\n try {\n return JSON.parse(cell);\n } catch (err) {\n throw new Error(\n `Failed to parse DESCRIBE TABLE EXTENDED JSON: ${(err as Error).message}`,\n );\n }\n}\n\n/**\n * Pure function: turn the parsed DESCRIBE JSON into structured column metadata.\n *\n * Tolerant of multiple JSON shapes (the field may be `columns` or\n * `schema.fields`, type may be a string or `{ name }` object, the measure\n * marker may be `is_measure` or under `metadata.is_measure`).\n */\nexport function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] {\n if (!parsed || typeof parsed !== \"object\") {\n return [];\n }\n\n const root = parsed as Record<string, unknown>;\n const columnsCandidate = (root.columns ??\n (root.schema && typeof root.schema === \"object\"\n ? (root.schema as Record<string, unknown>).fields\n : undefined)) as unknown;\n\n if (!Array.isArray(columnsCandidate)) {\n return [];\n }\n\n const columns: MetricColumnMetadata[] = [];\n for (const raw of columnsCandidate) {\n if (!raw || typeof raw !== \"object\") continue;\n const obj = raw as Record<string, unknown>;\n const name =\n typeof obj.name === \"string\"\n ? obj.name\n : typeof obj.column_name === \"string\"\n ? obj.column_name\n : undefined;\n if (!name) continue;\n\n const typeRaw = obj.type ?? obj.data_type ?? obj.type_name;\n let type = \"STRING\";\n if (typeof typeRaw === \"string\") {\n type = typeRaw;\n } else if (typeRaw && typeof typeRaw === \"object\") {\n const inner = (typeRaw as Record<string, unknown>).name;\n if (typeof inner === \"string\") type = inner;\n }\n\n let isMeasure = false;\n if (typeof obj.is_measure === \"boolean\") {\n isMeasure = obj.is_measure;\n } else if (\n obj.metadata &&\n typeof obj.metadata === \"object\" &&\n typeof (obj.metadata as Record<string, unknown>).is_measure === \"boolean\"\n ) {\n isMeasure = (obj.metadata as Record<string, unknown>)\n .is_measure as boolean;\n } else if (obj.kind === \"measure\" || obj.role === \"measure\") {\n isMeasure = true;\n }\n\n const description =\n typeof obj.comment === \"string\"\n ? obj.comment\n : typeof obj.description === \"string\"\n ? obj.description\n : undefined;\n\n const displayName = extractStringFromAny(obj, [\n \"display_name\",\n \"displayName\",\n ]);\n const format = extractFormatString(obj);\n\n // Time-grain inference is type-driven, not YAML-attribute-driven.\n const timeGrains = isMeasure ? undefined : inferTimeGrains(type);\n\n columns.push({\n name,\n type,\n isMeasure,\n description,\n ...(displayName ? { displayName } : {}),\n ...(format ? { format } : {}),\n ...(timeGrains ? { timeGrains } : {}),\n });\n }\n\n return columns;\n}\n\n/**\n * Read a non-empty string attribute from a DESCRIBE column entry, tolerating\n * the multiple shapes UC has shipped for this metadata over time.\n */\nfunction extractStringFromAny(\n obj: Record<string, unknown>,\n candidates: readonly string[],\n): string | undefined {\n for (const key of candidates) {\n const direct = obj[key];\n if (typeof direct === \"string\" && direct.trim().length > 0) {\n return direct;\n }\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n if (typeof nested === \"string\" && nested.trim().length > 0) {\n return nested;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Read the column's `format` attribute from a DESCRIBE entry and return a\n * printf-like format string suitable for `formatValue` and `toD3Format`.\n */\nfunction extractFormatString(obj: Record<string, unknown>): string | undefined {\n for (const key of [\"format\", \"format_spec\"]) {\n const direct = obj[key];\n const fromDirect = formatStringFromValue(direct);\n if (fromDirect) return fromDirect;\n\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n const fromMeta = formatStringFromValue(nested);\n if (fromMeta) return fromMeta;\n }\n }\n return undefined;\n}\n\nfunction formatStringFromValue(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return translateStructuredFormat(value as Record<string, unknown>);\n }\n return undefined;\n}\n\n/**\n * Translate the structured `format` object emitted by DESCRIBE TABLE EXTENDED\n * AS JSON into a printf-like format string.\n */\nfunction translateStructuredFormat(\n spec: Record<string, unknown>,\n): string | undefined {\n if (spec.currency && typeof spec.currency === \"object\") {\n return currencyFormatString(spec.currency as Record<string, unknown>);\n }\n if (spec.percent && typeof spec.percent === \"object\") {\n return percentFormatString(spec.percent as Record<string, unknown>);\n }\n if (spec.number && typeof spec.number === \"object\") {\n return numberFormatString(spec.number as Record<string, unknown>);\n }\n return undefined;\n}\n\nfunction currencyFormatString(c: Record<string, unknown>): string {\n const places = readDecimalPlaces(c) ?? 2;\n const codeRaw = c.currency_code;\n const code =\n typeof codeRaw === \"string\" && codeRaw.trim().length > 0\n ? codeRaw.toUpperCase()\n : \"USD\";\n const symbol = currencySymbol(code);\n return `${symbol}#,##0${fractionalSuffix(places)}`;\n}\n\nfunction percentFormatString(p: Record<string, unknown>): string {\n const places = readDecimalPlaces(p) ?? 0;\n return `0${fractionalSuffix(places)}%`;\n}\n\nfunction numberFormatString(n: Record<string, unknown>): string {\n const places = readDecimalPlaces(n) ?? 0;\n return `#,##0${fractionalSuffix(places)}`;\n}\n\nfunction fractionalSuffix(places: number): string {\n return places > 0 ? `.${\"0\".repeat(places)}` : \"\";\n}\n\n/**\n * Maximum decimal places honored from a format spec.\n */\nconst MAX_DECIMAL_PLACES = 100;\n\nfunction readDecimalPlaces(obj: Record<string, unknown>): number | undefined {\n const dp = obj.decimal_places;\n if (typeof dp === \"number\" && Number.isFinite(dp) && dp >= 0) {\n return Math.min(Math.floor(dp), MAX_DECIMAL_PLACES);\n }\n if (dp && typeof dp === \"object\" && !Array.isArray(dp)) {\n const places = (dp as Record<string, unknown>).places;\n if (typeof places === \"number\" && Number.isFinite(places) && places >= 0) {\n return Math.min(Math.floor(places), MAX_DECIMAL_PLACES);\n }\n }\n return undefined;\n}\n\n/**\n * Map ISO currency codes to their conventional prefix symbol. Unknown codes\n * fall back to the literal code + space.\n */\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n USD: \"$\",\n EUR: \"€\",\n GBP: \"£\",\n JPY: \"¥\",\n CNY: \"¥\",\n INR: \"₹\",\n BRL: \"R$\",\n};\n\nfunction currencySymbol(code: string): string {\n return CURRENCY_SYMBOLS[code] ?? `${code} `;\n}\n\n/**\n * Infer the standard set of valid time grains for a dimension based on its\n * SQL data type.\n */\nfunction inferTimeGrains(type: string): string[] | undefined {\n const normalized = type\n .toLowerCase()\n .replace(/\\(.*\\)$/, \"\")\n .trim();\n if (\n normalized === \"timestamp\" ||\n normalized === \"timestamp_ltz\" ||\n normalized === \"timestamp_ntz\"\n ) {\n return [\"day\", \"hour\", \"minute\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n if (normalized === \"date\") {\n return [\"day\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n return undefined;\n}\n\n/**\n * Build a DescribeFetcher from a real WorkspaceClient + warehouseId.\n */\nexport function createWorkspaceDescribeFetcher(\n client: WorkspaceClient,\n warehouseId: string,\n): DescribeFetcher {\n // One format probe per fetcher (= per typegen run): the first DESCRIBE\n // discovers the warehouse's working format, every later one reuses it.\n const describeFormat: DescribeFormatMemo = {};\n return async (fqn: string) => {\n /**\n * Defense-in-depth: every caller passes a source that already cleared\n * {@link resolveMetricConfig}, but this fetcher is an exported seam — re-check\n * before interpolating into SQL.\n */\n if (!isValidFqn(fqn)) {\n throw new Error(\n `Invalid metric source \"${fqn}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view>.`,\n );\n }\n // Escape + quote every segment before interpolation. isValidFqn already\n // rejects backticks/control chars for metric sources, so this is\n // belt-and-suspenders for the SQL-injection seam — and keeps the quoting\n // independent of the naming rule.\n const quotedFqn = quoteFqnForSql(fqn);\n return describeAdaptive(\n client,\n `DESCRIBE TABLE EXTENDED ${quotedFqn} AS JSON`,\n warehouseId,\n describeFormat,\n );\n };\n}\n"],"mappings":"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"}