@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
package/dist/index.js CHANGED
@@ -10,6 +10,9 @@ import { ServerError } from "./errors/server.js";
10
10
  import { TunnelError } from "./errors/tunnel.js";
11
11
  import { ValidationError } from "./errors/validation.js";
12
12
  import "./errors/index.js";
13
+ import { ApiError } from "./workspace-client/errors.js";
14
+ import { createWorkspaceClient } from "./workspace-client/factory.js";
15
+ import "./workspace-client/index.js";
13
16
  import { getExecutionContext } from "./context/execution-context.js";
14
17
  import { RequestedClaimsPermissionSet, createLakebasePool, generateDatabaseCredential, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup, getWorkspaceClient } from "./connectors/lakebase/index.js";
15
18
  import { SeverityNumber, SpanStatusCode } from "./telemetry/index.js";
@@ -37,4 +40,4 @@ import { server } from "./plugins/server/index.js";
37
40
  import { serving } from "./plugins/serving/serving.js";
38
41
  import "./plugins/ga-exports.generated.js";
39
42
 
40
- export { AppKitError, AuthenticationError, CacheManager, ConfigurationError, ConnectionError, ExecutionError, InitializationError, Plugin, PolicyDeniedError, READ_ACTIONS, RequestedClaimsPermissionSet, ResourceRegistry, ResourceType, ServerError, SeverityNumber, SpanStatusCode, TunnelError, ValidationError, WRITE_ACTIONS, analytics, appKitServingTypesPlugin, appKitTypesPlugin, createApp, createLakebasePool, createLakebasePoolManager, extractServingEndpoints, files, findServerFile, generateDatabaseCredential, genie, getExecutionContext, getLakebaseOrmConfig, getLakebasePgConfig, getPluginManifest, getResourceRequirements, getUsernameWithApiLookup, getWorkspaceClient, isSQLTypeMarker, jobs, lakebase, server, serving, sql, toPlugin };
43
+ export { ApiError, AppKitError, AuthenticationError, CacheManager, ConfigurationError, ConnectionError, ExecutionError, InitializationError, Plugin, PolicyDeniedError, READ_ACTIONS, RequestedClaimsPermissionSet, ResourceRegistry, ResourceType, ServerError, SeverityNumber, SpanStatusCode, TunnelError, ValidationError, WRITE_ACTIONS, analytics, appKitServingTypesPlugin, appKitTypesPlugin, createApp, createLakebasePool, createLakebasePoolManager, createWorkspaceClient, extractServingEndpoints, files, findServerFile, generateDatabaseCredential, genie, getExecutionContext, getLakebaseOrmConfig, getLakebasePgConfig, getPluginManifest, getResourceRequirements, getUsernameWithApiLookup, getWorkspaceClient, isSQLTypeMarker, jobs, lakebase, server, serving, sql, toPlugin };
@@ -1 +1 @@
1
- {"version":3,"file":"reporter.js","names":["#instance","#workspaceIdPromise","#client","#appId","#appkitVersion","#heartbeatIntervalMs","#metricsFlushIntervalMs","#heartbeatTimer","#metricsTimer","#buckets","#send","#wrap"],"sources":["../../src/internal-telemetry/reporter.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport {\n type AppkitLog,\n buildAppkitPayload,\n type RequestMetricsEvent,\n} from \"./appkit-log\";\n\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000;\nconst DEFAULT_METRICS_FLUSH_INTERVAL_MS = 60 * 1000;\n\ninterface ReporterOptions {\n workspaceId: Promise<string> | string;\n client: WorkspaceClient;\n appId: string;\n appkitVersion: string;\n heartbeatIntervalMs?: number;\n metricsFlushIntervalMs?: number;\n}\n\ninterface RequestBucket {\n count: number;\n latencyMsTotal: number;\n http4xx: number;\n http5xx: number;\n}\n\nfunction envIntervalMs(name: string, fallback: number): number {\n const raw = process.env[name];\n if (!raw) return fallback;\n const n = Number(raw);\n return Number.isFinite(n) && n > 0 ? n : fallback;\n}\n\nexport class TelemetryReporter {\n static #instance: TelemetryReporter | null = null;\n\n readonly #workspaceIdPromise: Promise<string>;\n readonly #client: WorkspaceClient;\n readonly #appId: string;\n readonly #appkitVersion: string;\n readonly #heartbeatIntervalMs: number;\n readonly #metricsFlushIntervalMs: number;\n\n #heartbeatTimer: NodeJS.Timeout | null = null;\n #metricsTimer: NodeJS.Timeout | null = null;\n #buckets: Map<string, RequestBucket> = new Map();\n\n private constructor(opts: ReporterOptions) {\n this.#workspaceIdPromise = Promise.resolve(opts.workspaceId);\n // Mark the rejection (if any) as handled so a misconfigured workspaceId\n // doesn't trigger an unhandled-rejection warning before the first #send\n // awaits it. The original promise still rejects when awaited.\n this.#workspaceIdPromise.catch(() => {});\n this.#client = opts.client;\n this.#appId = opts.appId;\n this.#appkitVersion = opts.appkitVersion;\n this.#heartbeatIntervalMs =\n opts.heartbeatIntervalMs ??\n envIntervalMs(\n \"APPKIT_TELEMETRY_HEARTBEAT_INTERVAL_MS\",\n DEFAULT_HEARTBEAT_INTERVAL_MS,\n );\n this.#metricsFlushIntervalMs =\n opts.metricsFlushIntervalMs ??\n envIntervalMs(\n \"APPKIT_TELEMETRY_METRICS_FLUSH_INTERVAL_MS\",\n DEFAULT_METRICS_FLUSH_INTERVAL_MS,\n );\n }\n\n static initialize(opts: ReporterOptions): TelemetryReporter {\n TelemetryReporter.#instance?.stop();\n TelemetryReporter.#instance = new TelemetryReporter(opts);\n return TelemetryReporter.#instance;\n }\n\n static getInstance(): TelemetryReporter | null {\n return TelemetryReporter.#instance;\n }\n\n /** @internal Test-only reset. */\n static _reset(): void {\n TelemetryReporter.#instance?.stop();\n TelemetryReporter.#instance = null;\n }\n\n start(): void {\n if (this.#heartbeatTimer || this.#metricsTimer) return;\n this.#heartbeatTimer = setInterval(() => {\n this.sendHeartbeat().catch(() => {});\n }, this.#heartbeatIntervalMs);\n this.#heartbeatTimer.unref?.();\n\n this.#metricsTimer = setInterval(() => {\n this.flushRequestMetrics().catch(() => {});\n }, this.#metricsFlushIntervalMs);\n this.#metricsTimer.unref?.();\n }\n\n stop(): void {\n if (this.#heartbeatTimer) clearInterval(this.#heartbeatTimer);\n if (this.#metricsTimer) clearInterval(this.#metricsTimer);\n this.#heartbeatTimer = null;\n this.#metricsTimer = null;\n }\n\n recordRequest(\n method: string,\n routeTemplate: string,\n statusCode: number,\n latencyMs: number,\n ): void {\n if (!routeTemplate) return;\n const key = `${method.toUpperCase()} ${routeTemplate}`;\n const bucket = this.#buckets.get(key) ?? {\n count: 0,\n latencyMsTotal: 0,\n http4xx: 0,\n http5xx: 0,\n };\n bucket.count += 1;\n bucket.latencyMsTotal += Math.max(0, latencyMs);\n if (statusCode >= 400 && statusCode < 500) bucket.http4xx += 1;\n if (statusCode >= 500 && statusCode < 600) bucket.http5xx += 1;\n this.#buckets.set(key, bucket);\n }\n\n async sendStartup(): Promise<void> {\n await this.#send([\n this.#wrap({ event_name: \"APP_STARTUP\", app_startup_event: {} }),\n ]);\n }\n\n async sendHeartbeat(): Promise<void> {\n await this.#send([\n this.#wrap({ event_name: \"HEARTBEAT\", heartbeat_event: {} }),\n ]);\n }\n\n async flushRequestMetrics(): Promise<void> {\n if (this.#buckets.size === 0) return;\n const drained = this.#buckets;\n this.#buckets = new Map();\n\n const logs: AppkitLog[] = [];\n for (const [endpoint, bucket] of drained) {\n const event: RequestMetricsEvent = {\n endpoint,\n request_count: bucket.count,\n request_latency_ms_avg: Math.round(\n bucket.latencyMsTotal / bucket.count,\n ),\n response_count_http4xx: bucket.http4xx,\n response_count_http5xx: bucket.http5xx,\n };\n logs.push(\n this.#wrap({\n event_name: \"REQUEST_METRICS\",\n request_metrics_event: event,\n }),\n );\n }\n await this.#send(logs);\n }\n\n #wrap(partial: AppkitLog): AppkitLog {\n return {\n ...partial,\n app_id: this.#appId,\n appkit_version: this.#appkitVersion,\n };\n }\n\n async #send(logs: AppkitLog[]): Promise<void> {\n if (logs.length === 0) return;\n const workspaceId = await this.#workspaceIdPromise;\n await this.#client.apiClient.request({\n path: \"/telemetry-ext\",\n method: \"POST\",\n query: { o: workspaceId },\n headers: new Headers(),\n payload: buildAppkitPayload(logs),\n raw: false,\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"reporter.js","names":["#instance","#workspaceIdPromise","#client","#appId","#appkitVersion","#heartbeatIntervalMs","#metricsFlushIntervalMs","#heartbeatTimer","#metricsTimer","#buckets","#send","#wrap"],"sources":["../../src/internal-telemetry/reporter.ts"],"sourcesContent":["import type { WorkspaceClient } from \"../workspace-client\";\nimport {\n type AppkitLog,\n buildAppkitPayload,\n type RequestMetricsEvent,\n} from \"./appkit-log\";\n\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000;\nconst DEFAULT_METRICS_FLUSH_INTERVAL_MS = 60 * 1000;\n\ninterface ReporterOptions {\n workspaceId: Promise<string> | string;\n client: WorkspaceClient;\n appId: string;\n appkitVersion: string;\n heartbeatIntervalMs?: number;\n metricsFlushIntervalMs?: number;\n}\n\ninterface RequestBucket {\n count: number;\n latencyMsTotal: number;\n http4xx: number;\n http5xx: number;\n}\n\nfunction envIntervalMs(name: string, fallback: number): number {\n const raw = process.env[name];\n if (!raw) return fallback;\n const n = Number(raw);\n return Number.isFinite(n) && n > 0 ? n : fallback;\n}\n\nexport class TelemetryReporter {\n static #instance: TelemetryReporter | null = null;\n\n readonly #workspaceIdPromise: Promise<string>;\n readonly #client: WorkspaceClient;\n readonly #appId: string;\n readonly #appkitVersion: string;\n readonly #heartbeatIntervalMs: number;\n readonly #metricsFlushIntervalMs: number;\n\n #heartbeatTimer: NodeJS.Timeout | null = null;\n #metricsTimer: NodeJS.Timeout | null = null;\n #buckets: Map<string, RequestBucket> = new Map();\n\n private constructor(opts: ReporterOptions) {\n this.#workspaceIdPromise = Promise.resolve(opts.workspaceId);\n // Mark the rejection (if any) as handled so a misconfigured workspaceId\n // doesn't trigger an unhandled-rejection warning before the first #send\n // awaits it. The original promise still rejects when awaited.\n this.#workspaceIdPromise.catch(() => {});\n this.#client = opts.client;\n this.#appId = opts.appId;\n this.#appkitVersion = opts.appkitVersion;\n this.#heartbeatIntervalMs =\n opts.heartbeatIntervalMs ??\n envIntervalMs(\n \"APPKIT_TELEMETRY_HEARTBEAT_INTERVAL_MS\",\n DEFAULT_HEARTBEAT_INTERVAL_MS,\n );\n this.#metricsFlushIntervalMs =\n opts.metricsFlushIntervalMs ??\n envIntervalMs(\n \"APPKIT_TELEMETRY_METRICS_FLUSH_INTERVAL_MS\",\n DEFAULT_METRICS_FLUSH_INTERVAL_MS,\n );\n }\n\n static initialize(opts: ReporterOptions): TelemetryReporter {\n TelemetryReporter.#instance?.stop();\n TelemetryReporter.#instance = new TelemetryReporter(opts);\n return TelemetryReporter.#instance;\n }\n\n static getInstance(): TelemetryReporter | null {\n return TelemetryReporter.#instance;\n }\n\n /** @internal Test-only reset. */\n static _reset(): void {\n TelemetryReporter.#instance?.stop();\n TelemetryReporter.#instance = null;\n }\n\n start(): void {\n if (this.#heartbeatTimer || this.#metricsTimer) return;\n this.#heartbeatTimer = setInterval(() => {\n this.sendHeartbeat().catch(() => {});\n }, this.#heartbeatIntervalMs);\n this.#heartbeatTimer.unref?.();\n\n this.#metricsTimer = setInterval(() => {\n this.flushRequestMetrics().catch(() => {});\n }, this.#metricsFlushIntervalMs);\n this.#metricsTimer.unref?.();\n }\n\n stop(): void {\n if (this.#heartbeatTimer) clearInterval(this.#heartbeatTimer);\n if (this.#metricsTimer) clearInterval(this.#metricsTimer);\n this.#heartbeatTimer = null;\n this.#metricsTimer = null;\n }\n\n recordRequest(\n method: string,\n routeTemplate: string,\n statusCode: number,\n latencyMs: number,\n ): void {\n if (!routeTemplate) return;\n const key = `${method.toUpperCase()} ${routeTemplate}`;\n const bucket = this.#buckets.get(key) ?? {\n count: 0,\n latencyMsTotal: 0,\n http4xx: 0,\n http5xx: 0,\n };\n bucket.count += 1;\n bucket.latencyMsTotal += Math.max(0, latencyMs);\n if (statusCode >= 400 && statusCode < 500) bucket.http4xx += 1;\n if (statusCode >= 500 && statusCode < 600) bucket.http5xx += 1;\n this.#buckets.set(key, bucket);\n }\n\n async sendStartup(): Promise<void> {\n await this.#send([\n this.#wrap({ event_name: \"APP_STARTUP\", app_startup_event: {} }),\n ]);\n }\n\n async sendHeartbeat(): Promise<void> {\n await this.#send([\n this.#wrap({ event_name: \"HEARTBEAT\", heartbeat_event: {} }),\n ]);\n }\n\n async flushRequestMetrics(): Promise<void> {\n if (this.#buckets.size === 0) return;\n const drained = this.#buckets;\n this.#buckets = new Map();\n\n const logs: AppkitLog[] = [];\n for (const [endpoint, bucket] of drained) {\n const event: RequestMetricsEvent = {\n endpoint,\n request_count: bucket.count,\n request_latency_ms_avg: Math.round(\n bucket.latencyMsTotal / bucket.count,\n ),\n response_count_http4xx: bucket.http4xx,\n response_count_http5xx: bucket.http5xx,\n };\n logs.push(\n this.#wrap({\n event_name: \"REQUEST_METRICS\",\n request_metrics_event: event,\n }),\n );\n }\n await this.#send(logs);\n }\n\n #wrap(partial: AppkitLog): AppkitLog {\n return {\n ...partial,\n app_id: this.#appId,\n appkit_version: this.#appkitVersion,\n };\n }\n\n async #send(logs: AppkitLog[]): Promise<void> {\n if (logs.length === 0) return;\n const workspaceId = await this.#workspaceIdPromise;\n await this.#client.apiClient.request({\n path: \"/telemetry-ext\",\n method: \"POST\",\n query: { o: workspaceId },\n headers: new Headers(),\n payload: buildAppkitPayload(logs),\n raw: false,\n });\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;cAyGa,eAAA,SAAwB,MAAA,YAAkB,YAAA;;SAE9C,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UACC,MAAA,EAAQ,gBAAA;EAAA,QAGlB,SAAA;EAAA,QACA,cAAA;;;AATV;;;;;;UAmBU,gBAAA;cAEI,MAAA,EAAQ,gBAAA;EAWpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA2ClB;;;;;;EAHG,mBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA4RA;;;;;;;EAAA,QAvQW,mBAAA;EAm0BI;;;;;EAvyBZ,eAAA,CAAgB,WAAA,WAAsB,OAAA;EAq1BkB;;;;EA70BxD,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAuwBA;;;;;;;;;;;;;;;EA1iBG,kBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAlViB;;;;;;EAAA,QAwkBN,qBAAA;EAphBC;;;;;;;;;EAAA,QAijBP,sBAAA;EAtfF;;;;;;;;;;;EAAA,QAshBQ,uBAAA;EApTP;;;;;;;EAycD,0BAAA,CAA2B,MAAA,EAAQ,WAAA,GAAc,OAAA;EAAd;;;;;;;;;;;;;;;;;;EAAA,QAiCjC,qBAAA;EAkHF;;;;;;;;;;;;;;;EAzDA,KAAA,CACJ,KAAA,UACA,UAAA,GAAa,MAAA,SAAe,aAAA,sBAC5B,gBAAA,GAAmB,MAAA,eACnB,MAAA,GAAS,WAAA,GACR,OAAA;EAqBG,QAAA,CAAA,GAAY,OAAA;EAAA,QAIV,KAAA;EAuBR,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EA1DqC;;;;AA2J1C;;;EAtFE,OAAA,CAAQ,IAAA,GAXE,cAAA,GAWoD,MAAA,SAAA,YAAA;EAsF1C;;;;EA9EpB,OAAA,CAAA;IA8EoB;;;2BA7JL,UAAA,GACA,MAAA,SAAe,aAAA,sBAAiC,gBAAA,GAC1C,MAAA,eAAmB,MAAA,GAC7B,WAAA,KACR,OAAA;EAAA;AAAA;;;;cAyJQ,SAAA,EAAS,QAAA,QAAA,eAAA,EAAA,gBAAA"}
1
+ {"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;cA0Ga,eAAA,SAAwB,MAAA,YAAkB,YAAA;;SAE9C,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UACC,MAAA,EAAQ,gBAAA;EAAA,QAGlB,SAAA;EAAA,QACA,cAAA;;;AATV;;;;;;UAmBU,gBAAA;cAEI,MAAA,EAAQ,gBAAA;EAWpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA2ClB;;;;;;EAHG,mBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA4RA;;;;;;;EAAA,QAvQW,mBAAA;EAm0BI;;;;;EAvyBZ,eAAA,CAAgB,WAAA,WAAsB,OAAA;EAq1BkB;;;;EA70BxD,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAuwBA;;;;;;;;;;;;;;;EA1iBG,kBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAlViB;;;;;;EAAA,QAwkBN,qBAAA;EAphBC;;;;;;;;;EAAA,QAijBP,sBAAA;EAtfF;;;;;;;;;;;EAAA,QAshBQ,uBAAA;EApTP;;;;;;;EAycD,0BAAA,CAA2B,MAAA,EAAQ,WAAA,GAAc,OAAA;EAAd;;;;;;;;;;;;;;;;;;EAAA,QAiCjC,qBAAA;EAkHF;;;;;;;;;;;;;;;EAzDA,KAAA,CACJ,KAAA,UACA,UAAA,GAAa,MAAA,SAAe,aAAA,sBAC5B,gBAAA,GAAmB,MAAA,eACnB,MAAA,GAAS,WAAA,GACR,OAAA;EAqBG,QAAA,CAAA,GAAY,OAAA;EAAA,QAIV,KAAA;EAuBR,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EA1DqC;;;;AA2J1C;;;EAtFE,OAAA,CAAQ,IAAA,GAXE,cAAA,GAWoD,MAAA,SAAA,YAAA;EAsF1C;;;;EA9EpB,OAAA,CAAA;IA8EoB;;;2BA7JL,UAAA,GACA,MAAA,SAAe,aAAA,sBAAiC,gBAAA,GAC1C,MAAA,eAAmB,MAAA,GAC7B,WAAA,KACR,OAAA;EAAA;AAAA;;;;cAyJQ,SAAA,EAAS,QAAA,QAAA,eAAA,EAAA,gBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type express from \"express\";\nimport {\n type AgentToolDefinition,\n type AnalyticsSseMessage,\n type IAppRouter,\n makeResultMessage,\n type PluginExecuteConfig,\n type SQLTypeMarker,\n type StreamExecutionSettings,\n type ToolProvider,\n} from \"shared\";\nimport { z } from \"zod\";\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n type WarehouseStatusUpdate,\n} from \"../../connectors/sql-warehouse/client\";\nimport { getWarehouseId, getWorkspaceClient } from \"../../context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport {\n buildMetricSql,\n composeMetricCacheKey,\n deriveMetricExecutorKey,\n loadMetricRegistry,\n validateMetricRequest,\n} from \"./metric\";\nimport { QueryProcessor } from \"./query\";\nimport {\n type ArrowCapability,\n deliverArrowBytes,\n deliverJsonResult,\n type QueryExecutor,\n} from \"./result-delivery\";\nimport {\n type AnalyticsQueryResponse,\n type AnalyticsStreamMessage,\n type IAnalyticsConfig,\n type IAnalyticsQueryRequest,\n type MetricRegistration,\n normalizeAnalyticsFormat,\n type WarehouseStatus,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\n/**\n * Bridges a callback-emitting async function into an async iterable.\n *\n * `start(emit)` runs concurrently; every value passed to `emit` is yielded\n * in order. The iterable completes when `start`'s promise resolves and\n * re-throws (after draining) if it rejects. Lets a callback-based progress\n * API (e.g. SQL warehouse readiness) be consumed with `for await`.\n */\nasync function* streamCallbacks<T>(\n start: (emit: (value: T) => void) => Promise<void>,\n): AsyncGenerator<T, void, unknown> {\n const queue: T[] = [];\n let wake: (() => void) | null = null;\n let settled = false;\n let error: unknown = null;\n\n const notify = (): void => {\n wake?.();\n wake = null;\n };\n\n // The .then(_, err => ...) chain converts a rejection into a resolved\n // promise; the consumer surfaces `error` after draining the queue.\n void start((value) => {\n queue.push(value);\n notify();\n }).then(\n () => {\n settled = true;\n notify();\n },\n (err) => {\n error = err;\n settled = true;\n notify();\n },\n );\n\n while (!settled || queue.length > 0) {\n while (queue.length > 0) yield queue.shift() as T;\n if (settled) break;\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n if (error) throw error;\n}\n\nexport class AnalyticsPlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"analytics\">;\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n /**\n * In-process memo of which arrow delivery mode each warehouse supports\n * (keyed by warehouse id). A standard warehouse rejects `INLINE+ARROW_STREAM`\n * on every query, so once learned we skip that doomed probe; Reyden stays\n * `\"inline\"`. Capability is a property of the warehouse, not the user, so it\n * is not user-scoped. Bounded by the number of distinct warehouses a process\n * talks to (effectively one).\n */\n private _arrowCapability = new Map<string, ArrowCapability>();\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // Metric-view route. Registered parallel to `/query`\n // measures a registered UC Metric View over the same SSE envelope.\n this.route(router, {\n name: \"metric\",\n method: \"post\",\n path: \"/metric/:key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleMetricRoute(req, res);\n },\n });\n\n // Column-names fallback for very wide Arrow schemas whose names don't fit\n // in the `X-Appkit-Arrow-Columns` response header.\n // The client hits this with the statement id from `X-Appkit-Arrow-Columns-Ref`.\n this.route(router, {\n name: \"arrow-columns\",\n method: \"get\",\n path: \"/columns/:statementId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleColumnsRoute(req, res);\n },\n });\n }\n\n /**\n * Column-names fallback endpoint. Re-derives the real column names from the\n * statement's result manifest (stateless — no server cache), for the client\n * to relabel a positional Arrow schema when the names were too large for the\n * response header.\n */\n async _handleColumnsRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { statementId } = req.params;\n const columns = await this._resolveColumnNames(req, statementId);\n if (columns && columns.length > 0) {\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.json({ columns });\n return;\n }\n res.status(404).json({\n error: \"Column names unavailable\",\n plugin: this.name,\n });\n }\n\n /**\n * Resolve a statement's real column names, trying the user's identity first\n * (required for `.obo.sql` statements, which the service principal cannot\n * `getStatement`) then falling back to the service principal (for\n * SP-executed statements). Returns undefined if neither identity can read it,\n * so the client falls back to the raw positional Arrow schema names.\n */\n private async _resolveColumnNames(\n req: express.Request,\n statementId: string,\n ): Promise<string[] | undefined> {\n const attempts: Array<() => Promise<string[] | undefined>> = [\n () => this.asUser(req)._getColumnNames(statementId),\n () => this._getColumnNames(statementId),\n ];\n for (const attempt of attempts) {\n try {\n const columns = await attempt();\n if (columns && columns.length > 0) return columns;\n } catch (error) {\n logger.debug(\n \"Arrow column-names lookup attempt failed for %s: %O\",\n statementId,\n error,\n );\n }\n }\n return undefined;\n }\n\n /**\n * Fetch column names in the current execution context. Proxied by `asUser`,\n * so `getWorkspaceClient()` resolves to the user's client when invoked via\n * `this.asUser(req)` and the service principal's otherwise.\n */\n async _getColumnNames(statementId: string): Promise<string[] | undefined> {\n return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format: rawFormat = \"JSON_ARRAY\" } =\n req.body as IAnalyticsQueryRequest;\n\n if (\n rawFormat !== \"JSON_ARRAY\" &&\n rawFormat !== \"ARROW_STREAM\" &&\n rawFormat !== \"JSON\" &&\n rawFormat !== \"ARROW\"\n ) {\n res.status(400).json({\n error: `Invalid format: ${String(rawFormat)}. Expected \"JSON_ARRAY\" or \"ARROW_STREAM\".`,\n });\n return;\n }\n\n const format = normalizeAnalyticsFormat(rawFormat);\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // ARROW_STREAM streams the raw Arrow IPC bytes back as the HTTP response\n // body — no SSE, no server-side stash, no second /arrow-result request.\n // INLINE attachments are piped straight through (the bytes are already in\n // hand from executeStatement); a warehouse that refuses INLINE falls back\n // to EXTERNAL_LINKS and streams those chunks. JSON keeps the SSE path\n // below (it carries warehouse-readiness progress + cached rows).\n if (format === \"ARROW_STREAM\") {\n await this._handleArrowStreamQuery(\n req,\n res,\n query_key,\n query,\n isAsUser,\n parameters,\n );\n return;\n }\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n format,\n hashedQuery,\n executorKey,\n ],\n };\n\n // Cache/retry/timeout are scoped to the SQL execution itself (inner\n // `execute`) so the warehouse-readiness phase isn't subject to retries\n // and the generator value never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // user-scoped trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`) the\n // fallback's `ExecutionError`s carry. Capture the original here so\n // we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const processedParams =\n await self.queryProcessor.processQueryParams(query, parameters);\n // JSON_ARRAY path: tries INLINE + JSON_ARRAY and, if the\n // warehouse only accepts ARROW_STREAM for INLINE, retries as\n // ARROW_STREAM and decodes server-side — returning the SSE\n // `result` message with plain rows. (ARROW_STREAM requests are\n // handled earlier via `_handleArrowStreamQuery`.)\n return await self._executeJsonArrayPath(\n executor,\n query,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` (e.g.\n // RESULT_TOO_LARGE_FOR_JSON_FALLBACK) and sanitized `clientMessage`\n // survive to the SSE error payload. Fall back to a generic\n // statement failure only if the original wasn't an AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Handle metric-view execution requests (`POST /api/analytics/metric/:key`).\n *\n * Mirrors {@link _handleQueryRoute}'s JSON SSE path: the outer\n * `executeStream` disables cache/retry and streams warehouse-readiness\n * (`warehouse_status`) events, then the inner `execute` builds the metric SQL\n * and delivers rows through {@link deliverJsonResult} as a `result` message.\n * The `originalError` re-throw discipline preserves each error's structured\n * `errorCode`/`clientMessage` for the SSE error payload.\n *\n * Lane dispatch is driven by the registration: an SP-lane metric runs as the\n * app service principal (shared cache); an OBO-lane metric runs\n * on-behalf-of the requesting user via `asUser(req)` (per-user cache keyed by\n * a hash of the user identity).\n */\n async _handleMetricRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { key } = req.params;\n\n logger.debug(req, \"Executing metric: %s\", key);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeMetric\").setContext(\"analytics\", {\n metric_key: key,\n plugin: this.name,\n });\n\n if (!key) {\n res.status(400).json({ error: \"metric key is required\" });\n return;\n }\n\n // Resolve the registry from disk: read + parse `definitions.json` once per\n // request (no memoization). Reads through the plugin's shared `this.app`\n // (the base `Plugin`'s `AppManager`) from `config/metric-views/` under the\n // process cwd, so this is dev-tunnel aware and inherits the traversal\n // guard — the same mechanism as the sibling `.sql` query path.\n let registry: Record<string, MetricRegistration>;\n try {\n registry = await loadMetricRegistry(this.app, req, this.devFileReader);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logger.warn(req, \"Failed to load metric registry: %s\", reason);\n event?.setContext(\"analytics\", {\n metric_registry_load_error: reason,\n });\n res.status(503).json({\n error: \"Metric registry not available\",\n code: \"METRIC_REGISTRY_LOAD_FAILED\",\n });\n return;\n }\n\n // Own-property lookup: never resolve `key` to an inherited `Object.prototype` member.\n const registration = Object.hasOwn(registry, key)\n ? registry[key]\n : undefined;\n if (!registration) {\n // Don't echo the user-supplied `key` back in the public response.\n event?.setContext(\"analytics\", { unknown_metric_key: key });\n res.status(404).json({ error: \"Metric not found\" });\n return;\n }\n\n // Validate the body on the canonical error path.\n // `validateMetricRequest` throws a `ValidationError` (400) whose message names only field paths, never raw values.\n let request: ReturnType<typeof validateMetricRequest>;\n try {\n request = validateMetricRequest(req.body ?? {});\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n event?.setContext(\"analytics\", {\n unexpected_error: err instanceof Error ? err.message : String(err),\n metric_key: key,\n });\n logger.warn(\n req,\n \"Unexpected throw during metric request validation for %s: %s\",\n key,\n err instanceof Error ? err.message : String(err),\n );\n res.status(400).json({ error: \"Invalid request body\" });\n return;\n }\n\n // Lane dispatch. The lane comes from the registration (the entry's\n // `executor` in definitions.json), NOT a URL segment or `.obo.sql`\n // filename: an OBO-lane metric runs on-behalf-of the requesting user\n // (per-user cache via `asUser(req)`), an SP-lane metric as the app service\n // principal (shared cache).\n let executor: AnalyticsPlugin;\n let executorKey: string;\n try {\n const isObo = registration.lane === \"obo\";\n executor = isObo ? this.asUser(req) : this;\n executorKey = deriveMetricExecutorKey({\n lane: registration.lane,\n userIdentity: isObo ? this.resolveUserId(req) : undefined,\n });\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n throw err;\n }\n\n // Cache key. Composed over the canonicalized args (sorted measures/\n // dimensions, stable-sorted predicates, grain, timeDimension, limit) plus\n // the `executorKey` — `\"sp\"` shares the cache across all users, a per-user\n // identity hash isolates OBO callers.\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: composeMetricCacheKey({\n metricKey: key,\n source: registration.source,\n measures: request.measures,\n dimensions: request.dimensions,\n timeGrain: request.timeGrain,\n timeDimension: request.timeDimension,\n filter: request.filter,\n format: \"JSON_ARRAY\",\n executorKey,\n limit: request.limit,\n }),\n };\n\n // Cache/retry/timeout scoped to the SQL execution itself (inner `execute`)\n // so the warehouse-readiness phase isn't retried and the generator value\n // never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`). Capture the\n // original here so we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const { statement, parameters } = buildMetricSql(\n registration,\n request,\n );\n const processedParams =\n await self.queryProcessor.processQueryParams(\n statement,\n Object.keys(parameters).length > 0 ? parameters : undefined,\n );\n // Reuse the query route's JSON delivery: INLINE JSON_ARRAY with\n // an ARROW_STREAM-inline fallback, returning plain rows in a\n // `result` message — byte-identical envelope to `/query`.\n return await self._executeJsonArrayPath(\n executor,\n statement,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` and\n // sanitized `clientMessage` survive to the SSE error payload. Fall\n // back to a generic statement failure only if it wasn't an\n // AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * JSON_ARRAY SSE path. Delegates the disposition/format fallback to\n * {@link deliverJsonResult} (INLINE JSON_ARRAY → on `needs-arrow-inline`,\n * INLINE ARROW_STREAM decoded to rows) and wraps the rows in a `result`\n * message. External links are never used for the JSON fallback.\n */\n private async _executeJsonArrayPath(\n executor: AnalyticsPlugin,\n query: string,\n processedParams:\n | Record<string, SQLTypeMarker | null | undefined>\n | undefined,\n signal?: AbortSignal,\n ): Promise<AnalyticsSseMessage> {\n const result = await deliverJsonResult(\n executor,\n query,\n processedParams,\n signal,\n );\n return makeResultMessage(result.data, {\n status: result.status,\n statement_id: result.statement_id,\n });\n }\n\n /**\n * Attach the real column names so the client can relabel the positional\n * Arrow schema (Databricks encodes ARROW_STREAM columns as col_0, …).\n *\n * Small schemas ride `X-Appkit-Arrow-Columns` directly. A very wide schema\n * whose URL-encoded names would blow the HTTP header size limit instead\n * advertises the statement id in `X-Appkit-Arrow-Columns-Ref`, and the\n * client fetches the names from `GET /columns/:statementId`.\n */\n private _setArrowColumnsHeader(\n res: express.Response,\n columnsRef: { columnNames?: string[]; statementId?: string },\n ): void {\n const names = columnsRef.columnNames;\n if (!names || names.length === 0) return;\n\n const encoded = encodeURIComponent(JSON.stringify(names));\n if (encoded.length <= MAX_ARROW_COLUMNS_HEADER_BYTES) {\n res.setHeader(\"X-Appkit-Arrow-Columns\", encoded);\n return;\n }\n if (columnsRef.statementId) {\n res.setHeader(\"X-Appkit-Arrow-Columns-Ref\", columnsRef.statementId);\n } else {\n logger.warn(\n \"Arrow column names exceed the header limit and no statement id is available for the fallback endpoint; client will fall back to the raw schema names\",\n );\n }\n }\n\n /**\n * ARROW_STREAM query handler: stream the raw Arrow IPC bytes back as the\n * HTTP response body — no SSE, no server-side stash, no second\n * `/arrow-result` request.\n *\n * The first chunk is pulled before headers are sent so a failure still\n * yields a clean JSON error; once bytes are in flight a mid-stream failure\n * can only abort the socket. Warehouse readiness is awaited (no SSE\n * progress on this path) — a no-op for a warm warehouse, a blocking wait\n * on a cold start. Runs under the user's context for `.obo.sql` queries.\n */\n private async _handleArrowStreamQuery(\n req: express.Request,\n res: express.Response,\n query_key: string,\n query: string,\n isAsUser: boolean,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n ): Promise<void> {\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n const abortController = new AbortController();\n const onClose = () => abortController.abort();\n res.on(\"close\", onClose);\n const signal = abortController.signal;\n\n // Fail-fast: bound the wait for the first byte (warehouse readiness +\n // execute + first chunk) so a stuck/overloaded warehouse returns a clear\n // 503 instead of hanging until the client gives up. Cleared once the\n // first chunk arrives — a legitimately long stream is never interrupted.\n const firstByteTimeoutMs =\n this.config.arrowFirstByteTimeoutMs ??\n DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS;\n let timedOut = false;\n const failFast = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, firstByteTimeoutMs);\n\n try {\n // Run warehouse readiness in the SAME identity context as the query:\n // for `.obo.sql`, `executor` is the `asUser(req)` proxy, so\n // `getWorkspaceClient()` inside resolves to the user's client (matching\n // the SSE path). Calling it bare here would auto-start the warehouse as\n // the service principal even for OBO requests.\n await executor._ensureArrowWarehouseReady(signal);\n\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const warehouseId = await getWarehouseId();\n\n // Populated by `deliverArrowBytes` from the result manifest before the\n // first chunk is yielded, so the header below carries the real names.\n const columnsRef: { columnNames?: string[]; statementId?: string } = {};\n const bytes = deliverArrowBytes(\n // Wrap the executor so the INLINE attempt runs through the interceptor\n // chain (cache + retry), matching the JSON path. Only inline attachments\n // are cached; EXTERNAL_LINKS carry expiring pre-signed URLs and are\n // never cached (see `_arrowCachingExecutor`).\n this._arrowCachingExecutor(\n executor,\n query_key,\n query,\n parameters,\n executorKey,\n ),\n this.SQLClient,\n query,\n processedParams,\n columnsRef,\n signal,\n {\n // Skip the doomed INLINE probe on a warehouse already known to need\n // EXTERNAL_LINKS; remember the resolved mode for next time.\n capabilityHint: this._arrowCapability.get(warehouseId),\n onCapabilityResolved: (capability) =>\n this._arrowCapability.set(warehouseId, capability),\n },\n );\n const first = await bytes.next();\n // First byte in hand — stop the fail-fast clock.\n clearTimeout(failFast);\n\n res.setHeader(\"Content-Type\", \"application/vnd.apache.arrow.stream\");\n res.setHeader(\"Cache-Control\", \"no-store\");\n this._setArrowColumnsHeader(res, columnsRef);\n\n if (!first.done) {\n await writeChunk(res, first.value);\n for await (const buf of bytes) {\n await writeChunk(res, buf);\n }\n }\n res.end();\n } catch (error) {\n clearTimeout(failFast);\n // Fail-fast timeout: the warehouse never produced a first byte. This also\n // aborts the signal, so it must be handled before the generic\n // `signal.aborted` branch below. Headers aren't sent yet (we time out\n // before the first chunk), so a clean 503 is still possible.\n if (timedOut) {\n logger.warn(\n \"Arrow query timed out before first byte after %dms\",\n firstByteTimeoutMs,\n );\n res.status(503).json({\n error:\n \"The SQL warehouse is starting or overloaded and did not respond in time. Please retry.\",\n errorCode: \"WAREHOUSE_UNAVAILABLE\",\n plugin: this.name,\n });\n return;\n }\n // Client disconnect / unmount aborts the signal (see `onClose`). That's\n // routine UI behavior, not a server error — tear down quietly whether it\n // fires before or after headers. Checked before the headersSent branch so\n // a mid-stream disconnect doesn't spam ERROR logs / alerting.\n if (signal.aborted) {\n if (res.headersSent) res.destroy();\n else res.end();\n return;\n }\n if (res.headersSent) {\n logger.error(\"Arrow query stream failed mid-flight: %O\", error);\n res.destroy(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n logger.error(\"Arrow query error: %O\", error);\n // Do not echo upstream / SDK error text — it can include statement\n // fragments and correlation ids. Keep the structured code so the\n // client can branch (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // ARROW_DELIVERY_UNSUPPORTED).\n const errorCode =\n error instanceof ExecutionError ? error.errorCode : undefined;\n res.status(500).json({\n // `clientMessage` is the sanitized, actionable text (e.g. \"Re-run with\n // JSON_ARRAY\" for ARROW_DELIVERY_UNSUPPORTED); it never carries raw\n // warehouse/SDK strings. Fall back to a generic message otherwise.\n error:\n error instanceof AppKitError\n ? error.clientMessage\n : \"Unable to execute query\",\n errorCode,\n plugin: this.name,\n });\n } finally {\n res.off(\"close\", onClose);\n }\n }\n\n /**\n * Await SQL warehouse readiness for the direct-binary Arrow path. There is no\n * SSE progress channel here — readiness is simply awaited, bounded by the\n * caller's abort signal / fail-fast timeout. Invoked via the request executor\n * (`asUser(req)` for `.obo.sql`) so `getWorkspaceClient()` resolves in the\n * correct identity context rather than defaulting to the service principal.\n */\n async _ensureArrowWarehouseReady(signal: AbortSignal): Promise<void> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStart = this.config.autoStartWarehouse ?? true;\n await this.SQLClient.ensureWarehouseRunning(workspaceClient, warehouseId, {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart,\n onStatus: () => {},\n });\n }\n\n /**\n * Wrap an executor so the `INLINE + ARROW_STREAM` attempt is served from\n * (and populates) the same per-user TTL cache the JSON path uses — otherwise\n * every arrow chart render is a fresh warehouse execution, unlike its JSON\n * twin. Caching is deliberately scoped to inline attachments:\n *\n * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the\n * same lifecycle as cached JSON rows — safe to cache.\n * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in\n * minutes; caching them would serve dead links, so those pass through\n * uncached (only the tiny link metadata would be cached anyway).\n *\n * Uses `this.cache.getOrExecute` directly rather than `this.execute()`\n * because `execute()` reduces a thrown error to `{ ok:false, message }`,\n * dropping the `errorCode` the capability fallback classifies on. The cache\n * re-throws `AppKitError`s intact and never caches a rejection, so the\n * INLINE→EXTERNAL_LINKS fallback still sees the structured rejection.\n */\n private _arrowCachingExecutor(\n executor: AnalyticsPlugin,\n query_key: string,\n query: string,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n executorKey: string,\n ): QueryExecutor {\n const hashedQuery = this.queryProcessor.hashQuery(query);\n const cache = this.cache;\n const ttl = queryDefaults.cache?.ttl;\n return {\n query: (q, params, formatParameters, signal) => {\n // Only the inline-arrow attempt is cacheable — EXTERNAL_LINKS carry\n // short-lived pre-signed URLs, so those pass straight through.\n if (\n formatParameters.disposition !== \"INLINE\" ||\n formatParameters.format !== \"ARROW_STREAM\"\n ) {\n return executor.query(q, params, formatParameters, signal);\n }\n // On a standard warehouse this throws a capability rejection — the\n // cache never stores a rejection, so the fallback still sees the\n // structured error. On Reyden it returns a bounded (<=25 MiB)\n // attachment that caches like the JSON path's rows. The shared signal\n // dedupes concurrent renders (e.g. React StrictMode double-mount).\n return cache.getOrExecute(\n [\n \"analytics:query:arrow\",\n query_key,\n JSON.stringify(parameters),\n hashedQuery,\n executorKey,\n ],\n (sharedSignal) =>\n executor.query(q, params, formatParameters, sharedSignal ?? signal),\n executorKey,\n { ttl, callerSignal: signal },\n );\n },\n };\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n private tools = {\n query: defineTool({\n description:\n \"Execute a read-only SQL query against the Databricks SQL warehouse. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted; writes are rejected. Returns the query results as JSON.\",\n schema: z.object({\n query: z\n .string()\n .describe(\n \"The SQL query to execute. Must be a SELECT, WITH, SHOW, EXPLAIN, or DESCRIBE statement.\",\n ),\n }),\n annotations: {\n effect: \"read\",\n requiresUserContext: true,\n },\n autoInheritable: true,\n execute: (args, signal) => {\n assertReadOnlySql(args.query);\n return this.query(args.query, undefined, undefined, signal);\n },\n }),\n };\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n /**\n * Returns the plugin's tools as a keyed record of `ToolkitEntry` markers.\n * Called by the agents plugin (via `resolveToolkitFromProvider`) to spread\n * a filtered, renamed view of the plugin's tools into an agent's tool\n * index. Inside the function form of `AgentDefinition.tools`, callers\n * reach this method via `plugins.analytics.toolkit(opts)`.\n */\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * Write one chunk to the response honoring backpressure: if the socket\n * buffer is full (`res.write` returns false), wait for `drain` before\n * resolving so a slow client can't balloon Node's internal write queue and\n * defeat the constant-memory goal of streaming.\n *\n * @internal exported for unit testing the backpressure/disconnect behavior.\n */\nexport function writeChunk(\n res: express.Response,\n bytes: Uint8Array,\n): Promise<void> {\n const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n // If the socket is already gone, `res.write` won't return true and the\n // `drain`/`close`/`error` events have already fired — so the promise below\n // would never settle, wedging the for-await loop and the upstream reader.\n // Reject up front instead.\n if (res.destroyed || res.writableEnded) {\n return Promise.reject(\n new DOMException(\"The response stream closed\", \"AbortError\"),\n );\n }\n if (res.write(buf)) return Promise.resolve();\n // Backpressured: resolve on `drain`, but also settle on `close`/`error`. A\n // client that disconnects mid-backpressure never emits `drain` on the\n // destroyed socket, so waiting on `drain` alone would wedge this promise —\n // and with it the awaiting for-await loop and the upstream Arrow reader —\n // forever. Rejecting instead unwinds the stream so its `finally` can cancel\n // the reader.\n return new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n res.off(\"error\", onClose);\n };\n const onDrain = () => {\n cleanup();\n resolve();\n };\n const onClose = () => {\n cleanup();\n reject(new DOMException(\"The response stream closed\", \"AbortError\"));\n };\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n res.once(\"error\", onClose);\n });\n}\n\n/**\n * Fail-fast ceiling on the wait for the first Arrow byte (warehouse\n * readiness + execute + first chunk). Past this a stuck/overloaded warehouse\n * yields a clear 503 instead of hanging. Override per plugin via\n * `arrowFirstByteTimeoutMs`.\n */\nconst DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000;\n\n/**\n * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a\n * very wide schema) the names are served via the `/columns/:statementId`\n * fallback endpoint instead of the header. Kept well under the common ~8 KiB\n * per-header limit.\n */\nconst MAX_ARROW_COLUMNS_HEADER_BYTES = 6000;\n\n/**\n * @internal\n */\nexport const analytics = toPlugin(AnalyticsPlugin);\n"],"mappings":"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"}
1
+ {"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type express from \"express\";\nimport {\n type AgentToolDefinition,\n type AnalyticsSseMessage,\n type IAppRouter,\n makeResultMessage,\n type PluginExecuteConfig,\n type SQLTypeMarker,\n type StreamExecutionSettings,\n type ToolProvider,\n} from \"shared\";\nimport { z } from \"zod\";\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n type WarehouseStatusUpdate,\n} from \"../../connectors/sql-warehouse/client\";\nimport { getWarehouseId, getWorkspaceClient } from \"../../context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport {\n buildMetricSql,\n composeMetricCacheKey,\n deriveMetricExecutorKey,\n loadMetricRegistry,\n validateMetricRequest,\n} from \"./metric\";\nimport { QueryProcessor } from \"./query\";\nimport {\n type ArrowCapability,\n deliverArrowBytes,\n deliverJsonResult,\n type QueryExecutor,\n} from \"./result-delivery\";\nimport {\n type AnalyticsQueryResponse,\n type AnalyticsStreamMessage,\n type IAnalyticsConfig,\n type IAnalyticsQueryRequest,\n type MetricRegistration,\n normalizeAnalyticsFormat,\n type WarehouseStatus,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\n/**\n * Bridges a callback-emitting async function into an async iterable.\n *\n * `start(emit)` runs concurrently; every value passed to `emit` is yielded\n * in order. The iterable completes when `start`'s promise resolves and\n * re-throws (after draining) if it rejects. Lets a callback-based progress\n * API (e.g. SQL warehouse readiness) be consumed with `for await`.\n */\nasync function* streamCallbacks<T>(\n start: (emit: (value: T) => void) => Promise<void>,\n): AsyncGenerator<T, void, unknown> {\n const queue: T[] = [];\n let wake: (() => void) | null = null;\n let settled = false;\n let error: unknown = null;\n\n const notify = (): void => {\n wake?.();\n wake = null;\n };\n\n // The .then(_, err => ...) chain converts a rejection into a resolved\n // promise; the consumer surfaces `error` after draining the queue.\n void start((value) => {\n queue.push(value);\n notify();\n }).then(\n () => {\n settled = true;\n notify();\n },\n (err) => {\n error = err;\n settled = true;\n notify();\n },\n );\n\n while (!settled || queue.length > 0) {\n while (queue.length > 0) yield queue.shift() as T;\n if (settled) break;\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n if (error) throw error;\n}\n\nexport class AnalyticsPlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"analytics\">;\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n /**\n * In-process memo of which arrow delivery mode each warehouse supports\n * (keyed by warehouse id). A standard warehouse rejects `INLINE+ARROW_STREAM`\n * on every query, so once learned we skip that doomed probe; Reyden stays\n * `\"inline\"`. Capability is a property of the warehouse, not the user, so it\n * is not user-scoped. Bounded by the number of distinct warehouses a process\n * talks to (effectively one).\n */\n private _arrowCapability = new Map<string, ArrowCapability>();\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // Metric-view route. Registered parallel to `/query`\n // measures a registered UC Metric View over the same SSE envelope.\n this.route(router, {\n name: \"metric\",\n method: \"post\",\n path: \"/metric/:key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleMetricRoute(req, res);\n },\n });\n\n // Column-names fallback for very wide Arrow schemas whose names don't fit\n // in the `X-Appkit-Arrow-Columns` response header.\n // The client hits this with the statement id from `X-Appkit-Arrow-Columns-Ref`.\n this.route(router, {\n name: \"arrow-columns\",\n method: \"get\",\n path: \"/columns/:statementId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleColumnsRoute(req, res);\n },\n });\n }\n\n /**\n * Column-names fallback endpoint. Re-derives the real column names from the\n * statement's result manifest (stateless — no server cache), for the client\n * to relabel a positional Arrow schema when the names were too large for the\n * response header.\n */\n async _handleColumnsRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { statementId } = req.params;\n const columns = await this._resolveColumnNames(req, statementId);\n if (columns && columns.length > 0) {\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.json({ columns });\n return;\n }\n res.status(404).json({\n error: \"Column names unavailable\",\n plugin: this.name,\n });\n }\n\n /**\n * Resolve a statement's real column names, trying the user's identity first\n * (required for `.obo.sql` statements, which the service principal cannot\n * `getStatement`) then falling back to the service principal (for\n * SP-executed statements). Returns undefined if neither identity can read it,\n * so the client falls back to the raw positional Arrow schema names.\n */\n private async _resolveColumnNames(\n req: express.Request,\n statementId: string,\n ): Promise<string[] | undefined> {\n const attempts: Array<() => Promise<string[] | undefined>> = [\n () => this.asUser(req)._getColumnNames(statementId),\n () => this._getColumnNames(statementId),\n ];\n for (const attempt of attempts) {\n try {\n const columns = await attempt();\n if (columns && columns.length > 0) return columns;\n } catch (error) {\n logger.debug(\n \"Arrow column-names lookup attempt failed for %s: %O\",\n statementId,\n error,\n );\n }\n }\n return undefined;\n }\n\n /**\n * Fetch column names in the current execution context. Proxied by `asUser`,\n * so `getWorkspaceClient()` resolves to the user's client when invoked via\n * `this.asUser(req)` and the service principal's otherwise.\n */\n async _getColumnNames(statementId: string): Promise<string[] | undefined> {\n return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format: rawFormat = \"JSON_ARRAY\" } =\n req.body as IAnalyticsQueryRequest;\n\n if (\n rawFormat !== \"JSON_ARRAY\" &&\n rawFormat !== \"ARROW_STREAM\" &&\n rawFormat !== \"JSON\" &&\n rawFormat !== \"ARROW\"\n ) {\n res.status(400).json({\n error: `Invalid format: ${String(rawFormat)}. Expected \"JSON_ARRAY\" or \"ARROW_STREAM\".`,\n });\n return;\n }\n\n const format = normalizeAnalyticsFormat(rawFormat);\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // ARROW_STREAM streams the raw Arrow IPC bytes back as the HTTP response\n // body — no SSE, no server-side stash, no second /arrow-result request.\n // INLINE attachments are piped straight through (the bytes are already in\n // hand from executeStatement); a warehouse that refuses INLINE falls back\n // to EXTERNAL_LINKS and streams those chunks. JSON keeps the SSE path\n // below (it carries warehouse-readiness progress + cached rows).\n if (format === \"ARROW_STREAM\") {\n await this._handleArrowStreamQuery(\n req,\n res,\n query_key,\n query,\n isAsUser,\n parameters,\n );\n return;\n }\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n format,\n hashedQuery,\n executorKey,\n ],\n };\n\n // Cache/retry/timeout are scoped to the SQL execution itself (inner\n // `execute`) so the warehouse-readiness phase isn't subject to retries\n // and the generator value never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // user-scoped trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`) the\n // fallback's `ExecutionError`s carry. Capture the original here so\n // we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const processedParams =\n await self.queryProcessor.processQueryParams(query, parameters);\n // JSON_ARRAY path: tries INLINE + JSON_ARRAY and, if the\n // warehouse only accepts ARROW_STREAM for INLINE, retries as\n // ARROW_STREAM and decodes server-side — returning the SSE\n // `result` message with plain rows. (ARROW_STREAM requests are\n // handled earlier via `_handleArrowStreamQuery`.)\n return await self._executeJsonArrayPath(\n executor,\n query,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` (e.g.\n // RESULT_TOO_LARGE_FOR_JSON_FALLBACK) and sanitized `clientMessage`\n // survive to the SSE error payload. Fall back to a generic\n // statement failure only if the original wasn't an AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Handle metric-view execution requests (`POST /api/analytics/metric/:key`).\n *\n * Mirrors {@link _handleQueryRoute}'s JSON SSE path: the outer\n * `executeStream` disables cache/retry and streams warehouse-readiness\n * (`warehouse_status`) events, then the inner `execute` builds the metric SQL\n * and delivers rows through {@link deliverJsonResult} as a `result` message.\n * The `originalError` re-throw discipline preserves each error's structured\n * `errorCode`/`clientMessage` for the SSE error payload.\n *\n * Lane dispatch is driven by the registration: an SP-lane metric runs as the\n * app service principal (shared cache); an OBO-lane metric runs\n * on-behalf-of the requesting user via `asUser(req)` (per-user cache keyed by\n * a hash of the user identity).\n */\n async _handleMetricRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { key } = req.params;\n\n logger.debug(req, \"Executing metric: %s\", key);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeMetric\").setContext(\"analytics\", {\n metric_key: key,\n plugin: this.name,\n });\n\n if (!key) {\n res.status(400).json({ error: \"metric key is required\" });\n return;\n }\n\n // Resolve the registry from disk: read + parse `definitions.json` once per\n // request (no memoization). Reads through the plugin's shared `this.app`\n // (the base `Plugin`'s `AppManager`) from `config/metric-views/` under the\n // process cwd, so this is dev-tunnel aware and inherits the traversal\n // guard — the same mechanism as the sibling `.sql` query path.\n let registry: Record<string, MetricRegistration>;\n try {\n registry = await loadMetricRegistry(this.app, req, this.devFileReader);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logger.warn(req, \"Failed to load metric registry: %s\", reason);\n event?.setContext(\"analytics\", {\n metric_registry_load_error: reason,\n });\n res.status(503).json({\n error: \"Metric registry not available\",\n code: \"METRIC_REGISTRY_LOAD_FAILED\",\n });\n return;\n }\n\n // Own-property lookup: never resolve `key` to an inherited `Object.prototype` member.\n const registration = Object.hasOwn(registry, key)\n ? registry[key]\n : undefined;\n if (!registration) {\n // Don't echo the user-supplied `key` back in the public response.\n event?.setContext(\"analytics\", { unknown_metric_key: key });\n res.status(404).json({ error: \"Metric not found\" });\n return;\n }\n\n // Validate the body on the canonical error path.\n // `validateMetricRequest` throws a `ValidationError` (400) whose message names only field paths, never raw values.\n let request: ReturnType<typeof validateMetricRequest>;\n try {\n request = validateMetricRequest(req.body ?? {});\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n event?.setContext(\"analytics\", {\n unexpected_error: err instanceof Error ? err.message : String(err),\n metric_key: key,\n });\n logger.warn(\n req,\n \"Unexpected throw during metric request validation for %s: %s\",\n key,\n err instanceof Error ? err.message : String(err),\n );\n res.status(400).json({ error: \"Invalid request body\" });\n return;\n }\n\n // Lane dispatch. The lane comes from the registration (the entry's\n // `executor` in definitions.json), NOT a URL segment or `.obo.sql`\n // filename: an OBO-lane metric runs on-behalf-of the requesting user\n // (per-user cache via `asUser(req)`), an SP-lane metric as the app service\n // principal (shared cache).\n let executor: AnalyticsPlugin;\n let executorKey: string;\n try {\n const isObo = registration.lane === \"obo\";\n executor = isObo ? this.asUser(req) : this;\n executorKey = deriveMetricExecutorKey({\n lane: registration.lane,\n userIdentity: isObo ? this.resolveUserId(req) : undefined,\n });\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n throw err;\n }\n\n // Cache key. Composed over the canonicalized args (sorted measures/\n // dimensions, stable-sorted predicates, grain, timeDimension, limit) plus\n // the `executorKey` — `\"sp\"` shares the cache across all users, a per-user\n // identity hash isolates OBO callers.\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: composeMetricCacheKey({\n metricKey: key,\n source: registration.source,\n measures: request.measures,\n dimensions: request.dimensions,\n timeGrain: request.timeGrain,\n timeDimension: request.timeDimension,\n filter: request.filter,\n format: \"JSON_ARRAY\",\n executorKey,\n limit: request.limit,\n }),\n };\n\n // Cache/retry/timeout scoped to the SQL execution itself (inner `execute`)\n // so the warehouse-readiness phase isn't retried and the generator value\n // never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`). Capture the\n // original here so we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const { statement, parameters } = buildMetricSql(\n registration,\n request,\n );\n const processedParams =\n await self.queryProcessor.processQueryParams(\n statement,\n Object.keys(parameters).length > 0 ? parameters : undefined,\n );\n // Reuse the query route's JSON delivery: INLINE JSON_ARRAY with\n // an ARROW_STREAM-inline fallback, returning plain rows in a\n // `result` message — byte-identical envelope to `/query`.\n return await self._executeJsonArrayPath(\n executor,\n statement,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` and\n // sanitized `clientMessage` survive to the SSE error payload. Fall\n // back to a generic statement failure only if it wasn't an\n // AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * JSON_ARRAY SSE path. Delegates the disposition/format fallback to\n * {@link deliverJsonResult} (INLINE JSON_ARRAY → on `needs-arrow-inline`,\n * INLINE ARROW_STREAM decoded to rows) and wraps the rows in a `result`\n * message. External links are never used for the JSON fallback.\n */\n private async _executeJsonArrayPath(\n executor: AnalyticsPlugin,\n query: string,\n processedParams:\n | Record<string, SQLTypeMarker | null | undefined>\n | undefined,\n signal?: AbortSignal,\n ): Promise<AnalyticsSseMessage> {\n const result = await deliverJsonResult(\n executor,\n query,\n processedParams,\n signal,\n );\n return makeResultMessage(result.data, {\n status: result.status,\n statement_id: result.statement_id,\n });\n }\n\n /**\n * Attach the real column names so the client can relabel the positional\n * Arrow schema (Databricks encodes ARROW_STREAM columns as col_0, …).\n *\n * Small schemas ride `X-Appkit-Arrow-Columns` directly. A very wide schema\n * whose URL-encoded names would blow the HTTP header size limit instead\n * advertises the statement id in `X-Appkit-Arrow-Columns-Ref`, and the\n * client fetches the names from `GET /columns/:statementId`.\n */\n private _setArrowColumnsHeader(\n res: express.Response,\n columnsRef: { columnNames?: string[]; statementId?: string },\n ): void {\n const names = columnsRef.columnNames;\n if (!names || names.length === 0) return;\n\n const encoded = encodeURIComponent(JSON.stringify(names));\n if (encoded.length <= MAX_ARROW_COLUMNS_HEADER_BYTES) {\n res.setHeader(\"X-Appkit-Arrow-Columns\", encoded);\n return;\n }\n if (columnsRef.statementId) {\n res.setHeader(\"X-Appkit-Arrow-Columns-Ref\", columnsRef.statementId);\n } else {\n logger.warn(\n \"Arrow column names exceed the header limit and no statement id is available for the fallback endpoint; client will fall back to the raw schema names\",\n );\n }\n }\n\n /**\n * ARROW_STREAM query handler: stream the raw Arrow IPC bytes back as the\n * HTTP response body — no SSE, no server-side stash, no second\n * `/arrow-result` request.\n *\n * The first chunk is pulled before headers are sent so a failure still\n * yields a clean JSON error; once bytes are in flight a mid-stream failure\n * can only abort the socket. Warehouse readiness is awaited (no SSE\n * progress on this path) — a no-op for a warm warehouse, a blocking wait\n * on a cold start. Runs under the user's context for `.obo.sql` queries.\n */\n private async _handleArrowStreamQuery(\n req: express.Request,\n res: express.Response,\n query_key: string,\n query: string,\n isAsUser: boolean,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n ): Promise<void> {\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n const abortController = new AbortController();\n const onClose = () => abortController.abort();\n res.on(\"close\", onClose);\n const signal = abortController.signal;\n\n // Fail-fast: bound the wait for the first byte (warehouse readiness +\n // execute + first chunk) so a stuck/overloaded warehouse returns a clear\n // 503 instead of hanging until the client gives up. Cleared once the\n // first chunk arrives — a legitimately long stream is never interrupted.\n const firstByteTimeoutMs =\n this.config.arrowFirstByteTimeoutMs ??\n DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS;\n let timedOut = false;\n const failFast = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, firstByteTimeoutMs);\n\n try {\n // Run warehouse readiness in the SAME identity context as the query:\n // for `.obo.sql`, `executor` is the `asUser(req)` proxy, so\n // `getWorkspaceClient()` inside resolves to the user's client (matching\n // the SSE path). Calling it bare here would auto-start the warehouse as\n // the service principal even for OBO requests.\n await executor._ensureArrowWarehouseReady(signal);\n\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const warehouseId = await getWarehouseId();\n\n // Populated by `deliverArrowBytes` from the result manifest before the\n // first chunk is yielded, so the header below carries the real names.\n const columnsRef: { columnNames?: string[]; statementId?: string } = {};\n const bytes = deliverArrowBytes(\n // Wrap the executor so the INLINE attempt runs through the interceptor\n // chain (cache + retry), matching the JSON path. Only inline attachments\n // are cached; EXTERNAL_LINKS carry expiring pre-signed URLs and are\n // never cached (see `_arrowCachingExecutor`).\n this._arrowCachingExecutor(\n executor,\n query_key,\n query,\n parameters,\n executorKey,\n ),\n this.SQLClient,\n query,\n processedParams,\n columnsRef,\n signal,\n {\n // Skip the doomed INLINE probe on a warehouse already known to need\n // EXTERNAL_LINKS; remember the resolved mode for next time.\n capabilityHint: this._arrowCapability.get(warehouseId),\n onCapabilityResolved: (capability) =>\n this._arrowCapability.set(warehouseId, capability),\n },\n );\n const first = await bytes.next();\n // First byte in hand — stop the fail-fast clock.\n clearTimeout(failFast);\n\n res.setHeader(\"Content-Type\", \"application/vnd.apache.arrow.stream\");\n res.setHeader(\"Cache-Control\", \"no-store\");\n this._setArrowColumnsHeader(res, columnsRef);\n\n if (!first.done) {\n await writeChunk(res, first.value);\n for await (const buf of bytes) {\n await writeChunk(res, buf);\n }\n }\n res.end();\n } catch (error) {\n clearTimeout(failFast);\n // Fail-fast timeout: the warehouse never produced a first byte. This also\n // aborts the signal, so it must be handled before the generic\n // `signal.aborted` branch below. Headers aren't sent yet (we time out\n // before the first chunk), so a clean 503 is still possible.\n if (timedOut) {\n logger.warn(\n \"Arrow query timed out before first byte after %dms\",\n firstByteTimeoutMs,\n );\n res.status(503).json({\n error:\n \"The SQL warehouse is starting or overloaded and did not respond in time. Please retry.\",\n errorCode: \"WAREHOUSE_UNAVAILABLE\",\n plugin: this.name,\n });\n return;\n }\n // Client disconnect / unmount aborts the signal (see `onClose`). That's\n // routine UI behavior, not a server error — tear down quietly whether it\n // fires before or after headers. Checked before the headersSent branch so\n // a mid-stream disconnect doesn't spam ERROR logs / alerting.\n if (signal.aborted) {\n if (res.headersSent) res.destroy();\n else res.end();\n return;\n }\n if (res.headersSent) {\n logger.error(\"Arrow query stream failed mid-flight: %O\", error);\n res.destroy(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n logger.error(\"Arrow query error: %O\", error);\n // Do not echo upstream / SDK error text — it can include statement\n // fragments and correlation ids. Keep the structured code so the\n // client can branch (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // ARROW_DELIVERY_UNSUPPORTED).\n const errorCode =\n error instanceof ExecutionError ? error.errorCode : undefined;\n res.status(500).json({\n // `clientMessage` is the sanitized, actionable text (e.g. \"Re-run with\n // JSON_ARRAY\" for ARROW_DELIVERY_UNSUPPORTED); it never carries raw\n // warehouse/SDK strings. Fall back to a generic message otherwise.\n error:\n error instanceof AppKitError\n ? error.clientMessage\n : \"Unable to execute query\",\n errorCode,\n plugin: this.name,\n });\n } finally {\n res.off(\"close\", onClose);\n }\n }\n\n /**\n * Await SQL warehouse readiness for the direct-binary Arrow path. There is no\n * SSE progress channel here — readiness is simply awaited, bounded by the\n * caller's abort signal / fail-fast timeout. Invoked via the request executor\n * (`asUser(req)` for `.obo.sql`) so `getWorkspaceClient()` resolves in the\n * correct identity context rather than defaulting to the service principal.\n */\n async _ensureArrowWarehouseReady(signal: AbortSignal): Promise<void> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStart = this.config.autoStartWarehouse ?? true;\n await this.SQLClient.ensureWarehouseRunning(workspaceClient, warehouseId, {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart,\n onStatus: () => {},\n });\n }\n\n /**\n * Wrap an executor so the `INLINE + ARROW_STREAM` attempt is served from\n * (and populates) the same per-user TTL cache the JSON path uses — otherwise\n * every arrow chart render is a fresh warehouse execution, unlike its JSON\n * twin. Caching is deliberately scoped to inline attachments:\n *\n * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the\n * same lifecycle as cached JSON rows — safe to cache.\n * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in\n * minutes; caching them would serve dead links, so those pass through\n * uncached (only the tiny link metadata would be cached anyway).\n *\n * Uses `this.cache.getOrExecute` directly rather than `this.execute()`\n * because `execute()` reduces a thrown error to `{ ok:false, message }`,\n * dropping the `errorCode` the capability fallback classifies on. The cache\n * re-throws `AppKitError`s intact and never caches a rejection, so the\n * INLINE→EXTERNAL_LINKS fallback still sees the structured rejection.\n */\n private _arrowCachingExecutor(\n executor: AnalyticsPlugin,\n query_key: string,\n query: string,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n executorKey: string,\n ): QueryExecutor {\n const hashedQuery = this.queryProcessor.hashQuery(query);\n const cache = this.cache;\n const ttl = queryDefaults.cache?.ttl;\n return {\n query: (q, params, formatParameters, signal) => {\n // Only the inline-arrow attempt is cacheable — EXTERNAL_LINKS carry\n // short-lived pre-signed URLs, so those pass straight through.\n if (\n formatParameters.disposition !== \"INLINE\" ||\n formatParameters.format !== \"ARROW_STREAM\"\n ) {\n return executor.query(q, params, formatParameters, signal);\n }\n // On a standard warehouse this throws a capability rejection — the\n // cache never stores a rejection, so the fallback still sees the\n // structured error. On Reyden it returns a bounded (<=25 MiB)\n // attachment that caches like the JSON path's rows. The shared signal\n // dedupes concurrent renders (e.g. React StrictMode double-mount).\n return cache.getOrExecute(\n [\n \"analytics:query:arrow\",\n query_key,\n JSON.stringify(parameters),\n hashedQuery,\n executorKey,\n ],\n (sharedSignal) =>\n executor.query(q, params, formatParameters, sharedSignal ?? signal),\n executorKey,\n { ttl, callerSignal: signal },\n );\n },\n };\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n private tools = {\n query: defineTool({\n description:\n \"Execute a read-only SQL query against the Databricks SQL warehouse. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted; writes are rejected. Returns the query results as JSON.\",\n schema: z.object({\n query: z\n .string()\n .describe(\n \"The SQL query to execute. Must be a SELECT, WITH, SHOW, EXPLAIN, or DESCRIBE statement.\",\n ),\n }),\n annotations: {\n effect: \"read\",\n requiresUserContext: true,\n },\n autoInheritable: true,\n execute: (args, signal) => {\n assertReadOnlySql(args.query);\n return this.query(args.query, undefined, undefined, signal);\n },\n }),\n };\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n /**\n * Returns the plugin's tools as a keyed record of `ToolkitEntry` markers.\n * Called by the agents plugin (via `resolveToolkitFromProvider`) to spread\n * a filtered, renamed view of the plugin's tools into an agent's tool\n * index. Inside the function form of `AgentDefinition.tools`, callers\n * reach this method via `plugins.analytics.toolkit(opts)`.\n */\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * Write one chunk to the response honoring backpressure: if the socket\n * buffer is full (`res.write` returns false), wait for `drain` before\n * resolving so a slow client can't balloon Node's internal write queue and\n * defeat the constant-memory goal of streaming.\n *\n * @internal exported for unit testing the backpressure/disconnect behavior.\n */\nexport function writeChunk(\n res: express.Response,\n bytes: Uint8Array,\n): Promise<void> {\n const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n // If the socket is already gone, `res.write` won't return true and the\n // `drain`/`close`/`error` events have already fired — so the promise below\n // would never settle, wedging the for-await loop and the upstream reader.\n // Reject up front instead.\n if (res.destroyed || res.writableEnded) {\n return Promise.reject(\n new DOMException(\"The response stream closed\", \"AbortError\"),\n );\n }\n if (res.write(buf)) return Promise.resolve();\n // Backpressured: resolve on `drain`, but also settle on `close`/`error`. A\n // client that disconnects mid-backpressure never emits `drain` on the\n // destroyed socket, so waiting on `drain` alone would wedge this promise —\n // and with it the awaiting for-await loop and the upstream Arrow reader —\n // forever. Rejecting instead unwinds the stream so its `finally` can cancel\n // the reader.\n return new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n res.off(\"error\", onClose);\n };\n const onDrain = () => {\n cleanup();\n resolve();\n };\n const onClose = () => {\n cleanup();\n reject(new DOMException(\"The response stream closed\", \"AbortError\"));\n };\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n res.once(\"error\", onClose);\n });\n}\n\n/**\n * Fail-fast ceiling on the wait for the first Arrow byte (warehouse\n * readiness + execute + first chunk). Past this a stuck/overloaded warehouse\n * yields a clear 503 instead of hanging. Override per plugin via\n * `arrowFirstByteTimeoutMs`.\n */\nconst DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000;\n\n/**\n * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a\n * very wide schema) the names are served via the `/columns/:statementId`\n * fallback endpoint instead of the header. Kept well under the common ~8 KiB\n * per-header limit.\n */\nconst MAX_ARROW_COLUMNS_HEADER_BYTES = 6000;\n\n/**\n * @internal\n */\nexport const analytics = toPlugin(AnalyticsPlugin);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../../src/plugins/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { sql } from \"@databricks/sdk-experimental\";\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport { getWorkspaceId } from \"../../context\";\nimport { ValidationError } from \"../../errors\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n const validParams = Array.from(queryParams).join(\", \") || \"none\";\n throw ValidationError.invalidValue(\n key,\n parameters[key],\n `a parameter defined in the query (valid: ${validParams})`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw ValidationError.invalidValue(\n key,\n value,\n \"SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())\",\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;;;AAQA,IAAa,iBAAb,MAA4B;CAC1B,MAAM,mBACJ,OACA,YAC4C;EAC5C,MAAM,YAAY,EAAE,GAAG,YAAY;EAGnC,MAAM,eAAe,MAAM,SAAS,mBAAmB;AAIvD,MAHoB,IAAI,IAAI,MAAM,KAAK,eAAe,MAAM,EAAE,GAAG,CAAC,CAGlD,IAAI,cAAc,IAAI,CAAC,UAAU,aAAa;GAC5D,MAAM,cAAc,MAAM,gBAAgB;AAC1C,OAAI,YACF,WAAU,cAAcA,IAAW,OAAO,YAAY;;AAI1D,SAAO;;CAGT,UAAU,OAAuB;AAC/B,SAAO,WAAW,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;CAGtD,uBACE,OACA,YACqE;EACrE,MAAM,gBAAkD,EAAE;AAE1D,MAAI,YAAY;GAEd,MAAM,oBAAoB,MAAM,SAAS,mBAAmB;GAC5D,MAAM,cAAc,IAAI,IAAI,MAAM,KAAK,oBAAoB,MAAM,EAAE,GAAG,CAAC;AAGvE,QAAK,MAAM,OAAO,OAAO,KAAK,WAAW,CACvC,KAAI,CAAC,YAAY,IAAI,IAAI,EAAE;IACzB,MAAM,cAAc,MAAM,KAAK,YAAY,CAAC,KAAK,KAAK,IAAI;AAC1D,UAAM,gBAAgB,aACpB,KACA,WAAW,MACX,4CAA4C,YAAY,GACzD;;AAKL,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;IACrD,MAAM,YAAY,KAAK,iBAAiB,KAAK,MAAM;AACnD,QAAI,UACF,eAAc,KAAK,UAAU;;;AAKnC,SAAO;GAAE,WAAW;GAAO,YAAY;GAAe;;CAGxD,AAAQ,iBACN,KACA,OACuC;AACvC,MAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO;AAGT,MAAI,CAAC,gBAAgB,MAAM,CACzB,OAAM,gBAAgB,aACpB,KACA,OACA,2FACD;AAGH,SAAO;GACL,MAAM;GACN,OAAO,MAAM;GACb,MAAM,MAAM;GACb"}
1
+ {"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../../src/plugins/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport { getWorkspaceId } from \"../../context\";\nimport { ValidationError } from \"../../errors\";\nimport type { sql } from \"../../workspace-client\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n const validParams = Array.from(queryParams).join(\", \") || \"none\";\n throw ValidationError.invalidValue(\n key,\n parameters[key],\n `a parameter defined in the query (valid: ${validParams})`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw ValidationError.invalidValue(\n key,\n value,\n \"SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())\",\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;;;AAQA,IAAa,iBAAb,MAA4B;CAC1B,MAAM,mBACJ,OACA,YAC4C;EAC5C,MAAM,YAAY,EAAE,GAAG,YAAY;EAGnC,MAAM,eAAe,MAAM,SAAS,mBAAmB;AAIvD,MAHoB,IAAI,IAAI,MAAM,KAAK,eAAe,MAAM,EAAE,GAAG,CAAC,CAGlD,IAAI,cAAc,IAAI,CAAC,UAAU,aAAa;GAC5D,MAAM,cAAc,MAAM,gBAAgB;AAC1C,OAAI,YACF,WAAU,cAAcA,IAAW,OAAO,YAAY;;AAI1D,SAAO;;CAGT,UAAU,OAAuB;AAC/B,SAAO,WAAW,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;CAGtD,uBACE,OACA,YACqE;EACrE,MAAM,gBAAkD,EAAE;AAE1D,MAAI,YAAY;GAEd,MAAM,oBAAoB,MAAM,SAAS,mBAAmB;GAC5D,MAAM,cAAc,IAAI,IAAI,MAAM,KAAK,oBAAoB,MAAM,EAAE,GAAG,CAAC;AAGvE,QAAK,MAAM,OAAO,OAAO,KAAK,WAAW,CACvC,KAAI,CAAC,YAAY,IAAI,IAAI,EAAE;IACzB,MAAM,cAAc,MAAM,KAAK,YAAY,CAAC,KAAK,KAAK,IAAI;AAC1D,UAAM,gBAAgB,aACpB,KACA,WAAW,MACX,4CAA4C,YAAY,GACzD;;AAKL,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;IACrD,MAAM,YAAY,KAAK,iBAAiB,KAAK,MAAM;AACnD,QAAI,UACF,eAAc,KAAK,UAAU;;;AAKnC,SAAO;GAAE,WAAW;GAAO,YAAY;GAAe;;CAGxD,AAAQ,iBACN,KACA,OACuC;AACvC,MAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO;AAGT,MAAI,CAAC,gBAAgB,MAAM,CACzB,OAAM,gBAAgB,aACpB,KACA,OACA,2FACD;AAGH,SAAO;GACL,MAAM;GACN,OAAO,MAAM;GACb,MAAM,MAAM;GACb"}
@@ -1 +1 @@
1
- {"version":3,"file":"result-delivery.js","names":[],"sources":["../../../src/plugins/analytics/result-delivery.ts"],"sourcesContent":["import type { sql } from \"@databricks/sdk-experimental\";\nimport { type DataType, type Field, Type, tableFromIPC } from \"apache-arrow\";\nimport type { SQLTypeMarker } from \"shared\";\nimport { ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { RefreshChunkLink } from \"../../stream/arrow-stream-processor\";\n\n/**\n * Centralized disposition/format fallback for analytics result delivery.\n *\n * Two warehouse capability profiles must both work, and their supported\n * combinations are mutually exclusive per warehouse:\n *\n * - **Reyden**: `ARROW_STREAM + INLINE` (and `JSON_ARRAY + INLINE`); does NOT\n * support `EXTERNAL_LINKS`.\n * - **Normal SQL warehouse**: `JSON_ARRAY + INLINE` and\n * `ARROW_STREAM + EXTERNAL_LINKS`; rejects `ARROW_STREAM + INLINE`.\n *\n * The fallback logic lives here (rather than inline in the plugin route) so it\n * can be unit-tested against a fake executor without HTTP, and so the brittle\n * \"which rejection is this\" decision is expressed once.\n */\n\nconst logger = createLogger(\"analytics:delivery\");\n\n/**\n * Minimal query surface the fallback needs. The analytics plugin (and its\n * `asUser(req)` proxy) satisfy it, so the same delivery logic runs unchanged\n * under service-principal and on-behalf-of identities.\n */\nexport interface QueryExecutor {\n query(\n query: string,\n parameters: Record<string, SQLTypeMarker | null | undefined> | undefined,\n formatParameters: { disposition: string; format: string },\n signal?: AbortSignal,\n ): Promise<\n | {\n attachment?: string;\n data?: Record<string, unknown>[];\n external_links?: sql.ExternalLink[];\n columnNames?: string[];\n statement_id?: string;\n status?: unknown;\n refreshChunkLink?: RefreshChunkLink;\n }\n | undefined\n >;\n}\n\n/** Streams already-resolved EXTERNAL_LINKS chunks; the connector provides it. */\nexport interface ArrowChunkStreamer {\n streamExternalLinks(\n chunks: sql.ExternalLink[],\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown>;\n}\n\n/** The arrow delivery mode a warehouse actually supports. */\nexport type ArrowCapability = \"inline\" | \"external\";\n\n/** Optional per-warehouse capability memo, avoiding a doomed probe per query. */\ninterface ArrowDeliveryOptions {\n /**\n * When `\"external\"`, skip the `INLINE+ARROW_STREAM` attempt and go straight\n * to `EXTERNAL_LINKS` — a standard warehouse rejects INLINE arrow on every\n * query, so once we've learned that, the probe is pure waste. `\"inline\"` or\n * undefined attempts INLINE first (the common/Reyden path).\n */\n capabilityHint?: ArrowCapability;\n /**\n * Called once the working delivery mode is known, so the caller can memoize\n * it per warehouse for subsequent queries.\n */\n onCapabilityResolved?: (capability: ArrowCapability) => void;\n}\n\n/**\n * How a warehouse rejected a disposition/format combination.\n *\n * Only the two structured error codes Databricks emits for capability\n * mismatches (`INVALID_PARAMETER_VALUE`, `NOT_IMPLEMENTED`) gate a fallback —\n * auth, permission, and SQL errors never match, so they propagate untouched.\n * Message matching only *disambiguates* which capability is missing once the\n * code has confirmed it's a capability error.\n */\ntype DispositionRejection =\n | \"needs-arrow-inline\" // INLINE requires ARROW_STREAM\n | \"needs-json-inline\" // INLINE requires JSON_ARRAY\n | \"external-links-unsupported\" // EXTERNAL_LINKS not implemented\n | null;\n\n/**\n * Whether an error is a disposition/format *capability* rejection — the only\n * class of error a fallback should react to. Gated on the two structured codes\n * Databricks emits for capability mismatches (`INVALID_PARAMETER_VALUE`,\n * `NOT_IMPLEMENTED`); auth, permission, and SQL errors carry other codes and\n * never match. This is deliberately message-independent: a Databricks wording\n * change must not turn a capability rejection into a hard 500.\n */\nfunction isCapabilityRejection(err: unknown): boolean {\n const structuredCode =\n err instanceof ExecutionError ? err.errorCode : undefined;\n if (\n structuredCode === \"INVALID_PARAMETER_VALUE\" ||\n structuredCode === \"NOT_IMPLEMENTED\"\n ) {\n return true;\n }\n const lower = (\n err instanceof Error ? err.message : String(err)\n ).toLowerCase();\n return (\n lower.includes(\"invalid_parameter_value\") ||\n lower.includes(\"not_implemented\")\n );\n}\n\nexport function classifyDispositionRejection(\n err: unknown,\n): DispositionRejection {\n // Auth / permission / SQL errors carry other codes → never fall back.\n if (!isCapabilityRejection(err)) return null;\n\n const msg = err instanceof Error ? err.message : String(err);\n const lower = msg.toLowerCase();\n\n // EXTERNAL_LINKS disposition not implemented (Reyden).\n if (\n /external[_\\s]?links/.test(lower) &&\n (lower.includes(\"not yet implemented\") ||\n lower.includes(\"not implemented\") ||\n lower.includes(\"not supported\"))\n ) {\n return \"external-links-unsupported\";\n }\n\n // Remaining cases are INLINE disposition/format mismatches.\n if (!lower.includes(\"inline\")) return null;\n\n if (\n /only supports\\s+arrow_stream/i.test(msg) ||\n /must be\\s+arrow_stream/i.test(msg)\n ) {\n return \"needs-arrow-inline\";\n }\n if (\n /only supports\\s+json_array/i.test(msg) ||\n /must be\\s+json_array/i.test(msg) ||\n /arrow_stream\\s+(is\\s+|was\\s+)?not\\s+supported/i.test(msg)\n ) {\n return \"needs-json-inline\";\n }\n\n return null;\n}\n\n/** Structured error: the warehouse supports no Arrow delivery mode at all. */\nexport function arrowDeliveryUnsupported(): ExecutionError {\n return ExecutionError.statementFailed(\n \"Warehouse supports neither ARROW_STREAM+INLINE nor ARROW_STREAM+EXTERNAL_LINKS\",\n \"ARROW_DELIVERY_UNSUPPORTED\",\n 'This warehouse cannot return Arrow results. Re-run the query with format=\"JSON_ARRAY\".',\n );\n}\n\nconst errMessage = (err: unknown): string =>\n err instanceof Error ? err.message : String(err);\n\n/**\n * Produce the raw Arrow IPC bytes for an ARROW_STREAM query, applying the\n * capability fallback:\n *\n * 1. `INLINE + ARROW_STREAM` — on success the base64 attachment is decoded\n * once and yielded as a `Uint8Array` view (no Arrow parsing, no stash).\n * 2. On a `needs-json-inline` rejection, `EXTERNAL_LINKS + ARROW_STREAM` —\n * the pre-signed chunks resolved in the executor's own context are\n * streamed one at a time.\n * 3. If EXTERNAL_LINKS is also unsupported, a structured\n * `ARROW_DELIVERY_UNSUPPORTED` error is thrown.\n *\n * `out` is populated with the real column names and statement id before the\n * first chunk is yielded, so the caller can emit them (e.g. as a header)\n * ahead of the body.\n */\nexport async function* deliverArrowBytes(\n executor: QueryExecutor,\n streamer: ArrowChunkStreamer,\n query: string,\n processedParams: Record<string, SQLTypeMarker | null | undefined> | undefined,\n out: { columnNames?: string[]; statementId?: string },\n signal?: AbortSignal,\n opts?: ArrowDeliveryOptions,\n): AsyncGenerator<Uint8Array, void, unknown> {\n // Skip the INLINE probe when a prior query already learned this warehouse\n // only serves arrow via EXTERNAL_LINKS (standard warehouses reject INLINE\n // arrow on every query, so probing each time is pure waste).\n if (opts?.capabilityHint !== \"external\") {\n try {\n const result = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"ARROW_STREAM\" },\n signal,\n );\n if (result?.attachment) {\n opts?.onCapabilityResolved?.(\"inline\");\n out.columnNames = result.columnNames;\n out.statementId = result.statement_id;\n // Decode base64 once; yield a view over the Buffer (no copy, no parse).\n const decoded = Buffer.from(result.attachment, \"base64\");\n yield new Uint8Array(\n decoded.buffer,\n decoded.byteOffset,\n decoded.byteLength,\n );\n return;\n }\n // INLINE succeeded but returned no attachment (rare: inline data_array\n // under ARROW_STREAM). Fall through to EXTERNAL_LINKS.\n logger.warn(\n \"ARROW_STREAM INLINE returned no attachment; falling back to EXTERNAL_LINKS\",\n );\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n // Any capability-coded rejection of INLINE+ARROW_STREAM means this\n // warehouse won't serve arrow inline — try EXTERNAL_LINKS. We\n // intentionally do NOT require the message to match a specific phrase\n // (\"must be JSON_ARRAY\", …): the errorCode gate already excludes auth/SQL\n // errors, and relying on exact wording would turn a Databricks message\n // reword into a hard 500 on every standard warehouse. Worst case for a\n // genuinely bad parameter that happens to carry a capability code: one\n // wasted re-execution that fails identically on EXTERNAL_LINKS and\n // propagates.\n if (!isCapabilityRejection(err)) throw err;\n logger.warn(\n \"ARROW_STREAM INLINE rejected by warehouse, falling back to EXTERNAL_LINKS: %s\",\n errMessage(err),\n );\n }\n }\n\n let ext: Awaited<ReturnType<QueryExecutor[\"query\"]>>;\n try {\n ext = await executor.query(\n query,\n processedParams,\n { disposition: \"EXTERNAL_LINKS\", format: \"ARROW_STREAM\" },\n signal,\n );\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n // Neither INLINE nor EXTERNAL_LINKS Arrow is supported — surface a clear,\n // actionable error instead of a raw warehouse rejection.\n if (classifyDispositionRejection(err) === \"external-links-unsupported\") {\n throw arrowDeliveryUnsupported();\n }\n throw err;\n }\n\n if (!ext?.external_links) {\n throw ExecutionError.missingData(\"external_links\");\n }\n opts?.onCapabilityResolved?.(\"external\");\n out.columnNames = ext.columnNames;\n out.statementId = ext.statement_id;\n // Stream the pre-signed links resolved in THIS request's execution context\n // (user creds for `.obo.sql`, service principal otherwise). Pre-signed URLs\n // need no auth to download, so there is no second `getStatement` under a\n // mismatched identity. `refreshChunkLink` (also bound to this context) lets\n // the streamer re-mint a link that expires mid-download.\n yield* streamer.streamExternalLinks(\n ext.external_links,\n signal,\n ext.refreshChunkLink,\n );\n}\n\n/** JSON row result plus the metadata the SSE `result` message forwards. */\ninterface JsonResult {\n data: Record<string, unknown>[] | undefined;\n status: unknown;\n statement_id?: string;\n}\n\n/**\n * Produce JSON rows for a JSON_ARRAY query, applying the capability fallback:\n *\n * 1. `INLINE + JSON_ARRAY` — the native, common path.\n * 2. On a `needs-arrow-inline` rejection (warehouse only accepts ARROW_STREAM\n * for INLINE, e.g. Reyden), retry `INLINE + ARROW_STREAM` and decode the\n * attachment to plain rows server-side so the caller's JSON contract holds.\n *\n * EXTERNAL_LINKS is never used for the JSON fallback.\n */\nexport async function deliverJsonResult(\n executor: QueryExecutor,\n query: string,\n processedParams: Record<string, SQLTypeMarker | null | undefined> | undefined,\n signal?: AbortSignal,\n): Promise<JsonResult> {\n try {\n const result = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"JSON_ARRAY\" },\n signal,\n );\n return {\n data: result?.data,\n status: result?.status,\n statement_id: result?.statement_id,\n };\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n if (classifyDispositionRejection(err) !== \"needs-arrow-inline\") throw err;\n logger.warn(\n \"JSON_ARRAY INLINE rejected by warehouse, retrying as ARROW_STREAM INLINE and decoding server-side: %s\",\n errMessage(err),\n );\n }\n\n const arrowResult = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"ARROW_STREAM\" },\n signal,\n );\n if (!arrowResult?.attachment) {\n throw ExecutionError.missingData(\"ARROW_STREAM attachment\");\n }\n const rows = decodeArrowAttachmentToRows(\n arrowResult.attachment,\n arrowResult.columnNames,\n );\n return {\n data: rows,\n status: arrowResult.status,\n statement_id: arrowResult.statement_id,\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Server-side Arrow → JSON row materializer (JSON fallback only)\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Hard caps on the server-side JSON_ARRAY fallback. The materializer builds\n * every row as a plain JS object on the Node main thread (O(rows × cols)\n * allocations), so a runaway result blocks the event loop and pressures GC.\n * Cap on rows AND decoded bytes — either dimension can blow up independently.\n */\nconst JSON_ARRAY_FALLBACK_MAX_ROWS = 100_000;\nconst JSON_ARRAY_FALLBACK_MAX_BYTES = 64 * 1024 * 1024;\n\n/** Render an apache-arrow Decimal cell to a fixed-point string. */\nfunction formatDecimalCell(\n value: { toString(): string },\n scale: number,\n): string {\n const unscaled = value.toString();\n if (scale <= 0) return unscaled;\n const negative = unscaled.startsWith(\"-\");\n let digits = negative ? unscaled.slice(1) : unscaled;\n if (digits.length <= scale) digits = digits.padStart(scale + 1, \"0\");\n const point = digits.length - scale;\n const out = `${digits.slice(0, point)}.${digits.slice(point)}`;\n return negative ? `-${out}` : out;\n}\n\n/**\n * Render an apache-arrow Timestamp cell to ISO-8601 ms precision — `Z` for\n * zoned columns, no `Z` for TIMESTAMP_NTZ — matching native JSON_ARRAY.\n */\nfunction formatTimestampCell(epochMs: number, hasTimezone: boolean): string {\n const iso = new Date(epochMs).toISOString();\n return hasTimezone ? iso : iso.slice(0, -1);\n}\n\n/** Render an apache-arrow Date cell to `yyyy-MM-dd` (UTC). */\nfunction formatDateCell(epochMs: number): string {\n return new Date(epochMs).toISOString().slice(0, 10);\n}\n\n/**\n * Render a single scalar Arrow value to the string form the warehouse emits\n * under native JSON_ARRAY. Used for both top-level cells and (recursively, via\n * {@link renderNestedValue}) scalar leaves inside List/Struct/Map columns,\n * keyed on the Arrow field type so a nested `DECIMAL` keeps its scale and a\n * nested `TIMESTAMP` becomes ISO-8601 instead of raw epoch-ms.\n */\nfunction formatScalarCell(value: unknown, type: DataType): string {\n switch (type.typeId) {\n case Type.Decimal:\n return formatDecimalCell(\n value as { toString(): string },\n (type as DataType & { scale: number }).scale,\n );\n case Type.Timestamp:\n return formatTimestampCell(\n Number(value),\n (type as DataType & { timezone?: string | null }).timezone != null,\n );\n case Type.Date:\n return formatDateCell(Number(value));\n default:\n if (value instanceof Uint8Array) {\n return Buffer.from(value).toString(\"base64\");\n }\n if (value instanceof Date) return value.toISOString();\n return String(value);\n }\n}\n\n/**\n * Recursively render a nested Arrow value (List / Struct / Map, or a scalar\n * leaf) to the plain JS shape the warehouse nests inside a JSON_ARRAY cell:\n * structs and maps become objects, lists become arrays, and every scalar leaf\n * is stringified per {@link formatScalarCell}. The caller `JSON.stringify`s the\n * result so nested columns match the warehouse's native `data_array` exactly.\n */\nfunction renderNestedValue(value: unknown, type: DataType): unknown {\n if (value == null) return null;\n switch (type.typeId) {\n case Type.List:\n case Type.FixedSizeList: {\n const itemType = (type as DataType & { children: Field[] }).children[0]\n .type;\n const out: unknown[] = [];\n for (const item of value as Iterable<unknown>) {\n out.push(renderNestedValue(item, itemType));\n }\n return out;\n }\n case Type.Struct: {\n const fields = (type as DataType & { children: Field[] }).children;\n const obj: Record<string, unknown> = {};\n for (const field of fields) {\n obj[field.name] = renderNestedValue(\n (value as Record<string, unknown>)[field.name],\n field.type,\n );\n }\n return obj;\n }\n case Type.Map: {\n // A Map is a List<Struct<key, value>>; the value child carries the type.\n const entriesType = (type as DataType & { children: Field[] }).children[0]\n .type;\n const valueType = (entriesType as DataType & { children: Field[] })\n .children[1].type;\n const obj: Record<string, unknown> = {};\n for (const [k, v] of value as Iterable<[unknown, unknown]>) {\n obj[String(k)] = renderNestedValue(v, valueType);\n }\n return obj;\n }\n default:\n return formatScalarCell(value, type);\n }\n}\n\n/** Parse a STRING cell that looks like JSON into an object/array. */\nfunction maybeParseJsonString(value: string): unknown {\n if (value && (value[0] === \"{\" || value[0] === \"[\")) {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n return value;\n}\n\n/**\n * Decode a base64 Arrow IPC attachment to plain row objects, matching what the\n * warehouse emits natively under JSON_ARRAY so callers cannot tell which path\n * served the query.\n *\n * The output key uses the manifest name (`columnNames[i]`) when available —\n * Databricks encodes the Arrow schema positionally (col_0, …) — while the\n * vector is still fetched by the Arrow field's own name.\n */\nexport function decodeArrowAttachmentToRows(\n attachment: string,\n columnNames?: string[],\n): Record<string, unknown>[] {\n const decoded = Buffer.from(attachment, \"base64\");\n if (decoded.byteLength > JSON_ARRAY_FALLBACK_MAX_BYTES) {\n throw ExecutionError.statementFailed(\n `Result attachment is ${decoded.byteLength} bytes; JSON_ARRAY fallback materializer caps at ${JSON_ARRAY_FALLBACK_MAX_BYTES} bytes. Re-issue the query with format=\"ARROW_STREAM\" to stream the full result.`,\n \"RESULT_TOO_LARGE_FOR_JSON_FALLBACK\",\n \"Result too large for JSON format. Re-run with ARROW_STREAM format.\",\n );\n }\n const table = tableFromIPC(\n new Uint8Array(decoded.buffer, decoded.byteOffset, decoded.byteLength),\n );\n if (table.numRows > JSON_ARRAY_FALLBACK_MAX_ROWS) {\n throw ExecutionError.statementFailed(\n `Result has ${table.numRows} rows; JSON_ARRAY fallback materializer caps at ${JSON_ARRAY_FALLBACK_MAX_ROWS}. Re-issue the query with format=\"ARROW_STREAM\" to stream the full result.`,\n \"RESULT_TOO_LARGE_FOR_JSON_FALLBACK\",\n `Result too large for JSON format (over ${JSON_ARRAY_FALLBACK_MAX_ROWS} rows). Re-run with ARROW_STREAM format.`,\n );\n }\n // Resolve child vectors once (getChild walks fields on every call). The\n // output key uses the manifest name; the vector is fetched by field name.\n const columns = table.schema.fields.map((f, i) => {\n const outName =\n columnNames?.[i] && columnNames[i].length > 0 ? columnNames[i] : f.name;\n return [outName, table.getChild(f.name), f.type] as const;\n });\n const rows: Record<string, unknown>[] = [];\n for (let i = 0; i < table.numRows; i++) {\n const row: Record<string, unknown> = {};\n for (const [name, col, type] of columns) {\n const v = col?.get(i);\n if (v == null) {\n row[name] = null;\n continue;\n }\n switch (type.typeId) {\n case Type.Decimal:\n case Type.Timestamp:\n case Type.Date:\n row[name] = formatScalarCell(v, type);\n continue;\n // Nested columns (STRUCT / ARRAY / MAP) arrive on the native\n // JSON_ARRAY wire as a JSON string with every scalar leaf stringified\n // by type; render the same shape and stringify so callers cannot tell\n // the fallback path from the native one.\n case Type.List:\n case Type.FixedSizeList:\n case Type.Struct:\n case Type.Map:\n row[name] = JSON.stringify(renderNestedValue(v, type));\n continue;\n }\n if (\n typeof v === \"number\" ||\n typeof v === \"bigint\" ||\n typeof v === \"boolean\"\n ) {\n row[name] = String(v);\n } else if (typeof v === \"string\") {\n row[name] = maybeParseJsonString(v);\n } else if (v instanceof Uint8Array) {\n row[name] = Buffer.from(v).toString(\"base64\");\n } else {\n row[name] = formatScalarCell(v, type);\n }\n }\n rows.push(row);\n }\n return rows;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"result-delivery.js","names":[],"sources":["../../../src/plugins/analytics/result-delivery.ts"],"sourcesContent":["import { type DataType, type Field, Type, tableFromIPC } from \"apache-arrow\";\nimport type { SQLTypeMarker } from \"shared\";\nimport { ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { RefreshChunkLink } from \"../../stream/arrow-stream-processor\";\nimport type { sql } from \"../../workspace-client\";\n\n/**\n * Centralized disposition/format fallback for analytics result delivery.\n *\n * Two warehouse capability profiles must both work, and their supported\n * combinations are mutually exclusive per warehouse:\n *\n * - **Reyden**: `ARROW_STREAM + INLINE` (and `JSON_ARRAY + INLINE`); does NOT\n * support `EXTERNAL_LINKS`.\n * - **Normal SQL warehouse**: `JSON_ARRAY + INLINE` and\n * `ARROW_STREAM + EXTERNAL_LINKS`; rejects `ARROW_STREAM + INLINE`.\n *\n * The fallback logic lives here (rather than inline in the plugin route) so it\n * can be unit-tested against a fake executor without HTTP, and so the brittle\n * \"which rejection is this\" decision is expressed once.\n */\n\nconst logger = createLogger(\"analytics:delivery\");\n\n/**\n * Minimal query surface the fallback needs. The analytics plugin (and its\n * `asUser(req)` proxy) satisfy it, so the same delivery logic runs unchanged\n * under service-principal and on-behalf-of identities.\n */\nexport interface QueryExecutor {\n query(\n query: string,\n parameters: Record<string, SQLTypeMarker | null | undefined> | undefined,\n formatParameters: { disposition: string; format: string },\n signal?: AbortSignal,\n ): Promise<\n | {\n attachment?: string;\n data?: Record<string, unknown>[];\n external_links?: sql.ExternalLink[];\n columnNames?: string[];\n statement_id?: string;\n status?: unknown;\n refreshChunkLink?: RefreshChunkLink;\n }\n | undefined\n >;\n}\n\n/** Streams already-resolved EXTERNAL_LINKS chunks; the connector provides it. */\nexport interface ArrowChunkStreamer {\n streamExternalLinks(\n chunks: sql.ExternalLink[],\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown>;\n}\n\n/** The arrow delivery mode a warehouse actually supports. */\nexport type ArrowCapability = \"inline\" | \"external\";\n\n/** Optional per-warehouse capability memo, avoiding a doomed probe per query. */\ninterface ArrowDeliveryOptions {\n /**\n * When `\"external\"`, skip the `INLINE+ARROW_STREAM` attempt and go straight\n * to `EXTERNAL_LINKS` — a standard warehouse rejects INLINE arrow on every\n * query, so once we've learned that, the probe is pure waste. `\"inline\"` or\n * undefined attempts INLINE first (the common/Reyden path).\n */\n capabilityHint?: ArrowCapability;\n /**\n * Called once the working delivery mode is known, so the caller can memoize\n * it per warehouse for subsequent queries.\n */\n onCapabilityResolved?: (capability: ArrowCapability) => void;\n}\n\n/**\n * How a warehouse rejected a disposition/format combination.\n *\n * Only the two structured error codes Databricks emits for capability\n * mismatches (`INVALID_PARAMETER_VALUE`, `NOT_IMPLEMENTED`) gate a fallback —\n * auth, permission, and SQL errors never match, so they propagate untouched.\n * Message matching only *disambiguates* which capability is missing once the\n * code has confirmed it's a capability error.\n */\ntype DispositionRejection =\n | \"needs-arrow-inline\" // INLINE requires ARROW_STREAM\n | \"needs-json-inline\" // INLINE requires JSON_ARRAY\n | \"external-links-unsupported\" // EXTERNAL_LINKS not implemented\n | null;\n\n/**\n * Whether an error is a disposition/format *capability* rejection — the only\n * class of error a fallback should react to. Gated on the two structured codes\n * Databricks emits for capability mismatches (`INVALID_PARAMETER_VALUE`,\n * `NOT_IMPLEMENTED`); auth, permission, and SQL errors carry other codes and\n * never match. This is deliberately message-independent: a Databricks wording\n * change must not turn a capability rejection into a hard 500.\n */\nfunction isCapabilityRejection(err: unknown): boolean {\n const structuredCode =\n err instanceof ExecutionError ? err.errorCode : undefined;\n if (\n structuredCode === \"INVALID_PARAMETER_VALUE\" ||\n structuredCode === \"NOT_IMPLEMENTED\"\n ) {\n return true;\n }\n const lower = (\n err instanceof Error ? err.message : String(err)\n ).toLowerCase();\n return (\n lower.includes(\"invalid_parameter_value\") ||\n lower.includes(\"not_implemented\")\n );\n}\n\nexport function classifyDispositionRejection(\n err: unknown,\n): DispositionRejection {\n // Auth / permission / SQL errors carry other codes → never fall back.\n if (!isCapabilityRejection(err)) return null;\n\n const msg = err instanceof Error ? err.message : String(err);\n const lower = msg.toLowerCase();\n\n // EXTERNAL_LINKS disposition not implemented (Reyden).\n if (\n /external[_\\s]?links/.test(lower) &&\n (lower.includes(\"not yet implemented\") ||\n lower.includes(\"not implemented\") ||\n lower.includes(\"not supported\"))\n ) {\n return \"external-links-unsupported\";\n }\n\n // Remaining cases are INLINE disposition/format mismatches.\n if (!lower.includes(\"inline\")) return null;\n\n if (\n /only supports\\s+arrow_stream/i.test(msg) ||\n /must be\\s+arrow_stream/i.test(msg)\n ) {\n return \"needs-arrow-inline\";\n }\n if (\n /only supports\\s+json_array/i.test(msg) ||\n /must be\\s+json_array/i.test(msg) ||\n /arrow_stream\\s+(is\\s+|was\\s+)?not\\s+supported/i.test(msg)\n ) {\n return \"needs-json-inline\";\n }\n\n return null;\n}\n\n/** Structured error: the warehouse supports no Arrow delivery mode at all. */\nexport function arrowDeliveryUnsupported(): ExecutionError {\n return ExecutionError.statementFailed(\n \"Warehouse supports neither ARROW_STREAM+INLINE nor ARROW_STREAM+EXTERNAL_LINKS\",\n \"ARROW_DELIVERY_UNSUPPORTED\",\n 'This warehouse cannot return Arrow results. Re-run the query with format=\"JSON_ARRAY\".',\n );\n}\n\nconst errMessage = (err: unknown): string =>\n err instanceof Error ? err.message : String(err);\n\n/**\n * Produce the raw Arrow IPC bytes for an ARROW_STREAM query, applying the\n * capability fallback:\n *\n * 1. `INLINE + ARROW_STREAM` — on success the base64 attachment is decoded\n * once and yielded as a `Uint8Array` view (no Arrow parsing, no stash).\n * 2. On a `needs-json-inline` rejection, `EXTERNAL_LINKS + ARROW_STREAM` —\n * the pre-signed chunks resolved in the executor's own context are\n * streamed one at a time.\n * 3. If EXTERNAL_LINKS is also unsupported, a structured\n * `ARROW_DELIVERY_UNSUPPORTED` error is thrown.\n *\n * `out` is populated with the real column names and statement id before the\n * first chunk is yielded, so the caller can emit them (e.g. as a header)\n * ahead of the body.\n */\nexport async function* deliverArrowBytes(\n executor: QueryExecutor,\n streamer: ArrowChunkStreamer,\n query: string,\n processedParams: Record<string, SQLTypeMarker | null | undefined> | undefined,\n out: { columnNames?: string[]; statementId?: string },\n signal?: AbortSignal,\n opts?: ArrowDeliveryOptions,\n): AsyncGenerator<Uint8Array, void, unknown> {\n // Skip the INLINE probe when a prior query already learned this warehouse\n // only serves arrow via EXTERNAL_LINKS (standard warehouses reject INLINE\n // arrow on every query, so probing each time is pure waste).\n if (opts?.capabilityHint !== \"external\") {\n try {\n const result = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"ARROW_STREAM\" },\n signal,\n );\n if (result?.attachment) {\n opts?.onCapabilityResolved?.(\"inline\");\n out.columnNames = result.columnNames;\n out.statementId = result.statement_id;\n // Decode base64 once; yield a view over the Buffer (no copy, no parse).\n const decoded = Buffer.from(result.attachment, \"base64\");\n yield new Uint8Array(\n decoded.buffer,\n decoded.byteOffset,\n decoded.byteLength,\n );\n return;\n }\n // INLINE succeeded but returned no attachment (rare: inline data_array\n // under ARROW_STREAM). Fall through to EXTERNAL_LINKS.\n logger.warn(\n \"ARROW_STREAM INLINE returned no attachment; falling back to EXTERNAL_LINKS\",\n );\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n // Any capability-coded rejection of INLINE+ARROW_STREAM means this\n // warehouse won't serve arrow inline — try EXTERNAL_LINKS. We\n // intentionally do NOT require the message to match a specific phrase\n // (\"must be JSON_ARRAY\", …): the errorCode gate already excludes auth/SQL\n // errors, and relying on exact wording would turn a Databricks message\n // reword into a hard 500 on every standard warehouse. Worst case for a\n // genuinely bad parameter that happens to carry a capability code: one\n // wasted re-execution that fails identically on EXTERNAL_LINKS and\n // propagates.\n if (!isCapabilityRejection(err)) throw err;\n logger.warn(\n \"ARROW_STREAM INLINE rejected by warehouse, falling back to EXTERNAL_LINKS: %s\",\n errMessage(err),\n );\n }\n }\n\n let ext: Awaited<ReturnType<QueryExecutor[\"query\"]>>;\n try {\n ext = await executor.query(\n query,\n processedParams,\n { disposition: \"EXTERNAL_LINKS\", format: \"ARROW_STREAM\" },\n signal,\n );\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n // Neither INLINE nor EXTERNAL_LINKS Arrow is supported — surface a clear,\n // actionable error instead of a raw warehouse rejection.\n if (classifyDispositionRejection(err) === \"external-links-unsupported\") {\n throw arrowDeliveryUnsupported();\n }\n throw err;\n }\n\n if (!ext?.external_links) {\n throw ExecutionError.missingData(\"external_links\");\n }\n opts?.onCapabilityResolved?.(\"external\");\n out.columnNames = ext.columnNames;\n out.statementId = ext.statement_id;\n // Stream the pre-signed links resolved in THIS request's execution context\n // (user creds for `.obo.sql`, service principal otherwise). Pre-signed URLs\n // need no auth to download, so there is no second `getStatement` under a\n // mismatched identity. `refreshChunkLink` (also bound to this context) lets\n // the streamer re-mint a link that expires mid-download.\n yield* streamer.streamExternalLinks(\n ext.external_links,\n signal,\n ext.refreshChunkLink,\n );\n}\n\n/** JSON row result plus the metadata the SSE `result` message forwards. */\ninterface JsonResult {\n data: Record<string, unknown>[] | undefined;\n status: unknown;\n statement_id?: string;\n}\n\n/**\n * Produce JSON rows for a JSON_ARRAY query, applying the capability fallback:\n *\n * 1. `INLINE + JSON_ARRAY` — the native, common path.\n * 2. On a `needs-arrow-inline` rejection (warehouse only accepts ARROW_STREAM\n * for INLINE, e.g. Reyden), retry `INLINE + ARROW_STREAM` and decode the\n * attachment to plain rows server-side so the caller's JSON contract holds.\n *\n * EXTERNAL_LINKS is never used for the JSON fallback.\n */\nexport async function deliverJsonResult(\n executor: QueryExecutor,\n query: string,\n processedParams: Record<string, SQLTypeMarker | null | undefined> | undefined,\n signal?: AbortSignal,\n): Promise<JsonResult> {\n try {\n const result = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"JSON_ARRAY\" },\n signal,\n );\n return {\n data: result?.data,\n status: result?.status,\n statement_id: result?.statement_id,\n };\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n if (classifyDispositionRejection(err) !== \"needs-arrow-inline\") throw err;\n logger.warn(\n \"JSON_ARRAY INLINE rejected by warehouse, retrying as ARROW_STREAM INLINE and decoding server-side: %s\",\n errMessage(err),\n );\n }\n\n const arrowResult = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"ARROW_STREAM\" },\n signal,\n );\n if (!arrowResult?.attachment) {\n throw ExecutionError.missingData(\"ARROW_STREAM attachment\");\n }\n const rows = decodeArrowAttachmentToRows(\n arrowResult.attachment,\n arrowResult.columnNames,\n );\n return {\n data: rows,\n status: arrowResult.status,\n statement_id: arrowResult.statement_id,\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Server-side Arrow → JSON row materializer (JSON fallback only)\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Hard caps on the server-side JSON_ARRAY fallback. The materializer builds\n * every row as a plain JS object on the Node main thread (O(rows × cols)\n * allocations), so a runaway result blocks the event loop and pressures GC.\n * Cap on rows AND decoded bytes — either dimension can blow up independently.\n */\nconst JSON_ARRAY_FALLBACK_MAX_ROWS = 100_000;\nconst JSON_ARRAY_FALLBACK_MAX_BYTES = 64 * 1024 * 1024;\n\n/** Render an apache-arrow Decimal cell to a fixed-point string. */\nfunction formatDecimalCell(\n value: { toString(): string },\n scale: number,\n): string {\n const unscaled = value.toString();\n if (scale <= 0) return unscaled;\n const negative = unscaled.startsWith(\"-\");\n let digits = negative ? unscaled.slice(1) : unscaled;\n if (digits.length <= scale) digits = digits.padStart(scale + 1, \"0\");\n const point = digits.length - scale;\n const out = `${digits.slice(0, point)}.${digits.slice(point)}`;\n return negative ? `-${out}` : out;\n}\n\n/**\n * Render an apache-arrow Timestamp cell to ISO-8601 ms precision — `Z` for\n * zoned columns, no `Z` for TIMESTAMP_NTZ — matching native JSON_ARRAY.\n */\nfunction formatTimestampCell(epochMs: number, hasTimezone: boolean): string {\n const iso = new Date(epochMs).toISOString();\n return hasTimezone ? iso : iso.slice(0, -1);\n}\n\n/** Render an apache-arrow Date cell to `yyyy-MM-dd` (UTC). */\nfunction formatDateCell(epochMs: number): string {\n return new Date(epochMs).toISOString().slice(0, 10);\n}\n\n/**\n * Render a single scalar Arrow value to the string form the warehouse emits\n * under native JSON_ARRAY. Used for both top-level cells and (recursively, via\n * {@link renderNestedValue}) scalar leaves inside List/Struct/Map columns,\n * keyed on the Arrow field type so a nested `DECIMAL` keeps its scale and a\n * nested `TIMESTAMP` becomes ISO-8601 instead of raw epoch-ms.\n */\nfunction formatScalarCell(value: unknown, type: DataType): string {\n switch (type.typeId) {\n case Type.Decimal:\n return formatDecimalCell(\n value as { toString(): string },\n (type as DataType & { scale: number }).scale,\n );\n case Type.Timestamp:\n return formatTimestampCell(\n Number(value),\n (type as DataType & { timezone?: string | null }).timezone != null,\n );\n case Type.Date:\n return formatDateCell(Number(value));\n default:\n if (value instanceof Uint8Array) {\n return Buffer.from(value).toString(\"base64\");\n }\n if (value instanceof Date) return value.toISOString();\n return String(value);\n }\n}\n\n/**\n * Recursively render a nested Arrow value (List / Struct / Map, or a scalar\n * leaf) to the plain JS shape the warehouse nests inside a JSON_ARRAY cell:\n * structs and maps become objects, lists become arrays, and every scalar leaf\n * is stringified per {@link formatScalarCell}. The caller `JSON.stringify`s the\n * result so nested columns match the warehouse's native `data_array` exactly.\n */\nfunction renderNestedValue(value: unknown, type: DataType): unknown {\n if (value == null) return null;\n switch (type.typeId) {\n case Type.List:\n case Type.FixedSizeList: {\n const itemType = (type as DataType & { children: Field[] }).children[0]\n .type;\n const out: unknown[] = [];\n for (const item of value as Iterable<unknown>) {\n out.push(renderNestedValue(item, itemType));\n }\n return out;\n }\n case Type.Struct: {\n const fields = (type as DataType & { children: Field[] }).children;\n const obj: Record<string, unknown> = {};\n for (const field of fields) {\n obj[field.name] = renderNestedValue(\n (value as Record<string, unknown>)[field.name],\n field.type,\n );\n }\n return obj;\n }\n case Type.Map: {\n // A Map is a List<Struct<key, value>>; the value child carries the type.\n const entriesType = (type as DataType & { children: Field[] }).children[0]\n .type;\n const valueType = (entriesType as DataType & { children: Field[] })\n .children[1].type;\n const obj: Record<string, unknown> = {};\n for (const [k, v] of value as Iterable<[unknown, unknown]>) {\n obj[String(k)] = renderNestedValue(v, valueType);\n }\n return obj;\n }\n default:\n return formatScalarCell(value, type);\n }\n}\n\n/** Parse a STRING cell that looks like JSON into an object/array. */\nfunction maybeParseJsonString(value: string): unknown {\n if (value && (value[0] === \"{\" || value[0] === \"[\")) {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n return value;\n}\n\n/**\n * Decode a base64 Arrow IPC attachment to plain row objects, matching what the\n * warehouse emits natively under JSON_ARRAY so callers cannot tell which path\n * served the query.\n *\n * The output key uses the manifest name (`columnNames[i]`) when available —\n * Databricks encodes the Arrow schema positionally (col_0, …) — while the\n * vector is still fetched by the Arrow field's own name.\n */\nexport function decodeArrowAttachmentToRows(\n attachment: string,\n columnNames?: string[],\n): Record<string, unknown>[] {\n const decoded = Buffer.from(attachment, \"base64\");\n if (decoded.byteLength > JSON_ARRAY_FALLBACK_MAX_BYTES) {\n throw ExecutionError.statementFailed(\n `Result attachment is ${decoded.byteLength} bytes; JSON_ARRAY fallback materializer caps at ${JSON_ARRAY_FALLBACK_MAX_BYTES} bytes. Re-issue the query with format=\"ARROW_STREAM\" to stream the full result.`,\n \"RESULT_TOO_LARGE_FOR_JSON_FALLBACK\",\n \"Result too large for JSON format. Re-run with ARROW_STREAM format.\",\n );\n }\n const table = tableFromIPC(\n new Uint8Array(decoded.buffer, decoded.byteOffset, decoded.byteLength),\n );\n if (table.numRows > JSON_ARRAY_FALLBACK_MAX_ROWS) {\n throw ExecutionError.statementFailed(\n `Result has ${table.numRows} rows; JSON_ARRAY fallback materializer caps at ${JSON_ARRAY_FALLBACK_MAX_ROWS}. Re-issue the query with format=\"ARROW_STREAM\" to stream the full result.`,\n \"RESULT_TOO_LARGE_FOR_JSON_FALLBACK\",\n `Result too large for JSON format (over ${JSON_ARRAY_FALLBACK_MAX_ROWS} rows). Re-run with ARROW_STREAM format.`,\n );\n }\n // Resolve child vectors once (getChild walks fields on every call). The\n // output key uses the manifest name; the vector is fetched by field name.\n const columns = table.schema.fields.map((f, i) => {\n const outName =\n columnNames?.[i] && columnNames[i].length > 0 ? columnNames[i] : f.name;\n return [outName, table.getChild(f.name), f.type] as const;\n });\n const rows: Record<string, unknown>[] = [];\n for (let i = 0; i < table.numRows; i++) {\n const row: Record<string, unknown> = {};\n for (const [name, col, type] of columns) {\n const v = col?.get(i);\n if (v == null) {\n row[name] = null;\n continue;\n }\n switch (type.typeId) {\n case Type.Decimal:\n case Type.Timestamp:\n case Type.Date:\n row[name] = formatScalarCell(v, type);\n continue;\n // Nested columns (STRUCT / ARRAY / MAP) arrive on the native\n // JSON_ARRAY wire as a JSON string with every scalar leaf stringified\n // by type; render the same shape and stringify so callers cannot tell\n // the fallback path from the native one.\n case Type.List:\n case Type.FixedSizeList:\n case Type.Struct:\n case Type.Map:\n row[name] = JSON.stringify(renderNestedValue(v, type));\n continue;\n }\n if (\n typeof v === \"number\" ||\n typeof v === \"bigint\" ||\n typeof v === \"boolean\"\n ) {\n row[name] = String(v);\n } else if (typeof v === \"string\") {\n row[name] = maybeParseJsonString(v);\n } else if (v instanceof Uint8Array) {\n row[name] = Buffer.from(v).toString(\"base64\");\n } else {\n row[name] = formatScalarCell(v, type);\n }\n }\n rows.push(row);\n }\n return rows;\n}\n"],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
2
  import { AuthenticationError } from "../../errors/authentication.js";
3
3
  import "../../errors/index.js";
4
+ import { ApiError } from "../../workspace-client/errors.js";
5
+ import "../../workspace-client/index.js";
4
6
  import { ServiceContext } from "../../context/service-context.js";
5
7
  import { isUserContext } from "../../context/user-context.js";
6
8
  import { getCurrentUserId, getExecutionContext, getWorkspaceClient, runInUserContext } from "../../context/execution-context.js";
@@ -20,7 +22,6 @@ import { FILES_DOWNLOAD_DEFAULTS, FILES_MAX_UPLOAD_SIZE, FILES_READ_DEFAULTS, FI
20
22
  import { parentDirectory, sanitizeFilename } from "./helpers.js";
21
23
  import manifest_default from "./manifest.js";
22
24
  import { z } from "zod";
23
- import { ApiError } from "@databricks/sdk-experimental";
24
25
  import { STATUS_CODES } from "node:http";
25
26
  import { Readable } from "node:stream";
26
27
 
@@ -1308,8 +1309,8 @@ var FilesPlugin = class FilesPlugin extends Plugin {
1308
1309
  /**
1309
1310
  * @internal
1310
1311
  */
1311
- const files$1 = Object.assign(toPlugin(FilesPlugin), { policy });
1312
+ const files = Object.assign(toPlugin(FilesPlugin), { policy });
1312
1313
 
1313
1314
  //#endregion
1314
- export { FilesPlugin, files$1 as files };
1315
+ export { FilesPlugin, files };
1315
1316
  //# sourceMappingURL=plugin.js.map