@databricks/appkit 0.43.1 → 0.45.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 (68) hide show
  1. package/CLAUDE.md +0 -1
  2. package/NOTICE.md +1 -0
  3. package/dist/appkit/package.js +1 -1
  4. package/dist/connectors/sql-warehouse/arrow-schema.js +283 -0
  5. package/dist/connectors/sql-warehouse/arrow-schema.js.map +1 -0
  6. package/dist/connectors/sql-warehouse/client.js +166 -60
  7. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  8. package/dist/errors/base.d.ts +18 -0
  9. package/dist/errors/base.d.ts.map +1 -1
  10. package/dist/errors/base.js +20 -0
  11. package/dist/errors/base.js.map +1 -1
  12. package/dist/errors/execution.d.ts +34 -2
  13. package/dist/errors/execution.d.ts.map +1 -1
  14. package/dist/errors/execution.js +40 -5
  15. package/dist/errors/execution.js.map +1 -1
  16. package/dist/plugins/agents/agents.d.ts.map +1 -1
  17. package/dist/plugins/agents/agents.js.map +1 -1
  18. package/dist/plugins/analytics/analytics.d.ts +84 -8
  19. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  20. package/dist/plugins/analytics/analytics.js +296 -61
  21. package/dist/plugins/analytics/analytics.js.map +1 -1
  22. package/dist/plugins/analytics/index.js +1 -1
  23. package/dist/plugins/analytics/result-delivery.js +287 -0
  24. package/dist/plugins/analytics/result-delivery.js.map +1 -0
  25. package/dist/plugins/analytics/types.d.ts +8 -0
  26. package/dist/plugins/analytics/types.d.ts.map +1 -1
  27. package/dist/plugins/analytics/types.js.map +1 -1
  28. package/dist/plugins/files/plugin.js +1 -1
  29. package/dist/plugins/genie/genie.js +1 -1
  30. package/dist/plugins/jobs/types.d.ts +1 -1
  31. package/dist/plugins/lakebase/lakebase.js +1 -1
  32. package/dist/shared/src/index.d.ts +1 -0
  33. package/dist/shared/src/sse/analytics.d.ts +1 -0
  34. package/dist/shared/src/sse/analytics.js +40 -0
  35. package/dist/shared/src/sse/analytics.js.map +1 -0
  36. package/dist/stream/arrow-stream-processor.js +93 -117
  37. package/dist/stream/arrow-stream-processor.js.map +1 -1
  38. package/dist/stream/defaults.js +1 -1
  39. package/dist/stream/defaults.js.map +1 -1
  40. package/dist/stream/sse-writer.js +3 -2
  41. package/dist/stream/sse-writer.js.map +1 -1
  42. package/dist/stream/stream-manager.d.ts.map +1 -1
  43. package/dist/stream/stream-manager.js +15 -9
  44. package/dist/stream/stream-manager.js.map +1 -1
  45. package/dist/stream/types.js.map +1 -1
  46. package/dist/type-generator/query-registry.js +32 -2
  47. package/dist/type-generator/query-registry.js.map +1 -1
  48. package/docs/api/appkit/Class.AppKitError.md +40 -8
  49. package/docs/api/appkit/Class.AuthenticationError.md +56 -16
  50. package/docs/api/appkit/Class.ConfigurationError.md +57 -17
  51. package/docs/api/appkit/Class.ConnectionError.md +57 -17
  52. package/docs/api/appkit/Class.ExecutionError.md +80 -22
  53. package/docs/api/appkit/Class.InitializationError.md +55 -15
  54. package/docs/api/appkit/Class.ServerError.md +55 -15
  55. package/docs/api/appkit/Class.TunnelError.md +56 -16
  56. package/docs/api/appkit/Class.ValidationError.md +55 -15
  57. package/docs/api/appkit-ui/ui/ContextMenu.md +2 -2
  58. package/docs/api/appkit-ui/ui/Drawer.md +0 -56
  59. package/docs/api/appkit-ui/ui/DropdownMenu.md +3 -3
  60. package/docs/api/appkit-ui/ui/HoverCard.md +2 -2
  61. package/docs/api/appkit-ui/ui/Menubar.md +3 -3
  62. package/docs/api/appkit-ui/ui/Popover.md +2 -2
  63. package/docs/api/appkit-ui/ui/Select.md +2 -2
  64. package/docs/api/appkit-ui/ui/Tooltip.md +2 -2
  65. package/llms.txt +0 -1
  66. package/package.json +2 -2
  67. package/sbom.cdx.json +1 -1
  68. package/docs/api/appkit-ui/ui/ChartContainer.md +0 -343
@@ -1,4 +1,6 @@
1
+ import { makeResultMessage } from "../../shared/src/sse/analytics.js";
1
2
  import { createLogger } from "../../logging/logger.js";
3
+ import { AppKitError } from "../../errors/base.js";
2
4
  import { ExecutionError } from "../../errors/execution.js";
3
5
  import "../../errors/index.js";
4
6
  import { getWarehouseId, getWorkspaceClient } from "../../context/execution-context.js";
@@ -14,6 +16,7 @@ import { assertReadOnlySql } from "../../core/agent/tools/sql-policy.js";
14
16
  import { queryDefaults } from "./defaults.js";
15
17
  import manifest_default from "./manifest.js";
16
18
  import { QueryProcessor } from "./query.js";
19
+ import { deliverArrowBytes, deliverJsonResult } from "./result-delivery.js";
17
20
  import { normalizeAnalyticsFormat } from "./types.js";
18
21
  import { z } from "zod";
19
22
 
@@ -62,6 +65,15 @@ var AnalyticsPlugin = class extends Plugin {
62
65
  static description = "Analytics plugin for data analysis";
63
66
  SQLClient;
64
67
  queryProcessor;
68
+ /**
69
+ * In-process memo of which arrow delivery mode each warehouse supports
70
+ * (keyed by warehouse id). A standard warehouse rejects `INLINE+ARROW_STREAM`
71
+ * on every query, so once learned we skip that doomed probe; Reyden stays
72
+ * `"inline"`. Capability is a property of the warehouse, not the user, so it
73
+ * is not user-scoped. Bounded by the number of distinct warehouses a process
74
+ * talks to (effectively one).
75
+ */
76
+ _arrowCapability = /* @__PURE__ */ new Map();
65
77
  constructor(config) {
66
78
  super(config);
67
79
  this.config = config;
@@ -72,14 +84,6 @@ var AnalyticsPlugin = class extends Plugin {
72
84
  });
73
85
  }
74
86
  injectRoutes(router) {
75
- this.route(router, {
76
- name: "arrow",
77
- method: "get",
78
- path: "/arrow-result/:jobId",
79
- handler: async (req, res) => {
80
- await this._handleArrowRoute(req, res);
81
- }
82
- });
83
87
  this.route(router, {
84
88
  name: "query",
85
89
  method: "post",
@@ -88,41 +92,69 @@ var AnalyticsPlugin = class extends Plugin {
88
92
  await this._handleQueryRoute(req, res);
89
93
  }
90
94
  });
95
+ this.route(router, {
96
+ name: "arrow-columns",
97
+ method: "get",
98
+ path: "/columns/:statementId",
99
+ handler: async (req, res) => {
100
+ await this._handleColumnsRoute(req, res);
101
+ }
102
+ });
91
103
  }
92
104
  /**
93
- * Handle Arrow data download requests.
94
- * When called via asUser(req), uses the user's Databricks credentials.
105
+ * Column-names fallback endpoint. Re-derives the real column names from the
106
+ * statement's result manifest (stateless — no server cache), for the client
107
+ * to relabel a positional Arrow schema when the names were too large for the
108
+ * response header.
95
109
  */
96
- async _handleArrowRoute(req, res) {
97
- try {
98
- const { jobId } = req.params;
99
- const workspaceClient = getWorkspaceClient();
100
- logger.debug("Processing Arrow job request for jobId=%s", jobId);
101
- logger.event(req)?.setComponent("analytics", "getArrowData").setContext("analytics", {
102
- job_id: jobId,
103
- plugin: this.name
104
- });
105
- const result = await this.getArrowData(workspaceClient, jobId);
106
- res.setHeader("Content-Type", "application/octet-stream");
107
- res.setHeader("Content-Length", result.data.length.toString());
108
- res.setHeader("Cache-Control", "public, max-age=3600");
109
- logger.debug("Sending Arrow buffer: %d bytes for job %s", result.data.length, jobId);
110
- res.send(Buffer.from(result.data));
110
+ async _handleColumnsRoute(req, res) {
111
+ const { statementId } = req.params;
112
+ const columns = await this._resolveColumnNames(req, statementId);
113
+ if (columns && columns.length > 0) {
114
+ res.setHeader("Cache-Control", "no-store");
115
+ res.json({ columns });
116
+ return;
117
+ }
118
+ res.status(404).json({
119
+ error: "Column names unavailable",
120
+ plugin: this.name
121
+ });
122
+ }
123
+ /**
124
+ * Resolve a statement's real column names, trying the user's identity first
125
+ * (required for `.obo.sql` statements, which the service principal cannot
126
+ * `getStatement`) then falling back to the service principal (for
127
+ * SP-executed statements). Returns undefined if neither identity can read it,
128
+ * so the client falls back to the raw positional Arrow schema names.
129
+ */
130
+ async _resolveColumnNames(req, statementId) {
131
+ const attempts = [() => this.asUser(req)._getColumnNames(statementId), () => this._getColumnNames(statementId)];
132
+ for (const attempt of attempts) try {
133
+ const columns = await attempt();
134
+ if (columns && columns.length > 0) return columns;
111
135
  } catch (error) {
112
- logger.error("Arrow job error: %O", error);
113
- res.status(404).json({
114
- error: error instanceof Error ? error.message : "Arrow job not found",
115
- plugin: this.name
116
- });
136
+ logger.debug("Arrow column-names lookup attempt failed for %s: %O", statementId, error);
117
137
  }
118
138
  }
119
139
  /**
140
+ * Fetch column names in the current execution context. Proxied by `asUser`,
141
+ * so `getWorkspaceClient()` resolves to the user's client when invoked via
142
+ * `this.asUser(req)` and the service principal's otherwise.
143
+ */
144
+ async _getColumnNames(statementId) {
145
+ return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);
146
+ }
147
+ /**
120
148
  * Handle SQL query execution requests.
121
149
  * When called via asUser(req), uses the user's Databricks credentials.
122
150
  */
123
151
  async _handleQueryRoute(req, res) {
124
152
  const { query_key } = req.params;
125
153
  const { parameters, format: rawFormat = "JSON_ARRAY" } = req.body;
154
+ if (rawFormat !== "JSON_ARRAY" && rawFormat !== "ARROW_STREAM" && rawFormat !== "JSON" && rawFormat !== "ARROW") {
155
+ res.status(400).json({ error: `Invalid format: ${String(rawFormat)}. Expected "JSON_ARRAY" or "ARROW_STREAM".` });
156
+ return;
157
+ }
126
158
  const format = normalizeAnalyticsFormat(rawFormat);
127
159
  logger.debug(req, "Executing query: %s (format=%s)", query_key, format);
128
160
  logger.event(req)?.setComponent("analytics", "executeQuery").setContext("analytics", {
@@ -141,29 +173,27 @@ var AnalyticsPlugin = class extends Plugin {
141
173
  return;
142
174
  }
143
175
  const { query, isAsUser } = queryResult;
176
+ if (format === "ARROW_STREAM") {
177
+ await this._handleArrowStreamQuery(req, res, query_key, query, isAsUser, parameters);
178
+ return;
179
+ }
144
180
  const executor = isAsUser ? this.asUser(req) : this;
145
181
  const executorKey = isAsUser ? this.resolveUserId(req) : "global";
146
- const queryParameters = format === "ARROW_STREAM" ? {
147
- formatParameters: {
148
- disposition: "EXTERNAL_LINKS",
149
- format: "ARROW_STREAM"
150
- },
151
- type: "arrow"
152
- } : { type: "result" };
153
182
  const hashedQuery = this.queryProcessor.hashQuery(query);
183
+ const cacheConfig = {
184
+ ...queryDefaults.cache,
185
+ cacheKey: [
186
+ "analytics:query",
187
+ query_key,
188
+ JSON.stringify(parameters),
189
+ format,
190
+ hashedQuery,
191
+ executorKey
192
+ ]
193
+ };
154
194
  const sqlConfig = {
155
195
  ...queryDefaults,
156
- cache: {
157
- ...queryDefaults.cache,
158
- cacheKey: [
159
- "analytics:query",
160
- query_key,
161
- JSON.stringify(parameters),
162
- JSON.stringify(format),
163
- hashedQuery,
164
- executorKey
165
- ]
166
- }
196
+ cache: cacheConfig
167
197
  };
168
198
  const streamExecutionSettings = { default: {
169
199
  cache: { enabled: false },
@@ -188,18 +218,21 @@ var AnalyticsPlugin = class extends Plugin {
188
218
  elapsedMs: update.elapsedMs
189
219
  }
190
220
  };
221
+ let originalError;
191
222
  const sqlResult = await executor.execute(async (sig) => {
192
- const processedParams = await self.queryProcessor.processQueryParams(query, parameters);
193
- const result = await executor.query(query, processedParams, queryParameters.formatParameters, sig);
194
- return {
195
- type: queryParameters.type,
196
- ...result
197
- };
223
+ try {
224
+ const processedParams = await self.queryProcessor.processQueryParams(query, parameters);
225
+ return await self._executeJsonArrayPath(executor, query, processedParams, sig);
226
+ } catch (err) {
227
+ originalError = err;
228
+ throw err;
229
+ }
198
230
  }, { default: sqlConfig }, executorKey);
199
231
  if (!sqlResult.ok) {
200
232
  const msg = sqlResult.message;
201
233
  const lower = msg.toLowerCase();
202
234
  if (lower.includes("operation was aborted") || lower.includes("the request was aborted") || lower.includes("statement was canceled")) throw new DOMException(lower.includes("canceled") ? msg : "The operation was aborted.", "AbortError");
235
+ if (originalError instanceof AppKitError) throw originalError;
203
236
  const inner = msg.startsWith("Statement failed: ") ? msg.slice(18) : msg;
204
237
  throw ExecutionError.statementFailed(inner);
205
238
  }
@@ -207,6 +240,169 @@ var AnalyticsPlugin = class extends Plugin {
207
240
  }, streamExecutionSettings, executorKey);
208
241
  }
209
242
  /**
243
+ * JSON_ARRAY SSE path. Delegates the disposition/format fallback to
244
+ * {@link deliverJsonResult} (INLINE JSON_ARRAY → on `needs-arrow-inline`,
245
+ * INLINE ARROW_STREAM decoded to rows) and wraps the rows in a `result`
246
+ * message. External links are never used for the JSON fallback.
247
+ */
248
+ async _executeJsonArrayPath(executor, query, processedParams, signal) {
249
+ const result = await deliverJsonResult(executor, query, processedParams, signal);
250
+ return makeResultMessage(result.data, {
251
+ status: result.status,
252
+ statement_id: result.statement_id
253
+ });
254
+ }
255
+ /**
256
+ * Attach the real column names so the client can relabel the positional
257
+ * Arrow schema (Databricks encodes ARROW_STREAM columns as col_0, …).
258
+ *
259
+ * Small schemas ride `X-Appkit-Arrow-Columns` directly. A very wide schema
260
+ * whose URL-encoded names would blow the HTTP header size limit instead
261
+ * advertises the statement id in `X-Appkit-Arrow-Columns-Ref`, and the
262
+ * client fetches the names from `GET /columns/:statementId`.
263
+ */
264
+ _setArrowColumnsHeader(res, columnsRef) {
265
+ const names = columnsRef.columnNames;
266
+ if (!names || names.length === 0) return;
267
+ const encoded = encodeURIComponent(JSON.stringify(names));
268
+ if (encoded.length <= MAX_ARROW_COLUMNS_HEADER_BYTES) {
269
+ res.setHeader("X-Appkit-Arrow-Columns", encoded);
270
+ return;
271
+ }
272
+ if (columnsRef.statementId) res.setHeader("X-Appkit-Arrow-Columns-Ref", columnsRef.statementId);
273
+ else logger.warn("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");
274
+ }
275
+ /**
276
+ * ARROW_STREAM query handler: stream the raw Arrow IPC bytes back as the
277
+ * HTTP response body — no SSE, no server-side stash, no second
278
+ * `/arrow-result` request.
279
+ *
280
+ * The first chunk is pulled before headers are sent so a failure still
281
+ * yields a clean JSON error; once bytes are in flight a mid-stream failure
282
+ * can only abort the socket. Warehouse readiness is awaited (no SSE
283
+ * progress on this path) — a no-op for a warm warehouse, a blocking wait
284
+ * on a cold start. Runs under the user's context for `.obo.sql` queries.
285
+ */
286
+ async _handleArrowStreamQuery(req, res, query_key, query, isAsUser, parameters) {
287
+ const executor = isAsUser ? this.asUser(req) : this;
288
+ const executorKey = isAsUser ? this.resolveUserId(req) : "global";
289
+ const abortController = new AbortController();
290
+ const onClose = () => abortController.abort();
291
+ res.on("close", onClose);
292
+ const signal = abortController.signal;
293
+ const firstByteTimeoutMs = this.config.arrowFirstByteTimeoutMs ?? DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS;
294
+ let timedOut = false;
295
+ const failFast = setTimeout(() => {
296
+ timedOut = true;
297
+ abortController.abort();
298
+ }, firstByteTimeoutMs);
299
+ try {
300
+ await executor._ensureArrowWarehouseReady(signal);
301
+ const processedParams = await this.queryProcessor.processQueryParams(query, parameters);
302
+ const warehouseId = await getWarehouseId();
303
+ const columnsRef = {};
304
+ const bytes = deliverArrowBytes(this._arrowCachingExecutor(executor, query_key, query, parameters, executorKey), this.SQLClient, query, processedParams, columnsRef, signal, {
305
+ capabilityHint: this._arrowCapability.get(warehouseId),
306
+ onCapabilityResolved: (capability) => this._arrowCapability.set(warehouseId, capability)
307
+ });
308
+ const first = await bytes.next();
309
+ clearTimeout(failFast);
310
+ res.setHeader("Content-Type", "application/vnd.apache.arrow.stream");
311
+ res.setHeader("Cache-Control", "no-store");
312
+ this._setArrowColumnsHeader(res, columnsRef);
313
+ if (!first.done) {
314
+ await writeChunk(res, first.value);
315
+ for await (const buf of bytes) await writeChunk(res, buf);
316
+ }
317
+ res.end();
318
+ } catch (error) {
319
+ clearTimeout(failFast);
320
+ if (timedOut) {
321
+ logger.warn("Arrow query timed out before first byte after %dms", firstByteTimeoutMs);
322
+ res.status(503).json({
323
+ error: "The SQL warehouse is starting or overloaded and did not respond in time. Please retry.",
324
+ errorCode: "WAREHOUSE_UNAVAILABLE",
325
+ plugin: this.name
326
+ });
327
+ return;
328
+ }
329
+ if (signal.aborted) {
330
+ if (res.headersSent) res.destroy();
331
+ else res.end();
332
+ return;
333
+ }
334
+ if (res.headersSent) {
335
+ logger.error("Arrow query stream failed mid-flight: %O", error);
336
+ res.destroy(error instanceof Error ? error : new Error(String(error)));
337
+ return;
338
+ }
339
+ logger.error("Arrow query error: %O", error);
340
+ const errorCode = error instanceof ExecutionError ? error.errorCode : void 0;
341
+ res.status(500).json({
342
+ error: error instanceof AppKitError ? error.clientMessage : "Unable to execute query",
343
+ errorCode,
344
+ plugin: this.name
345
+ });
346
+ } finally {
347
+ res.off("close", onClose);
348
+ }
349
+ }
350
+ /**
351
+ * Await SQL warehouse readiness for the direct-binary Arrow path. There is no
352
+ * SSE progress channel here — readiness is simply awaited, bounded by the
353
+ * caller's abort signal / fail-fast timeout. Invoked via the request executor
354
+ * (`asUser(req)` for `.obo.sql`) so `getWorkspaceClient()` resolves in the
355
+ * correct identity context rather than defaulting to the service principal.
356
+ */
357
+ async _ensureArrowWarehouseReady(signal) {
358
+ const workspaceClient = getWorkspaceClient();
359
+ const warehouseId = await getWarehouseId();
360
+ const startupTimeoutMs = this.config.warehouseStartupTimeoutMs ?? DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;
361
+ const autoStart = this.config.autoStartWarehouse ?? true;
362
+ await this.SQLClient.ensureWarehouseRunning(workspaceClient, warehouseId, {
363
+ signal,
364
+ timeoutMs: startupTimeoutMs,
365
+ autoStart,
366
+ onStatus: () => {}
367
+ });
368
+ }
369
+ /**
370
+ * Wrap an executor so the `INLINE + ARROW_STREAM` attempt is served from
371
+ * (and populates) the same per-user TTL cache the JSON path uses — otherwise
372
+ * every arrow chart render is a fresh warehouse execution, unlike its JSON
373
+ * twin. Caching is deliberately scoped to inline attachments:
374
+ *
375
+ * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the
376
+ * same lifecycle as cached JSON rows — safe to cache.
377
+ * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in
378
+ * minutes; caching them would serve dead links, so those pass through
379
+ * uncached (only the tiny link metadata would be cached anyway).
380
+ *
381
+ * Uses `this.cache.getOrExecute` directly rather than `this.execute()`
382
+ * because `execute()` reduces a thrown error to `{ ok:false, message }`,
383
+ * dropping the `errorCode` the capability fallback classifies on. The cache
384
+ * re-throws `AppKitError`s intact and never caches a rejection, so the
385
+ * INLINE→EXTERNAL_LINKS fallback still sees the structured rejection.
386
+ */
387
+ _arrowCachingExecutor(executor, query_key, query, parameters, executorKey) {
388
+ const hashedQuery = this.queryProcessor.hashQuery(query);
389
+ const cache = this.cache;
390
+ const ttl = queryDefaults.cache?.ttl;
391
+ return { query: (q, params, formatParameters, signal) => {
392
+ if (formatParameters.disposition !== "INLINE" || formatParameters.format !== "ARROW_STREAM") return executor.query(q, params, formatParameters, signal);
393
+ return cache.getOrExecute([
394
+ "analytics:query:arrow",
395
+ query_key,
396
+ JSON.stringify(parameters),
397
+ hashedQuery,
398
+ executorKey
399
+ ], (sharedSignal) => executor.query(q, params, formatParameters, sharedSignal ?? signal), executorKey, {
400
+ ttl,
401
+ callerSignal: signal
402
+ });
403
+ } };
404
+ }
405
+ /**
210
406
  * Execute a SQL query using the current execution context.
211
407
  *
212
408
  * When called directly: uses service principal credentials.
@@ -232,12 +428,6 @@ var AnalyticsPlugin = class extends Plugin {
232
428
  ...formatParameters
233
429
  }, signal)).result;
234
430
  }
235
- /**
236
- * Get Arrow-formatted data for a completed query job.
237
- */
238
- async getArrowData(workspaceClient, jobId, signal) {
239
- return await this.SQLClient.getArrowData(workspaceClient, jobId, signal);
240
- }
241
431
  async shutdown() {
242
432
  this.streamManager.abortAll();
243
433
  }
@@ -279,10 +469,55 @@ var AnalyticsPlugin = class extends Plugin {
279
469
  }
280
470
  };
281
471
  /**
472
+ * Write one chunk to the response honoring backpressure: if the socket
473
+ * buffer is full (`res.write` returns false), wait for `drain` before
474
+ * resolving so a slow client can't balloon Node's internal write queue and
475
+ * defeat the constant-memory goal of streaming.
476
+ *
477
+ * @internal exported for unit testing the backpressure/disconnect behavior.
478
+ */
479
+ function writeChunk(res, bytes) {
480
+ const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
481
+ if (res.destroyed || res.writableEnded) return Promise.reject(new DOMException("The response stream closed", "AbortError"));
482
+ if (res.write(buf)) return Promise.resolve();
483
+ return new Promise((resolve, reject) => {
484
+ const cleanup = () => {
485
+ res.off("drain", onDrain);
486
+ res.off("close", onClose);
487
+ res.off("error", onClose);
488
+ };
489
+ const onDrain = () => {
490
+ cleanup();
491
+ resolve();
492
+ };
493
+ const onClose = () => {
494
+ cleanup();
495
+ reject(new DOMException("The response stream closed", "AbortError"));
496
+ };
497
+ res.once("drain", onDrain);
498
+ res.once("close", onClose);
499
+ res.once("error", onClose);
500
+ });
501
+ }
502
+ /**
503
+ * Fail-fast ceiling on the wait for the first Arrow byte (warehouse
504
+ * readiness + execute + first chunk). Past this a stuck/overloaded warehouse
505
+ * yields a clear 503 instead of hanging. Override per plugin via
506
+ * `arrowFirstByteTimeoutMs`.
507
+ */
508
+ const DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 12e4;
509
+ /**
510
+ * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a
511
+ * very wide schema) the names are served via the `/columns/:statementId`
512
+ * fallback endpoint instead of the header. Kept well under the common ~8 KiB
513
+ * per-header limit.
514
+ */
515
+ const MAX_ARROW_COLUMNS_HEADER_BYTES = 6e3;
516
+ /**
282
517
  * @internal
283
518
  */
284
519
  const analytics = toPlugin(AnalyticsPlugin);
285
520
 
286
521
  //#endregion
287
- export { AnalyticsPlugin, analytics };
522
+ export { AnalyticsPlugin, analytics, writeChunk };
288
523
  //# sourceMappingURL=analytics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type {\n AgentToolDefinition,\n IAppRouter,\n PluginExecuteConfig,\n SQLTypeMarker,\n StreamExecutionSettings,\n 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 { 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 { QueryProcessor } from \"./query\";\nimport type {\n AnalyticsQueryResponse,\n AnalyticsStreamMessage,\n IAnalyticsConfig,\n IAnalyticsQueryRequest,\n WarehouseStatus,\n} from \"./types\";\nimport { normalizeAnalyticsFormat } 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 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 // Arrow data downloads always run as service principal and bypass the\n // interceptor chain (execute/executeStream). The original query execution\n // handles OBO via executeStream(); this endpoint fetches pre-computed\n // results by job ID.\n this.route(router, {\n name: \"arrow\",\n method: \"get\",\n path: \"/arrow-result/:jobId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleArrowRoute(req, res);\n },\n });\n\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\n /**\n * Handle Arrow data download requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleArrowRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n try {\n const { jobId } = req.params;\n const workspaceClient = getWorkspaceClient();\n\n logger.debug(\"Processing Arrow job request for jobId=%s\", jobId);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"getArrowData\").setContext(\"analytics\", {\n job_id: jobId,\n plugin: this.name,\n });\n\n const result = await this.getArrowData(workspaceClient, jobId);\n\n res.setHeader(\"Content-Type\", \"application/octet-stream\");\n res.setHeader(\"Content-Length\", result.data.length.toString());\n res.setHeader(\"Cache-Control\", \"public, max-age=3600\");\n\n logger.debug(\n \"Sending Arrow buffer: %d bytes for job %s\",\n result.data.length,\n jobId,\n );\n res.send(Buffer.from(result.data));\n } catch (error) {\n logger.error(\"Arrow job error: %O\", error);\n res.status(404).json({\n error: error instanceof Error ? error.message : \"Arrow job not found\",\n plugin: this.name,\n });\n }\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 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 // 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 queryParameters =\n format === \"ARROW_STREAM\"\n ? {\n formatParameters: {\n disposition: \"EXTERNAL_LINKS\",\n format: \"ARROW_STREAM\",\n },\n type: \"arrow\" as const,\n }\n : {\n type: \"result\" as const,\n };\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\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: {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n JSON.stringify(format),\n hashedQuery,\n executorKey,\n ],\n },\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 const sqlResult = await executor.execute(\n async (sig) => {\n const processedParams =\n await self.queryProcessor.processQueryParams(query, parameters);\n const result = await executor.query(\n query,\n processedParams,\n queryParameters.formatParameters,\n sig,\n );\n return { type: queryParameters.type, ...result };\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 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 * 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 /**\n * Get Arrow-formatted data for a completed query job.\n */\n protected async getArrowData(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<ReturnType<typeof this.SQLClient.getArrowData>> {\n return await this.SQLClient.getArrowData(workspaceClient, jobId, signal);\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 * @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 { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\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 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 // Column-names fallback for very wide Arrow schemas whose names don't fit\n // in the `X-Appkit-Arrow-Columns` response header (see\n // `_setArrowColumnsHeader`). The client hits this with the statement id\n // 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 * 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,4 +1,4 @@
1
1
  import { normalizeAnalyticsFormat } from "./types.js";
2
- import { AnalyticsPlugin, analytics } from "./analytics.js";
2
+ import { AnalyticsPlugin, analytics, writeChunk } from "./analytics.js";
3
3
 
4
4
  export { };