@databricks/appkit 0.44.0 → 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 (56) hide show
  1. package/dist/appkit/package.js +1 -1
  2. package/dist/connectors/sql-warehouse/arrow-schema.js +283 -0
  3. package/dist/connectors/sql-warehouse/arrow-schema.js.map +1 -0
  4. package/dist/connectors/sql-warehouse/client.js +166 -60
  5. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  6. package/dist/errors/base.d.ts +18 -0
  7. package/dist/errors/base.d.ts.map +1 -1
  8. package/dist/errors/base.js +20 -0
  9. package/dist/errors/base.js.map +1 -1
  10. package/dist/errors/execution.d.ts +34 -2
  11. package/dist/errors/execution.d.ts.map +1 -1
  12. package/dist/errors/execution.js +40 -5
  13. package/dist/errors/execution.js.map +1 -1
  14. package/dist/plugins/agents/agents.d.ts.map +1 -1
  15. package/dist/plugins/agents/agents.js.map +1 -1
  16. package/dist/plugins/analytics/analytics.d.ts +84 -8
  17. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  18. package/dist/plugins/analytics/analytics.js +296 -61
  19. package/dist/plugins/analytics/analytics.js.map +1 -1
  20. package/dist/plugins/analytics/index.js +1 -1
  21. package/dist/plugins/analytics/result-delivery.js +287 -0
  22. package/dist/plugins/analytics/result-delivery.js.map +1 -0
  23. package/dist/plugins/analytics/types.d.ts +8 -0
  24. package/dist/plugins/analytics/types.d.ts.map +1 -1
  25. package/dist/plugins/analytics/types.js.map +1 -1
  26. package/dist/plugins/files/plugin.js +1 -1
  27. package/dist/plugins/genie/genie.js +1 -1
  28. package/dist/plugins/jobs/types.d.ts +1 -1
  29. package/dist/plugins/lakebase/lakebase.js +1 -1
  30. package/dist/shared/src/index.d.ts +1 -0
  31. package/dist/shared/src/sse/analytics.d.ts +1 -0
  32. package/dist/shared/src/sse/analytics.js +40 -0
  33. package/dist/shared/src/sse/analytics.js.map +1 -0
  34. package/dist/stream/arrow-stream-processor.js +93 -117
  35. package/dist/stream/arrow-stream-processor.js.map +1 -1
  36. package/dist/stream/defaults.js +1 -1
  37. package/dist/stream/defaults.js.map +1 -1
  38. package/dist/stream/sse-writer.js +3 -2
  39. package/dist/stream/sse-writer.js.map +1 -1
  40. package/dist/stream/stream-manager.d.ts.map +1 -1
  41. package/dist/stream/stream-manager.js +15 -9
  42. package/dist/stream/stream-manager.js.map +1 -1
  43. package/dist/stream/types.js.map +1 -1
  44. package/dist/type-generator/query-registry.js +32 -2
  45. package/dist/type-generator/query-registry.js.map +1 -1
  46. package/docs/api/appkit/Class.AppKitError.md +40 -8
  47. package/docs/api/appkit/Class.AuthenticationError.md +56 -16
  48. package/docs/api/appkit/Class.ConfigurationError.md +57 -17
  49. package/docs/api/appkit/Class.ConnectionError.md +57 -17
  50. package/docs/api/appkit/Class.ExecutionError.md +80 -22
  51. package/docs/api/appkit/Class.InitializationError.md +55 -15
  52. package/docs/api/appkit/Class.ServerError.md +55 -15
  53. package/docs/api/appkit/Class.TunnelError.md +56 -16
  54. package/docs/api/appkit/Class.ValidationError.md +55 -15
  55. package/package.json +1 -1
  56. package/sbom.cdx.json +1 -1
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.44.0";
3
+ var version = "0.45.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -0,0 +1,283 @@
1
+ import { Binary, Bool, DateDay, Decimal, DurationMicrosecond, Field, Float32, Float64, Int16, Int32, Int64, Int8, IntervalYearMonth, List, Map_, Null, Schema, Struct, Table, TimestampMicrosecond, Utf8, tableToIPC } from "apache-arrow";
2
+
3
+ //#region src/connectors/sql-warehouse/arrow-schema.ts
4
+ /**
5
+ * Parse a Databricks SQL type text (the value returned by the Statement
6
+ * Execution API in `ColumnInfo.type_text`) into an Apache Arrow DataType.
7
+ *
8
+ * Supports:
9
+ * - All scalar types (STRING, INT, BIGINT, DECIMAL, TIMESTAMP, etc.)
10
+ * - Parameterized scalars: DECIMAL(p,s), VARCHAR(n), CHAR(n)
11
+ * - Nested types: ARRAY<T>, MAP<K,V>, STRUCT<f1:T1, f2:T2 COMMENT '...', ...>
12
+ * - INTERVAL year-month and day-time variants
13
+ * - Backtick-quoted struct field names with embedded `` `` `` escapes
14
+ *
15
+ * Unknown or unparseable types fall back to Utf8 — empty-Table consumers
16
+ * still see a column with the right name; only the inner type is degraded.
17
+ */
18
+ function parseDatabricksType(typeText) {
19
+ const parser = new TypeParser(typeText);
20
+ const result = parser.parseType();
21
+ parser.expectEnd();
22
+ return result;
23
+ }
24
+ /**
25
+ * Build an empty Arrow IPC stream (base64-encoded) matching the column schema
26
+ * returned by the warehouse. Used so ARROW_STREAM responses with no rows still
27
+ * deliver a real Arrow Table to the client, preserving the hook's typed
28
+ * contract.
29
+ */
30
+ function buildEmptyArrowIPCBase64(columns) {
31
+ const ipc = tableToIPC(new Table(new Schema(columns.map((col, index) => {
32
+ const typeText = col.type_text ?? col.type_name ?? "STRING";
33
+ let dataType;
34
+ try {
35
+ dataType = parseDatabricksType(typeText);
36
+ } catch {
37
+ dataType = new Utf8();
38
+ }
39
+ return new Field(col.name && col.name.length > 0 ? col.name : `column_${index}`, dataType, true);
40
+ }))), "stream");
41
+ return Buffer.from(ipc).toString("base64");
42
+ }
43
+ var TypeParser = class {
44
+ input;
45
+ pos = 0;
46
+ constructor(input) {
47
+ this.input = input;
48
+ }
49
+ parseType() {
50
+ this.skipWs();
51
+ let name;
52
+ if (this.peek() === "`") name = this.consumeBacktickIdent();
53
+ else name = this.consumeIdent();
54
+ const upper = name.toUpperCase();
55
+ this.skipWs();
56
+ if (upper === "INTERVAL") return this.parseInterval();
57
+ if (this.peek() === "(") {
58
+ this.consume("(");
59
+ const args = this.parseNumberArgs();
60
+ this.consume(")");
61
+ this.skipWs();
62
+ return this.makeParameterized(upper, args);
63
+ }
64
+ if (this.peek() === "<") {
65
+ this.consume("<");
66
+ const result = this.makeGeneric(upper);
67
+ this.skipWs();
68
+ this.consume(">");
69
+ return result;
70
+ }
71
+ return this.makeScalar(upper);
72
+ }
73
+ expectEnd() {
74
+ this.skipWs();
75
+ if (this.pos < this.input.length) throw new Error(`Unexpected trailing input at position ${this.pos}: "${this.input.slice(this.pos)}"`);
76
+ }
77
+ makeScalar(upper) {
78
+ switch (upper) {
79
+ case "STRING":
80
+ case "VARIANT": return new Utf8();
81
+ case "VARCHAR":
82
+ case "CHAR": return new Utf8();
83
+ case "BINARY":
84
+ case "GEOGRAPHY":
85
+ case "GEOMETRY": return new Binary();
86
+ case "BOOLEAN":
87
+ case "BOOL": return new Bool();
88
+ case "TINYINT":
89
+ case "BYTE": return new Int8();
90
+ case "SMALLINT":
91
+ case "SHORT": return new Int16();
92
+ case "INT":
93
+ case "INTEGER": return new Int32();
94
+ case "BIGINT":
95
+ case "LONG": return new Int64();
96
+ case "FLOAT":
97
+ case "REAL": return new Float32();
98
+ case "DOUBLE": return new Float64();
99
+ case "DECIMAL":
100
+ case "NUMERIC":
101
+ case "DEC": return new Decimal(0, 10, 128);
102
+ case "DATE": return new DateDay();
103
+ case "TIMESTAMP":
104
+ case "TIMESTAMP_LTZ": return new TimestampMicrosecond("UTC");
105
+ case "TIMESTAMP_NTZ": return new TimestampMicrosecond();
106
+ case "VOID":
107
+ case "NULL": return new Null();
108
+ default: return new Utf8();
109
+ }
110
+ }
111
+ makeParameterized(upper, args) {
112
+ switch (upper) {
113
+ case "DECIMAL":
114
+ case "NUMERIC":
115
+ case "DEC": {
116
+ const precision = args[0] ?? 10;
117
+ return new Decimal(args[1] ?? 0, precision, 128);
118
+ }
119
+ case "VARCHAR":
120
+ case "CHAR": return new Utf8();
121
+ default: return new Utf8();
122
+ }
123
+ }
124
+ makeGeneric(upper) {
125
+ switch (upper) {
126
+ case "ARRAY": return new List(new Field("item", this.parseType(), true));
127
+ case "MAP": {
128
+ const keyType = this.parseType();
129
+ this.skipWs();
130
+ this.consume(",");
131
+ this.skipWs();
132
+ const valueType = this.parseType();
133
+ return new Map_(new Field("entries", new Struct([new Field("key", keyType, false), new Field("value", valueType, true)]), false), false);
134
+ }
135
+ case "STRUCT": return this.parseStructFields();
136
+ default:
137
+ this.skipBalancedAngles();
138
+ return new Utf8();
139
+ }
140
+ }
141
+ parseStructFields() {
142
+ const fields = [];
143
+ while (true) {
144
+ this.skipWs();
145
+ if (this.peek() === ">") break;
146
+ let name;
147
+ if (this.peek() === "`") name = this.consumeBacktickIdent();
148
+ else name = this.consumeIdent();
149
+ this.skipWs();
150
+ this.consume(":");
151
+ this.skipWs();
152
+ const type = this.parseType();
153
+ this.skipWs();
154
+ while (this.peekKeyword("NOT")) {
155
+ this.consumeIdent();
156
+ this.skipWs();
157
+ if (this.peekKeyword("NULL")) this.consumeIdent();
158
+ this.skipWs();
159
+ }
160
+ if (this.peekKeyword("COMMENT")) {
161
+ this.consumeIdent();
162
+ this.skipWs();
163
+ this.consumeStringLiteral();
164
+ this.skipWs();
165
+ }
166
+ fields.push(new Field(name, type, true));
167
+ this.skipWs();
168
+ if (this.peek() === ",") this.consume(",");
169
+ else break;
170
+ }
171
+ return new Struct(fields);
172
+ }
173
+ parseInterval() {
174
+ const seen = [];
175
+ while (this.pos < this.input.length) {
176
+ this.skipWs();
177
+ const c = this.peek();
178
+ if (c === "" || c === "," || c === ">" || c === ")") break;
179
+ const word = this.consumeIdent().toUpperCase();
180
+ seen.push(word);
181
+ }
182
+ return seen.some((w) => w === "YEAR" || w === "MONTH") ? new IntervalYearMonth() : new DurationMicrosecond();
183
+ }
184
+ parseNumberArgs() {
185
+ const args = [];
186
+ while (true) {
187
+ this.skipWs();
188
+ if (this.peek() === ")") break;
189
+ args.push(this.consumeNumber());
190
+ this.skipWs();
191
+ if (this.peek() === ",") this.consume(",");
192
+ else break;
193
+ }
194
+ return args;
195
+ }
196
+ peek() {
197
+ return this.input[this.pos] ?? "";
198
+ }
199
+ peekKeyword(word) {
200
+ if (this.input.slice(this.pos, this.pos + word.length).toUpperCase() !== word.toUpperCase()) return false;
201
+ const next = this.input[this.pos + word.length] ?? "";
202
+ return !/[A-Za-z0-9_]/.test(next);
203
+ }
204
+ consume(expected) {
205
+ if (this.peek() !== expected) throw new Error(`Expected "${expected}" at position ${this.pos}, got "${this.peek()}" in "${this.input}"`);
206
+ this.pos++;
207
+ }
208
+ skipWs() {
209
+ while (this.pos < this.input.length && /\s/.test(this.input[this.pos] ?? "")) this.pos++;
210
+ }
211
+ consumeIdent() {
212
+ const start = this.pos;
213
+ while (this.pos < this.input.length && /[A-Za-z0-9_]/.test(this.input[this.pos] ?? "")) this.pos++;
214
+ if (this.pos === start) throw new Error(`Expected identifier at position ${this.pos}, got "${this.peek()}" in "${this.input}"`);
215
+ return this.input.slice(start, this.pos);
216
+ }
217
+ consumeBacktickIdent() {
218
+ this.consume("`");
219
+ let value = "";
220
+ while (this.pos < this.input.length) {
221
+ if (this.input[this.pos] === "`") {
222
+ if (this.input[this.pos + 1] === "`") {
223
+ value += "`";
224
+ this.pos += 2;
225
+ continue;
226
+ }
227
+ break;
228
+ }
229
+ value += this.input[this.pos];
230
+ this.pos++;
231
+ }
232
+ this.consume("`");
233
+ return value;
234
+ }
235
+ consumeNumber() {
236
+ const start = this.pos;
237
+ while (this.pos < this.input.length && /[0-9]/.test(this.input[this.pos] ?? "")) this.pos++;
238
+ if (this.pos === start) throw new Error(`Expected number at position ${this.pos}, got "${this.peek()}" in "${this.input}"`);
239
+ return Number.parseInt(this.input.slice(start, this.pos), 10);
240
+ }
241
+ consumeStringLiteral() {
242
+ const quote = this.peek();
243
+ if (quote !== "'" && quote !== "\"") throw new Error(`Expected string literal at position ${this.pos}, got "${quote}" in "${this.input}"`);
244
+ this.pos++;
245
+ let value = "";
246
+ while (this.pos < this.input.length) {
247
+ const c = this.input[this.pos];
248
+ if (c === "\\") {
249
+ const next = this.input[this.pos + 1];
250
+ if (next !== void 0) {
251
+ value += next;
252
+ this.pos += 2;
253
+ continue;
254
+ }
255
+ this.pos++;
256
+ continue;
257
+ }
258
+ if (c === quote) {
259
+ this.pos++;
260
+ return value;
261
+ }
262
+ value += c;
263
+ this.pos++;
264
+ }
265
+ throw new Error(`Unterminated string literal in "${this.input}"`);
266
+ }
267
+ skipBalancedAngles() {
268
+ let depth = 1;
269
+ while (this.pos < this.input.length && depth > 0) {
270
+ const c = this.peek();
271
+ if (c === "<") depth++;
272
+ else if (c === ">") {
273
+ depth--;
274
+ if (depth === 0) return;
275
+ }
276
+ this.pos++;
277
+ }
278
+ }
279
+ };
280
+
281
+ //#endregion
282
+ export { buildEmptyArrowIPCBase64 };
283
+ //# sourceMappingURL=arrow-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arrow-schema.js","names":[],"sources":["../../../src/connectors/sql-warehouse/arrow-schema.ts"],"sourcesContent":["import {\n Binary,\n Bool,\n type DataType,\n DateDay,\n Decimal,\n DurationMicrosecond,\n Field,\n Float32,\n Float64,\n Int8,\n Int16,\n Int32,\n Int64,\n IntervalYearMonth,\n List,\n Map_,\n Null,\n Schema,\n Struct,\n Table,\n TimestampMicrosecond,\n tableToIPC,\n Utf8,\n} from \"apache-arrow\";\n\n/**\n * Parse a Databricks SQL type text (the value returned by the Statement\n * Execution API in `ColumnInfo.type_text`) into an Apache Arrow DataType.\n *\n * Supports:\n * - All scalar types (STRING, INT, BIGINT, DECIMAL, TIMESTAMP, etc.)\n * - Parameterized scalars: DECIMAL(p,s), VARCHAR(n), CHAR(n)\n * - Nested types: ARRAY<T>, MAP<K,V>, STRUCT<f1:T1, f2:T2 COMMENT '...', ...>\n * - INTERVAL year-month and day-time variants\n * - Backtick-quoted struct field names with embedded `` `` `` escapes\n *\n * Unknown or unparseable types fall back to Utf8 — empty-Table consumers\n * still see a column with the right name; only the inner type is degraded.\n */\nexport function parseDatabricksType(typeText: string): DataType {\n const parser = new TypeParser(typeText);\n const result = parser.parseType();\n parser.expectEnd();\n return result;\n}\n\n/**\n * Build an empty Arrow IPC stream (base64-encoded) matching the column schema\n * returned by the warehouse. Used so ARROW_STREAM responses with no rows still\n * deliver a real Arrow Table to the client, preserving the hook's typed\n * contract.\n */\nexport function buildEmptyArrowIPCBase64(\n columns: Array<{\n name?: string;\n type_text?: string;\n type_name?: string;\n }>,\n): string {\n const fields = columns.map((col, index) => {\n const typeText = col.type_text ?? col.type_name ?? \"STRING\";\n let dataType: DataType;\n try {\n dataType = parseDatabricksType(typeText);\n } catch {\n dataType = new Utf8();\n }\n const name = col.name && col.name.length > 0 ? col.name : `column_${index}`;\n return new Field(name, dataType, true);\n });\n const schema = new Schema(fields);\n const table = new Table(schema);\n const ipc = tableToIPC(table, \"stream\");\n return Buffer.from(ipc).toString(\"base64\");\n}\n\n// ============================================================================\n// Recursive-descent parser\n// ============================================================================\n\nclass TypeParser {\n private readonly input: string;\n private pos = 0;\n\n constructor(input: string) {\n this.input = input;\n }\n\n parseType(): DataType {\n this.skipWs();\n\n let name: string;\n if (this.peek() === \"`\") {\n name = this.consumeBacktickIdent();\n } else {\n name = this.consumeIdent();\n }\n const upper = name.toUpperCase();\n\n this.skipWs();\n\n if (upper === \"INTERVAL\") {\n return this.parseInterval();\n }\n\n if (this.peek() === \"(\") {\n this.consume(\"(\");\n const args = this.parseNumberArgs();\n this.consume(\")\");\n this.skipWs();\n return this.makeParameterized(upper, args);\n }\n\n if (this.peek() === \"<\") {\n this.consume(\"<\");\n const result = this.makeGeneric(upper);\n this.skipWs();\n this.consume(\">\");\n return result;\n }\n\n return this.makeScalar(upper);\n }\n\n expectEnd(): void {\n this.skipWs();\n if (this.pos < this.input.length) {\n throw new Error(\n `Unexpected trailing input at position ${this.pos}: \"${this.input.slice(this.pos)}\"`,\n );\n }\n }\n\n // ─── Type constructors ───────────────────────────────────\n\n private makeScalar(upper: string): DataType {\n switch (upper) {\n case \"STRING\":\n case \"VARIANT\":\n return new Utf8();\n case \"VARCHAR\":\n case \"CHAR\":\n return new Utf8();\n case \"BINARY\":\n case \"GEOGRAPHY\":\n case \"GEOMETRY\":\n return new Binary();\n case \"BOOLEAN\":\n case \"BOOL\":\n return new Bool();\n case \"TINYINT\":\n case \"BYTE\":\n return new Int8();\n case \"SMALLINT\":\n case \"SHORT\":\n return new Int16();\n case \"INT\":\n case \"INTEGER\":\n return new Int32();\n case \"BIGINT\":\n case \"LONG\":\n return new Int64();\n case \"FLOAT\":\n case \"REAL\":\n return new Float32();\n case \"DOUBLE\":\n return new Float64();\n case \"DECIMAL\":\n case \"NUMERIC\":\n case \"DEC\":\n return new Decimal(0, 10, 128);\n case \"DATE\":\n return new DateDay();\n case \"TIMESTAMP\":\n case \"TIMESTAMP_LTZ\":\n return new TimestampMicrosecond(\"UTC\");\n case \"TIMESTAMP_NTZ\":\n return new TimestampMicrosecond();\n case \"VOID\":\n case \"NULL\":\n return new Null();\n default:\n return new Utf8();\n }\n }\n\n private makeParameterized(upper: string, args: number[]): DataType {\n switch (upper) {\n case \"DECIMAL\":\n case \"NUMERIC\":\n case \"DEC\": {\n const precision = args[0] ?? 10;\n const scale = args[1] ?? 0;\n // Arrow JS Decimal constructor signature is (scale, precision, bitWidth).\n return new Decimal(scale, precision, 128);\n }\n case \"VARCHAR\":\n case \"CHAR\":\n return new Utf8();\n default:\n return new Utf8();\n }\n }\n\n private makeGeneric(upper: string): DataType {\n switch (upper) {\n case \"ARRAY\": {\n const inner = this.parseType();\n return new List(new Field(\"item\", inner, true));\n }\n case \"MAP\": {\n const keyType = this.parseType();\n this.skipWs();\n this.consume(\",\");\n this.skipWs();\n const valueType = this.parseType();\n const entriesStruct = new Struct([\n new Field(\"key\", keyType, false),\n new Field(\"value\", valueType, true),\n ]);\n return new Map_(new Field(\"entries\", entriesStruct, false), false);\n }\n case \"STRUCT\":\n return this.parseStructFields();\n default:\n // Unknown generic — skip to matching '>' and fall back.\n this.skipBalancedAngles();\n return new Utf8();\n }\n }\n\n private parseStructFields(): DataType {\n const fields: Field[] = [];\n while (true) {\n this.skipWs();\n if (this.peek() === \">\") break;\n\n let name: string;\n if (this.peek() === \"`\") {\n name = this.consumeBacktickIdent();\n } else {\n name = this.consumeIdent();\n }\n\n this.skipWs();\n this.consume(\":\");\n this.skipWs();\n\n const type = this.parseType();\n\n // Optional `NOT NULL` and `COMMENT '...'`. Both are accepted by\n // Databricks DDL and may appear in `type_text`.\n this.skipWs();\n while (this.peekKeyword(\"NOT\")) {\n this.consumeIdent();\n this.skipWs();\n if (this.peekKeyword(\"NULL\")) {\n this.consumeIdent();\n }\n this.skipWs();\n }\n if (this.peekKeyword(\"COMMENT\")) {\n this.consumeIdent();\n this.skipWs();\n this.consumeStringLiteral();\n this.skipWs();\n }\n\n fields.push(new Field(name, type, true));\n\n this.skipWs();\n if (this.peek() === \",\") {\n this.consume(\",\");\n } else {\n break;\n }\n }\n return new Struct(fields);\n }\n\n private parseInterval(): DataType {\n // Grammar: INTERVAL <unit> [TO <unit>]\n // YEAR / MONTH variants -> IntervalYearMonth\n // DAY / HOUR / MINUTE / SECOND variants -> Duration(microsecond)\n const seen: string[] = [];\n while (this.pos < this.input.length) {\n this.skipWs();\n const c = this.peek();\n if (c === \"\" || c === \",\" || c === \">\" || c === \")\") break;\n const word = this.consumeIdent().toUpperCase();\n seen.push(word);\n }\n const isYearMonth = seen.some((w) => w === \"YEAR\" || w === \"MONTH\");\n return isYearMonth ? new IntervalYearMonth() : new DurationMicrosecond();\n }\n\n private parseNumberArgs(): number[] {\n const args: number[] = [];\n while (true) {\n this.skipWs();\n if (this.peek() === \")\") break;\n args.push(this.consumeNumber());\n this.skipWs();\n if (this.peek() === \",\") {\n this.consume(\",\");\n } else {\n break;\n }\n }\n return args;\n }\n\n // ─── Token utilities ─────────────────────────────────────\n\n private peek(): string {\n return this.input[this.pos] ?? \"\";\n }\n\n private peekKeyword(word: string): boolean {\n const slice = this.input.slice(this.pos, this.pos + word.length);\n if (slice.toUpperCase() !== word.toUpperCase()) return false;\n // Must be followed by a non-identifier character (boundary check).\n const next = this.input[this.pos + word.length] ?? \"\";\n return !/[A-Za-z0-9_]/.test(next);\n }\n\n private consume(expected: string): void {\n if (this.peek() !== expected) {\n throw new Error(\n `Expected \"${expected}\" at position ${this.pos}, got \"${this.peek()}\" in \"${this.input}\"`,\n );\n }\n this.pos++;\n }\n\n private skipWs(): void {\n while (\n this.pos < this.input.length &&\n /\\s/.test(this.input[this.pos] ?? \"\")\n ) {\n this.pos++;\n }\n }\n\n private consumeIdent(): string {\n const start = this.pos;\n while (\n this.pos < this.input.length &&\n /[A-Za-z0-9_]/.test(this.input[this.pos] ?? \"\")\n ) {\n this.pos++;\n }\n if (this.pos === start) {\n throw new Error(\n `Expected identifier at position ${this.pos}, got \"${this.peek()}\" in \"${this.input}\"`,\n );\n }\n return this.input.slice(start, this.pos);\n }\n\n private consumeBacktickIdent(): string {\n this.consume(\"`\");\n let value = \"\";\n while (this.pos < this.input.length) {\n if (this.input[this.pos] === \"`\") {\n if (this.input[this.pos + 1] === \"`\") {\n value += \"`\";\n this.pos += 2;\n continue;\n }\n break;\n }\n value += this.input[this.pos];\n this.pos++;\n }\n this.consume(\"`\");\n return value;\n }\n\n private consumeNumber(): number {\n const start = this.pos;\n while (\n this.pos < this.input.length &&\n /[0-9]/.test(this.input[this.pos] ?? \"\")\n ) {\n this.pos++;\n }\n if (this.pos === start) {\n throw new Error(\n `Expected number at position ${this.pos}, got \"${this.peek()}\" in \"${this.input}\"`,\n );\n }\n return Number.parseInt(this.input.slice(start, this.pos), 10);\n }\n\n private consumeStringLiteral(): string {\n const quote = this.peek();\n if (quote !== \"'\" && quote !== '\"') {\n throw new Error(\n `Expected string literal at position ${this.pos}, got \"${quote}\" in \"${this.input}\"`,\n );\n }\n this.pos++;\n let value = \"\";\n while (this.pos < this.input.length) {\n const c = this.input[this.pos];\n if (c === \"\\\\\") {\n // Escape sequence: keep the next char verbatim.\n const next = this.input[this.pos + 1];\n if (next !== undefined) {\n value += next;\n this.pos += 2;\n continue;\n }\n this.pos++;\n continue;\n }\n if (c === quote) {\n this.pos++;\n return value;\n }\n value += c;\n this.pos++;\n }\n throw new Error(`Unterminated string literal in \"${this.input}\"`);\n }\n\n private skipBalancedAngles(): void {\n let depth = 1;\n while (this.pos < this.input.length && depth > 0) {\n const c = this.peek();\n if (c === \"<\") depth++;\n else if (c === \">\") {\n depth--;\n if (depth === 0) return;\n }\n this.pos++;\n }\n }\n}\n"],"mappings":"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"}
@@ -8,6 +8,7 @@ import "../../errors/index.js";
8
8
  import { TelemetryManager } from "../../telemetry/telemetry-manager.js";
9
9
  import { SpanKind, SpanStatusCode } from "../../telemetry/index.js";
10
10
  import { ArrowStreamProcessor } from "../../stream/arrow-stream-processor.js";
11
+ import { buildEmptyArrowIPCBase64 } from "./arrow-schema.js";
11
12
  import { executeStatementDefaults } from "./defaults.js";
12
13
  import { WarehousePollBackoff } from "./warehouse-poll-backoff.js";
13
14
  import { WarehouseStatusEmitter } from "./warehouse-status-emitter.js";
@@ -16,6 +17,35 @@ import { Context } from "@databricks/sdk-experimental";
16
17
  //#region src/connectors/sql-warehouse/client.ts
17
18
  const logger = createLogger("connectors:sql-warehouse");
18
19
  /**
20
+ * Real column names from a statement's result manifest (positional
21
+ * `column_N` fallback for any blank ones). Databricks encodes ARROW_STREAM
22
+ * schemas positionally (`col_0`, …); these names let consumers relabel an
23
+ * Arrow result to match the JSON path. Returns `undefined` when the manifest
24
+ * carries no columns.
25
+ */
26
+ function arrowColumnNames(response) {
27
+ const cols = response.manifest?.schema?.columns;
28
+ if (!cols || cols.length === 0) return void 0;
29
+ return cols.map((c, i) => c.name && c.name.length > 0 ? c.name : `column_${i}`);
30
+ }
31
+ /**
32
+ * Maximum size for inline Arrow IPC attachments (25 MiB decoded — the
33
+ * Databricks Statement Execution API hard cap on INLINE responses).
34
+ *
35
+ * The analytics route streams the decoded attachment straight back on the
36
+ * query response body; this cap bounds the single in-memory copy. Larger
37
+ * results fall through to `disposition: "EXTERNAL_LINKS"`, which streams
38
+ * chunk-by-chunk and is not bound by this limit.
39
+ */
40
+ const MAX_INLINE_ATTACHMENT_BYTES = 25 * 1024 * 1024;
41
+ /**
42
+ * Safety cap on how many additional EXTERNAL_LINKS chunks
43
+ * {@link SQLWarehouseConnector._resolveAllExternalLinks} will follow when the
44
+ * manifest omits `total_chunk_count`. High enough to cover any real result;
45
+ * only bounds a misbehaving warehouse with a cyclic `next_chunk_index`.
46
+ */
47
+ const MAX_EXTERNAL_CHUNK_FOLLOWS = 1e4;
48
+ /**
19
49
  * Default ceiling for how long {@link SQLWarehouseConnector.ensureWarehouseRunning}
20
50
  * will wait for a warehouse to reach the RUNNING state before giving up.
21
51
  *
@@ -66,7 +96,6 @@ var SQLWarehouseConnector = class {
66
96
  get arrowProcessor() {
67
97
  if (!this._arrowProcessor) this._arrowProcessor = new ArrowStreamProcessor({
68
98
  timeout: this.config.timeout || executeStatementDefaults.timeout,
69
- maxConcurrentDownloads: ArrowStreamProcessor.DEFAULT_MAX_CONCURRENT_DOWNLOADS,
70
99
  retries: ArrowStreamProcessor.DEFAULT_RETRIES
71
100
  });
72
101
  return this._arrowProcessor;
@@ -135,9 +164,9 @@ var SQLWarehouseConnector = class {
135
164
  result = await this._pollForStatementResult(workspaceClient, statementId, this.config.timeout, signal);
136
165
  break;
137
166
  case "SUCCEEDED":
138
- result = this._transformDataArray(response);
167
+ result = await this._transformDataArray(response, workspaceClient, signal);
139
168
  break;
140
- case "FAILED": throw ExecutionError.statementFailed(status.error?.message);
169
+ case "FAILED": throw ExecutionError.statementFailed(status.error?.message, status.error?.error_code);
141
170
  case "CANCELED": throw ExecutionError.canceled();
142
171
  case "CLOSED": throw ExecutionError.resultsClosed();
143
172
  default: throw ExecutionError.unknownState(String(status?.state ?? "unknown"));
@@ -163,9 +192,7 @@ var SQLWarehouseConnector = class {
163
192
  });
164
193
  logger.error("Statement execution failed: %s", error instanceof Error ? error.message : String(error));
165
194
  }
166
- if (error instanceof Error && error.name === "AbortError") throw error;
167
- if (error instanceof AppKitError) throw error;
168
- throw ExecutionError.statementFailed(error instanceof Error ? error.message : String(error));
195
+ this._rethrowStatementError(error);
169
196
  } finally {
170
197
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
171
198
  const duration = Date.now() - startTime;
@@ -462,8 +489,8 @@ var SQLWarehouseConnector = class {
462
489
  "poll.duration_ms": elapsedTime
463
490
  });
464
491
  span.setStatus({ code: SpanStatusCode.OK });
465
- return this._transformDataArray(response);
466
- case "FAILED": throw ExecutionError.statementFailed(status.error?.message);
492
+ return this._transformDataArray(response, workspaceClient, signal);
493
+ case "FAILED": throw ExecutionError.statementFailed(status.error?.message, status.error?.error_code);
467
494
  case "CANCELED": throw ExecutionError.canceled();
468
495
  case "CLOSED": throw ExecutionError.resultsClosed();
469
496
  default: throw ExecutionError.unknownState(String(status?.state ?? "unknown"));
@@ -477,9 +504,7 @@ var SQLWarehouseConnector = class {
477
504
  code: SpanStatusCode.ERROR,
478
505
  message: error instanceof Error ? error.message : String(error)
479
506
  });
480
- if (error instanceof Error && error.name === "AbortError") throw error;
481
- if (error instanceof AppKitError) throw error;
482
- throw ExecutionError.statementFailed(error instanceof Error ? error.message : String(error));
507
+ this._rethrowStatementError(error);
483
508
  } finally {
484
509
  span.end();
485
510
  }
@@ -488,8 +513,22 @@ var SQLWarehouseConnector = class {
488
513
  includePrefix: true
489
514
  });
490
515
  }
491
- _transformDataArray(response) {
492
- if (response.manifest?.format === "ARROW_STREAM") return this.updateWithArrowStatus(response);
516
+ async _transformDataArray(response, workspaceClient, signal) {
517
+ if (response.manifest?.format === "ARROW_STREAM") {
518
+ const result = response.result;
519
+ if (result?.attachment) return this._validateArrowAttachment(response, result.attachment);
520
+ if (result?.external_links && result.external_links.length > 0) return this.updateWithArrowStatus(response, workspaceClient, signal);
521
+ if ((!result?.data_array || Array.isArray(result.data_array) && result.data_array.length === 0) && response.manifest?.schema?.columns) {
522
+ const synthesized = buildEmptyArrowIPCBase64(response.manifest.schema.columns);
523
+ return {
524
+ ...response,
525
+ result: {
526
+ ...result ?? {},
527
+ attachment: synthesized
528
+ }
529
+ };
530
+ }
531
+ }
493
532
  if (!response.result?.data_array || !response.manifest?.schema?.columns) return response;
494
533
  const columns = response.manifest.schema.columns;
495
534
  const transformedData = response.result.data_array.map((row) => {
@@ -515,59 +554,126 @@ var SQLWarehouseConnector = class {
515
554
  }
516
555
  };
517
556
  }
518
- updateWithArrowStatus(response) {
557
+ /**
558
+ * Validate a base64 Arrow IPC attachment (INLINE ARROW_STREAM) and attach
559
+ * the real column names from the result manifest.
560
+ *
561
+ * Databricks warehouses encode the Arrow schema positionally (`col_0`, …);
562
+ * the real, aliased names live only in `manifest.schema.columns`. Rather
563
+ * than decode/re-encode the payload server-side, we carry the names on the
564
+ * result (`columnNames`) so the route can hand them to the client in a
565
+ * response header and the client relabels the decoded Table — the one
566
+ * mechanism used for both INLINE and EXTERNAL_LINKS.
567
+ */
568
+ _validateArrowAttachment(response, attachment) {
569
+ const stripped = attachment.replace(/\s+/g, "");
570
+ const padding = stripped.endsWith("==") ? 2 : stripped.endsWith("=") ? 1 : 0;
571
+ const decodedSize = Math.floor(stripped.length * 3 / 4) - padding;
572
+ if (decodedSize > MAX_INLINE_ATTACHMENT_BYTES) throw ExecutionError.statementFailed(`Inline Arrow attachment exceeds maximum size (${decodedSize} > ${MAX_INLINE_ATTACHMENT_BYTES} bytes)`);
573
+ const columnNames = arrowColumnNames(response);
574
+ if (columnNames) return {
575
+ ...response,
576
+ result: {
577
+ ...response.result,
578
+ statement_id: response.statement_id,
579
+ columnNames
580
+ }
581
+ };
582
+ return response;
583
+ }
584
+ async updateWithArrowStatus(response, workspaceClient, signal) {
585
+ const statementId = response.statement_id;
519
586
  return { result: {
520
- statement_id: response.statement_id,
587
+ statement_id: statementId,
521
588
  status: {
522
589
  state: response.status?.state,
523
590
  error: response.status?.error
524
- }
591
+ },
592
+ columnNames: arrowColumnNames(response),
593
+ external_links: await this._resolveAllExternalLinks(workspaceClient, statementId, response, signal),
594
+ refreshChunkLink: this._makeChunkLinkRefresher(workspaceClient, statementId)
525
595
  } };
526
596
  }
527
- async getArrowData(workspaceClient, jobId, signal) {
528
- const startTime = Date.now();
529
- return this.telemetry.startActiveSpan("arrow.getData", {
530
- kind: SpanKind.CLIENT,
531
- attributes: {
532
- "db.system": "databricks",
533
- "arrow.job_id": jobId
534
- }
535
- }, async (span) => {
536
- try {
537
- const response = await workspaceClient.statementExecution.getStatement({ statement_id: jobId }, this._createContext(signal));
538
- const chunks = response.result?.external_links;
539
- const schema = response.manifest?.schema;
540
- if (!chunks || !schema) throw ExecutionError.missingData("chunks or schema");
541
- span.setAttribute("arrow.chunk_count", chunks.length);
542
- const result = await this.arrowProcessor.processChunks(chunks, schema, signal);
543
- span.setAttribute("arrow.data_size_bytes", result.data.length);
544
- span.setStatus({ code: SpanStatusCode.OK });
545
- const duration = Date.now() - startTime;
546
- this.telemetryMetrics.queryDuration.record(duration, {
547
- operation: "arrow.getData",
548
- status: "success"
549
- });
550
- logger.event()?.setContext("sql-warehouse", {
551
- arrow_data_size_bytes: result.data.length,
552
- arrow_job_id: jobId
553
- });
554
- return result;
555
- } catch (error) {
556
- span.setStatus({
557
- code: SpanStatusCode.ERROR,
558
- message: error instanceof Error ? error.message : "Unknown error"
559
- });
560
- span.recordException(error);
561
- const duration = Date.now() - startTime;
562
- this.telemetryMetrics.queryDuration.record(duration, {
563
- operation: "arrow.getData",
564
- status: "error"
565
- });
566
- logger.error("Failed Arrow job: %s %O", jobId, error);
567
- if (error instanceof AppKitError) throw error;
568
- throw ExecutionError.statementFailed(error instanceof Error ? error.message : String(error));
569
- }
570
- });
597
+ /**
598
+ * Resolve pre-signed links for EVERY chunk of an EXTERNAL_LINKS result.
599
+ *
600
+ * The execute/getStatement response carries only the first chunk's links
601
+ * (each link, except the last, exposes `next_chunk_index`); the remaining
602
+ * chunks are fetched with `getStatementResultChunkN`. Runs in the caller's
603
+ * identity context (user creds for `.obo.sql`), so there is no cross-identity
604
+ * fetch. Only the tiny link metadata is resolved eagerly — the bytes still
605
+ * stream one chunk at a time downstream. Without this a multi-chunk result
606
+ * would be silently truncated to its first chunk.
607
+ */
608
+ async _resolveAllExternalLinks(workspaceClient, statementId, response, signal) {
609
+ const first = response.result?.external_links;
610
+ if (!first || first.length === 0) return first;
611
+ const links = [...first];
612
+ const maxFetches = response.manifest?.total_chunk_count ?? MAX_EXTERNAL_CHUNK_FOLLOWS;
613
+ let next = this._nextChunkIndex(first);
614
+ for (let fetches = 0; next != null && fetches < maxFetches; fetches++) {
615
+ if (signal?.aborted) throw ExecutionError.canceled();
616
+ const chunkLinks = (await workspaceClient.statementExecution.getStatementResultChunkN({
617
+ statement_id: statementId,
618
+ chunk_index: next
619
+ }, this._createContext(signal))).external_links ?? [];
620
+ if (chunkLinks.length === 0) break;
621
+ links.push(...chunkLinks);
622
+ next = this._nextChunkIndex(chunkLinks);
623
+ }
624
+ return links;
625
+ }
626
+ /** The `next_chunk_index` advertised by a chunk's links, if any. */
627
+ _nextChunkIndex(links) {
628
+ for (const link of links) if (link.next_chunk_index != null) return link.next_chunk_index;
629
+ }
630
+ /**
631
+ * A closure that re-mints a single chunk's pre-signed link via
632
+ * `getStatementResultChunkN`, bound to the caller's workspace client +
633
+ * statement id. Created here (in the caller's identity context) so the
634
+ * streamer — which runs outside that context — can refresh an expired link
635
+ * for `.obo.sql` statements without a cross-identity `getStatement`.
636
+ */
637
+ _makeChunkLinkRefresher(workspaceClient, statementId) {
638
+ return async (chunkIndex, signal) => {
639
+ return (await workspaceClient.statementExecution.getStatementResultChunkN({
640
+ statement_id: statementId,
641
+ chunk_index: chunkIndex
642
+ }, this._createContext(signal))).external_links?.find((l) => l.chunk_index === chunkIndex);
643
+ };
644
+ }
645
+ /**
646
+ * Stream already-resolved EXTERNAL_LINKS chunks as raw Arrow IPC bytes, one
647
+ * chunk in memory at a time. Takes the `external_links` the caller already
648
+ * has from `executeStatement` (see {@link updateWithArrowStatus}), so there
649
+ * is no extra `getStatement` round-trip and no execution-context mismatch —
650
+ * the pre-signed URLs need no auth to download.
651
+ */
652
+ streamExternalLinks(chunks, signal, refresh) {
653
+ return this.arrowProcessor.streamChunks(chunks, signal, refresh);
654
+ }
655
+ /**
656
+ * Fetch just the real column names for a completed statement from its result
657
+ * manifest — no result data. Backs the `/columns/:statementId` fallback the
658
+ * client uses when a very wide schema's names exceed the response-header
659
+ * size limit. Stateless: re-derives from the warehouse, no server cache.
660
+ */
661
+ async getColumnNames(workspaceClient, jobId, signal) {
662
+ return arrowColumnNames(await workspaceClient.statementExecution.getStatement({ statement_id: jobId }, this._createContext(signal)));
663
+ }
664
+ /**
665
+ * Normalize an error caught during statement execution/polling and rethrow.
666
+ * Abort and already-structured `AppKitError`s pass through untouched;
667
+ * anything else is wrapped as an `ExecutionError`, preserving the SDK's
668
+ * structured `ApiError.errorCode` (e.g. "INVALID_PARAMETER_VALUE",
669
+ * "BAD_REQUEST") so callers can branch on a stable identifier rather than
670
+ * substring-matching the message.
671
+ */
672
+ _rethrowStatementError(error) {
673
+ if (error instanceof Error && error.name === "AbortError") throw error;
674
+ if (error instanceof AppKitError) throw error;
675
+ const sdkErrorCode = error && typeof error === "object" && "errorCode" in error ? error.errorCode : void 0;
676
+ throw ExecutionError.statementFailed(error instanceof Error ? error.message : String(error), typeof sdkErrorCode === "string" ? sdkErrorCode : void 0);
571
677
  }
572
678
  _createContext(signal) {
573
679
  return new Context({ cancellationToken: {