@pythia-software/query-table-duckdb 0.0.0-stage → 0.7.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.
package/dist/index.js ADDED
@@ -0,0 +1,1028 @@
1
+ // src/sql.ts
2
+ import {
3
+ coerceValue,
4
+ compileFormula,
5
+ compileMetric,
6
+ isFilterable,
7
+ isSortable,
8
+ isSelectable,
9
+ opsForField
10
+ } from "@pythia-software/query-table-core";
11
+ function quoteIdentifier(value) {
12
+ if (!value || value.includes("\0")) throw Error("Invalid DuckDB identifier.");
13
+ return `"${value.replace(/"/g, '""')}"`;
14
+ }
15
+ function quoteRelation(value) {
16
+ return value.split(".").map(quoteIdentifier).join(".");
17
+ }
18
+ var aggregates = /* @__PURE__ */ new Set([
19
+ "COUNT",
20
+ "COUNT_DISTINCT",
21
+ "SUM",
22
+ "AVG",
23
+ "MIN",
24
+ "MAX",
25
+ "MEDIAN"
26
+ ]);
27
+ var safeNumber = (value) => `(CASE WHEN ${value} IS NULL OR (isfinite(${value}) AND abs(${value}) <= 9007199254740991) THEN ${value} ELSE error('Query Table numeric precision exceeded') END)`;
28
+ var SQLBuilder = class {
29
+ constructor(schema, now = Date.now(), bindings = {}) {
30
+ this.schema = schema;
31
+ this.now = now;
32
+ this.bindings = bindings;
33
+ this.args = [];
34
+ }
35
+ bind(value) {
36
+ this.args.push(value);
37
+ return `$${this.args.length}`;
38
+ }
39
+ statement(sql) {
40
+ if (sql.length > 262144 || this.args.length > 1e4)
41
+ throw Error("DuckDB statement budget exceeded.");
42
+ return { sql, args: [...this.args] };
43
+ }
44
+ definition(name) {
45
+ const field = this.schema.fields.find((field2) => field2.name === name);
46
+ if (!field || field.source.kind !== "backend")
47
+ throw Error(`Unavailable DuckDB field: ${name}.`);
48
+ return field;
49
+ }
50
+ field(name, normalize = true) {
51
+ const field = this.definition(name), value = (Object.prototype.hasOwnProperty.call(this.bindings, name) ? this.bindings[name] : void 0) ?? quoteIdentifier(
52
+ field.source.kind === "backend" ? field.source.path ?? name : name
53
+ );
54
+ if (!normalize) return value;
55
+ if (field.type === "text" || field.type === "enum")
56
+ return `NULLIF(CAST(${value} AS VARCHAR), '')`;
57
+ if (field.type === "datetime") return `CAST(${value} AS TIMESTAMPTZ)`;
58
+ return value;
59
+ }
60
+ where(terms) {
61
+ if (terms.length > 100)
62
+ throw Error("At most 100 filter terms are allowed.");
63
+ let count = 0;
64
+ return terms.length ? terms.map((term) => {
65
+ const clauses = "any" in term ? term.any : [term];
66
+ if (!clauses.length || (count += clauses.length) > 100)
67
+ throw Error("Invalid filter predicate budget.");
68
+ return `(${clauses.map((clause) => {
69
+ const field = this.definition(clause.field), raw = this.field(field.name), op = clause.op;
70
+ if (!isFilterable(field) || !opsForField(field).includes(op))
71
+ throw Error(`Unsupported filter on ${field.name}: ${op}.`);
72
+ const value = field.type === "textarray" ? `CASE WHEN len(${raw}) > 0 THEN ${raw} END` : raw;
73
+ let sql;
74
+ if (op === "is_null" || op === "is_not_null")
75
+ sql = `${value} IS ${op === "is_null" ? "" : "NOT "}NULL`;
76
+ else {
77
+ const typed = coerceValue(field.type, clause.value, this.now);
78
+ if (typeof typed === "number" && !Number.isFinite(typed))
79
+ throw Error("Filter operands must be finite.");
80
+ const param = this.bind(
81
+ field.type === "datetime" ? new Date(typed).toISOString() : typed
82
+ );
83
+ const rhs = field.type === "datetime" ? `CAST(${param} AS TIMESTAMPTZ)` : param;
84
+ if (["=", "!=", ">", ">=", "<", "<="].includes(op))
85
+ sql = `${value} ${op} ${rhs}`;
86
+ else if (["contains", "starts_with", "ends_with"].includes(op))
87
+ sql = `${op}(lower(${value}), lower(${param}))`;
88
+ else if (op === "includes")
89
+ sql = field.filter?.arrayCaseSensitive ? `list_contains(${value}, ${param})` : `list_contains(list_transform(${value}, x -> lower(x)), lower(${param}))`;
90
+ else if (op.startsWith("length_"))
91
+ sql = `length(${value}) ${op === "length_gt" ? ">" : op === "length_lt" ? "<" : "="} CAST(${param} AS DOUBLE)`;
92
+ else if (op === "matches_regex" || op === "not_matches_regex") {
93
+ if (clause.value.length > 2e3)
94
+ throw Error("Regex exceeds 2,000 characters.");
95
+ sql = `${op === "not_matches_regex" ? "NOT " : ""}regexp_matches(${value}, ${param})`;
96
+ } else throw Error(`Unsupported DuckDB operator: ${op}.`);
97
+ }
98
+ return clause.negated ? `NOT (${sql})` : sql;
99
+ }).join(" OR ")})`;
100
+ }).join(" AND ") : "TRUE";
101
+ }
102
+ order(terms, identity = true) {
103
+ if (terms.length > 20) throw Error("At most 20 sort fields are allowed.");
104
+ const output = terms.map((term) => {
105
+ const field = this.definition(term.field);
106
+ if (!isSortable(field) && field.name !== this.schema.idField)
107
+ throw Error(`Field ${field.name} is not sortable.`);
108
+ if (!["asc", "desc"].includes(term.dir) || term.nulls && !["first", "last"].includes(term.nulls))
109
+ throw Error("Invalid ordering.");
110
+ let value = this.field(field.name);
111
+ if (term.extract) {
112
+ if (field.type !== "text" || term.extract.regex.length > 2e3)
113
+ throw Error("Invalid regex extraction.");
114
+ const pattern = this.bind(term.extract.regex);
115
+ const capture = /(^|[^\\])\((?!\?)/.test(term.extract.regex) ? 1 : 0;
116
+ value = `CASE WHEN regexp_matches(${value}, ${pattern}) THEN regexp_extract(${value}, ${pattern}, ${capture}) END`;
117
+ }
118
+ return `${value} ${term.dir.toUpperCase()} NULLS ${(term.nulls ?? "last").toUpperCase()}`;
119
+ });
120
+ if (identity && !terms.some((term) => term.field === this.schema.idField))
121
+ output.push(`${this.field(this.schema.idField)} ASC NULLS LAST`);
122
+ return output.length ? `ORDER BY ${output.join(", ")}` : "";
123
+ }
124
+ expression(source, aggregate = false) {
125
+ const plan = aggregate ? compileMetric(source, this.schema) : compileFormula(source, this.schema.fields);
126
+ return { sql: this.node(plan.ast), plan };
127
+ }
128
+ node(node) {
129
+ const sql = this.nodeSQL(node);
130
+ if (sql.length > 131072)
131
+ throw Error("DuckDB expression SQL budget exceeded.");
132
+ return sql;
133
+ }
134
+ nodeSQL(node) {
135
+ if (node.kind === "literal")
136
+ return node.value === null ? "NULL" : this.bind(node.value);
137
+ if (node.kind === "field") return this.field(node.name, false);
138
+ const name = node.name, vs = node.args.map((arg) => this.node(arg)), a = vs[0], b = vs[1], c = vs[2];
139
+ if (aggregates.has(name)) {
140
+ if (name === "COUNT") return `CAST(COUNT(${a ?? "*"}) AS DOUBLE)`;
141
+ if (name === "COUNT_DISTINCT")
142
+ return `CAST(COUNT(DISTINCT ${a}) AS DOUBLE)`;
143
+ const sql = `${name}(${a})`;
144
+ return ["SUM", "AVG", "MEDIAN"].includes(name) ? safeNumber(
145
+ `CAST(${name === "SUM" ? `COALESCE(${sql}, 0)` : sql} AS DOUBLE)`
146
+ ) : sql;
147
+ }
148
+ if (name.startsWith("op:")) {
149
+ const op = name.slice(3);
150
+ if (op === "IN") return `(${a} IN (${vs.slice(1).join(", ")}))`;
151
+ if (op === "BETWEEN") return `(${a} BETWEEN ${b} AND ${c})`;
152
+ if (["/", "%"].includes(op))
153
+ return safeNumber(
154
+ `(CASE WHEN ${b} = 0 THEN error('Division by zero') ELSE ${a} ${op} ${b} END)`
155
+ );
156
+ return ["+", "-", "*"].includes(op) ? safeNumber(`(${a} ${op} ${b})`) : `(${a} ${op} ${b})`;
157
+ }
158
+ if (name.startsWith("unary:")) return `(${name.slice(6)} ${a})`;
159
+ if (name === "IF") return `CASE WHEN ${a} THEN ${b} ELSE ${c} END`;
160
+ if (name === "IFS")
161
+ return `CASE ${vs.slice(0, -1).filter((_, i) => i % 2 === 0).map((v, i) => `WHEN ${v} THEN ${vs[2 * i + 1]}`).join(" ")} ELSE ${vs[vs.length - 1]} END`;
162
+ if (name === "SWITCH")
163
+ return `CASE ${vs.slice(1, -1).filter((_, i) => i % 2 === 0).map(
164
+ (v, i) => `WHEN ${a} IS NOT DISTINCT FROM ${v} THEN ${vs[2 * i + 2]}`
165
+ ).join(" ")} ELSE ${vs[vs.length - 1]} END`;
166
+ if (name === "IS_NULL") return `(${a} IS NULL)`;
167
+ if (name === "IS_EMPTY") {
168
+ const input = node.args[0];
169
+ const array = input.kind === "field" && input.valueType === "textarray" || input.kind === "call" && ["SPLIT", "ARRAY_UNIQUE", "ARRAY_SORT"].includes(input.name);
170
+ return `(${a} IS NULL OR len(${array ? a : `CAST(${a} AS VARCHAR)`}) = 0)`;
171
+ }
172
+ if (name === "COALESCE" || name === "NULLIF")
173
+ return `${name}(${vs.join(", ")})`;
174
+ if (name === "IFERROR")
175
+ throw Error("IFERROR is not available in the qt-duckdb-v1 SQL profile.");
176
+ if (name === "ROUND") {
177
+ const scale = `power(10, ${b ?? "0"})`;
178
+ return safeNumber(`(floor(${a} * ${scale} + 0.5) / ${scale})`);
179
+ }
180
+ if (name === "CLAMP")
181
+ return `CASE WHEN ${a} IS NULL OR ${b} IS NULL OR ${c} IS NULL THEN NULL ELSE greatest(${b}, least(${c}, ${a})) END`;
182
+ if (name === "LEAST" || name === "GREATEST" || name === "CONCAT")
183
+ return `CASE WHEN ${vs.map((v) => `${v} IS NULL`).join(" OR ")} THEN NULL ELSE ${name}(${vs.join(", ")}) END`;
184
+ if (name === "CONCAT_WS")
185
+ return `CASE WHEN ${a} IS NULL THEN NULL ELSE concat_ws(${vs.join(", ")}) END`;
186
+ if (["CONTAINS", "STARTS_WITH", "ENDS_WITH"].includes(name))
187
+ return `${name.toLowerCase()}(CASE WHEN ${c ?? "FALSE"} THEN lower(${a}) ELSE ${a} END, CASE WHEN ${c ?? "FALSE"} THEN lower(${b}) ELSE ${b} END)`;
188
+ if (name === "TO_NUMBER") return safeNumber(`CAST(${a} AS DOUBLE)`);
189
+ if (name === "TO_TEXT") return `CAST(${a} AS VARCHAR)`;
190
+ if (name === "TO_BOOLEAN") return `CAST(${a} AS BOOLEAN)`;
191
+ if (name === "TO_DATETIME") return `CAST(${a} AS TIMESTAMPTZ)`;
192
+ if (name === "SPLIT") return `string_split(${a}, ${b})`;
193
+ if (name === "JOIN") return `array_to_string(${a}, ${b})`;
194
+ if (name === "ARRAY_LENGTH") return `len(${a})`;
195
+ if (name === "ARRAY_CONTAINS") return `list_contains(${a}, ${b})`;
196
+ if (name === "ARRAY_GET")
197
+ return `CASE WHEN ${b} < 0 THEN error('Array index must be nonnegative') ELSE list_extract(${a}, CAST(${b} + 1 AS BIGINT)) END`;
198
+ if (name === "ARRAY_SORT") return `list_sort(${a})`;
199
+ if (name === "ARRAY_UNIQUE")
200
+ return `list_filter(${a}, (x, i) -> list_position(${a}, x) = i)`;
201
+ if (name === "WEEKDAY")
202
+ return `extract(isodow FROM CAST(${a} AS TIMESTAMPTZ))`;
203
+ if (["YEAR", "MONTH", "DAY", "HOUR"].includes(name))
204
+ return `extract(${name.toLowerCase()} FROM CAST(${a} AS TIMESTAMPTZ))`;
205
+ if (name === "DATE_TRUNC")
206
+ return `date_trunc(${a}, CAST(${b} AS TIMESTAMPTZ))`;
207
+ if (name === "DATE_DIFF" || name === "DATE_ADD" || name === "FORMAT_DATE" || name.startsWith("REGEX_"))
208
+ throw Error(`${name} is not available in the qt-duckdb-v1 SQL profile.`);
209
+ if (["LEFT", "RIGHT", "SUBSTRING", "LPAD", "RPAD", "SPLIT_PART"].includes(
210
+ name
211
+ )) {
212
+ const args = [...vs];
213
+ const indices = name === "SUBSTRING" ? [1, 2] : name === "SPLIT_PART" ? [2] : [1];
214
+ for (const index of indices)
215
+ if (args[index] !== void 0)
216
+ args[index] = `CAST(${args[index]} AS BIGINT)`;
217
+ return `${name}(${args.join(", ")})`;
218
+ }
219
+ if ([
220
+ "LENGTH",
221
+ "LOWER",
222
+ "UPPER",
223
+ "TRIM",
224
+ "LTRIM",
225
+ "RTRIM",
226
+ "REPLACE",
227
+ "ABS",
228
+ "FLOOR",
229
+ "CEIL",
230
+ "TRUNC",
231
+ "POWER",
232
+ "SQRT"
233
+ ].includes(name))
234
+ return `${name}(${vs.join(", ")})`;
235
+ throw Error(`Unsupported DuckDB function: ${name}.`);
236
+ }
237
+ };
238
+ function compileDuckDBRows(query, schema, relation, now = Date.now()) {
239
+ const sql = new SQLBuilder(schema, now);
240
+ if (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > 1e4 || !Number.isSafeInteger(query.offset) || query.offset < 0)
241
+ throw Error("Invalid result window.");
242
+ const select = [.../* @__PURE__ */ new Set([schema.idField, ...query.select])].map((name) => {
243
+ const field = sql.definition(name);
244
+ if (!isSelectable(field) && name !== schema.idField)
245
+ throw Error(`Field ${name} is not selectable.`);
246
+ return `${sql.field(name, false)} AS ${quoteIdentifier(name)}`;
247
+ });
248
+ const where = sql.where(query.where), order = sql.order(query.orderBy);
249
+ return sql.statement(
250
+ `SELECT ${select.join(", ")} FROM ${quoteRelation(relation)} WHERE ${where} ${order} LIMIT ${sql.bind(query.limit)} OFFSET ${sql.bind(query.offset)}`
251
+ );
252
+ }
253
+
254
+ // src/backend.ts
255
+ import {
256
+ compileFormula as compileFormula2,
257
+ createGroupingModel,
258
+ GROUP_ID_FIELD,
259
+ metricExpression,
260
+ validateMetric,
261
+ isSelectable as isSelectable2,
262
+ isFilterable as isFilterable2,
263
+ boxSummary,
264
+ histogramEdges,
265
+ histogramCounts,
266
+ compareMetricValues,
267
+ metricDateFloor,
268
+ metricDateTime,
269
+ metricDateNext,
270
+ metricDateRange
271
+ } from "@pythia-software/query-table-core";
272
+ var DUCKDB_PROFILE = "qt-duckdb-v1";
273
+ function abort(signal) {
274
+ if (signal?.aborted)
275
+ throw new DOMException("DuckDB query cancelled.", "AbortError");
276
+ }
277
+ function normalizeDuckDBValue(value) {
278
+ if (typeof value === "bigint") {
279
+ const numeric = Number(value);
280
+ if (!Number.isSafeInteger(numeric))
281
+ throw Error("DuckDB integer exceeds JavaScript safe precision.");
282
+ return numeric;
283
+ }
284
+ if (value instanceof Date) return value.toISOString();
285
+ if (typeof value === "number" && (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER))
286
+ throw Error("DuckDB number exceeds finite safe precision.");
287
+ if (Array.isArray(value)) return value.map(normalizeDuckDBValue);
288
+ if (value && typeof value === "object") {
289
+ if ("toArray" in value && typeof value.toArray === "function")
290
+ return Array.from(value.toArray()).map(normalizeDuckDBValue);
291
+ if ("toJSON" in value && typeof value.toJSON === "function")
292
+ return normalizeDuckDBValue(value.toJSON());
293
+ return Object.fromEntries(
294
+ Object.entries(value).map(([key, item]) => [
295
+ key,
296
+ normalizeDuckDBValue(item)
297
+ ])
298
+ );
299
+ }
300
+ return value;
301
+ }
302
+ function positive(value, fallback, maximum) {
303
+ const out = value ?? fallback;
304
+ if (!Number.isSafeInteger(out) || out < 1 || out > maximum)
305
+ throw Error("Invalid DuckDB resource budget.");
306
+ return out;
307
+ }
308
+ var DuckDBBackend = class {
309
+ constructor(options) {
310
+ this.options = options;
311
+ this.queue = Promise.resolve();
312
+ this.sequence = 0;
313
+ this.computed = /* @__PURE__ */ new Map();
314
+ const fields = options.schema.fields.filter((field) => field.source.kind === "backend").map((field) => ({
315
+ ...field,
316
+ source: { ...field.source, kind: "backend", path: field.name }
317
+ }));
318
+ if (!fields.some((field) => field.name === options.schema.idField) || new Set(fields.map((field) => field.name)).size !== fields.length)
319
+ throw Error(
320
+ "DuckDB needs a unique field catalogue and a bound identity."
321
+ );
322
+ quoteRelation(options.relation);
323
+ this.schema = { ...options.schema, fields };
324
+ const definitions = new Map(
325
+ (options.computedColumns ?? []).map((column) => [
326
+ `@computed/${column.id}`,
327
+ column
328
+ ])
329
+ );
330
+ const visiting = /* @__PURE__ */ new Set();
331
+ const resolve = (name) => {
332
+ if (this.computed.has(name)) return this.computed.get(name);
333
+ const definition = definitions.get(name);
334
+ if (!definition) return void 0;
335
+ if (visiting.has(name)) throw Error("Computed definition cycle.");
336
+ visiting.add(name);
337
+ const plan = compileFormula2(
338
+ definition.expression.source,
339
+ fields,
340
+ resolve
341
+ );
342
+ new SQLBuilder(this.schema).node(plan.ast);
343
+ visiting.delete(name);
344
+ this.computed.set(name, plan);
345
+ return plan;
346
+ };
347
+ for (const name of definitions.keys()) {
348
+ const plan = resolve(name);
349
+ fields.push({
350
+ name,
351
+ label: definitions.get(name).label,
352
+ type: plan.type === "null" ? "text" : plan.type,
353
+ source: { kind: "backend", path: name },
354
+ filter: { enabled: true },
355
+ sort: { enabled: true },
356
+ aggregate: { measure: true, groupable: plan.type !== "textarray" }
357
+ });
358
+ }
359
+ this.execution = {
360
+ profile: DUCKDB_PROFILE,
361
+ planToken: `duckdb:${JSON.stringify([...definitions.values()].map((column) => [column.id, column.revision]))}`,
362
+ resolvedRevisions: Object.fromEntries(
363
+ [...definitions.values()].map((column) => [column.id, column.revision])
364
+ ),
365
+ fields: Object.fromEntries(
366
+ fields.filter((field) => definitions.has(field.name)).map((field) => [
367
+ field.name,
368
+ {
369
+ type: field.type,
370
+ select: true,
371
+ filter: true,
372
+ sort: true,
373
+ measure: true,
374
+ group: field.type !== "textarray"
375
+ }
376
+ ])
377
+ ),
378
+ ...options.snapshot !== void 0 ? { snapshot: options.snapshot } : {}
379
+ };
380
+ this.capabilities = {
381
+ version: 3,
382
+ profile: DUCKDB_PROFILE,
383
+ groupedRows: true,
384
+ calendarGrouping: true,
385
+ computedSourceFilters: definitions.size > 0,
386
+ regex: true,
387
+ regexSourceFilters: true,
388
+ regexAggregateExpressions: false,
389
+ maxRows: positive(options.maxRows, 1e4, 1e4),
390
+ maxGroups: positive(options.maxGroups, 1e4, 1e4),
391
+ maxPopulation: positive(options.maxPopulation, 25e4, 1e7)
392
+ };
393
+ this.maxSamples = positive(options.maxSamples, 1e5, 1e6);
394
+ this.maxStagingBytes = positive(
395
+ options.maxStagingBytes,
396
+ 64 * 1024 * 1024,
397
+ 1024 * 1024 * 1024
398
+ );
399
+ }
400
+ serial(work, signal) {
401
+ const next = this.queue.then(() => {
402
+ abort(signal);
403
+ return work();
404
+ });
405
+ this.queue = next.catch(() => {
406
+ });
407
+ return next;
408
+ }
409
+ /** Wait before closing an owned driver connection. */
410
+ async drain() {
411
+ await this.queue;
412
+ }
413
+ async run(statement, signal) {
414
+ abort(signal);
415
+ const cancel = () => {
416
+ void this.options.connection.interrupt?.();
417
+ };
418
+ signal?.addEventListener("abort", cancel, { once: true });
419
+ try {
420
+ const rows = await this.options.connection.query(
421
+ statement.sql,
422
+ statement.args
423
+ );
424
+ abort(signal);
425
+ return rows.map((row) => normalizeDuckDBValue(row));
426
+ } finally {
427
+ signal?.removeEventListener("abort", cancel);
428
+ }
429
+ }
430
+ sql(statement, args = [], signal) {
431
+ return this.run({ sql: statement, args }, signal);
432
+ }
433
+ window(query) {
434
+ if (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > this.capabilities.maxRows || !Number.isSafeInteger(query.offset) || query.offset < 0)
435
+ throw Error("Invalid DuckDB result window.");
436
+ }
437
+ builder(schema = this.schema, now = Date.now()) {
438
+ return new SQLBuilder(schema, now);
439
+ }
440
+ projectedSource(now) {
441
+ const bindings = Object.fromEntries(
442
+ this.schema.fields.filter((field) => !this.computed.has(field.name)).map((field) => [
443
+ field.name,
444
+ (this.options.bindings && Object.prototype.hasOwnProperty.call(
445
+ this.options.bindings,
446
+ field.name
447
+ ) ? this.options.bindings[field.name] : void 0) ?? quoteIdentifier(
448
+ (() => {
449
+ const original = this.options.schema.fields.find(
450
+ (item) => item.name === field.name
451
+ );
452
+ return original?.source.kind === "backend" ? original.source.path ?? field.name : field.name;
453
+ })()
454
+ )
455
+ ])
456
+ );
457
+ const builder = new SQLBuilder(this.schema, now, bindings);
458
+ const projections = this.schema.fields.map((field) => {
459
+ const plan = this.computed.get(field.name), raw = plan ? builder.node(plan.ast) : bindings[field.name];
460
+ return `${raw} AS ${quoteIdentifier(field.name)}`;
461
+ });
462
+ return builder.statement(
463
+ `SELECT ${projections.join(", ")} FROM ${quoteRelation(this.options.relation)} AS r`
464
+ );
465
+ }
466
+ async transaction(work, signal) {
467
+ return this.serial(async () => {
468
+ await this.sql("BEGIN TRANSACTION", [], signal);
469
+ const tables = [];
470
+ try {
471
+ const result = await work(tables);
472
+ for (const table of tables.reverse())
473
+ await this.sql(`DROP TABLE ${quoteIdentifier(table)}`, [], signal);
474
+ await this.sql("COMMIT", [], signal);
475
+ return result;
476
+ } catch (error) {
477
+ await this.sql("ROLLBACK").catch(() => {
478
+ });
479
+ abort(signal);
480
+ throw error;
481
+ }
482
+ }, signal);
483
+ }
484
+ temp(tables) {
485
+ const name = `qt_duckdb_${++this.sequence}`;
486
+ tables.push(name);
487
+ return name;
488
+ }
489
+ async stage(source, now, tables, signal) {
490
+ const projection = this.projectedSource(now), builder = this.builder(this.schema, now);
491
+ builder.args.push(...projection.args);
492
+ const where = builder.where(source.where), name = this.temp(tables);
493
+ const population = `(${projection.sql}) r WHERE ${where}`;
494
+ const [{ n, bytes }] = await this.run(
495
+ builder.statement(
496
+ `SELECT COUNT(*) AS n, COALESCE(SUM(octet_length(encode(CAST(to_json(r) AS VARCHAR)))),0) AS bytes FROM ${population}`
497
+ ),
498
+ signal
499
+ );
500
+ if (n > this.capabilities.maxPopulation || bytes > this.maxStagingBytes)
501
+ throw Error("DuckDB source population or staging byte budget exceeded.");
502
+ await this.run(
503
+ builder.statement(
504
+ `CREATE TEMP TABLE ${quoteIdentifier(name)} AS SELECT * FROM ${population}`
505
+ ),
506
+ signal
507
+ );
508
+ const [{ invalid }] = await this.sql(
509
+ `SELECT COUNT(*) - COUNT(DISTINCT ${quoteIdentifier(this.schema.idField)}) AS invalid FROM ${quoteIdentifier(name)}`,
510
+ [],
511
+ signal
512
+ );
513
+ if (invalid !== 0)
514
+ throw Error("DuckDB source identity must be unique and non-null.");
515
+ return { name, count: n };
516
+ }
517
+ async page(relation, schema, query, now, signal) {
518
+ this.window(query);
519
+ const builder = this.builder(schema, now), where = builder.where(query.where);
520
+ const [{ total }] = await this.run(
521
+ builder.statement(
522
+ `SELECT COUNT(*) AS total FROM ${quoteIdentifier(relation)} WHERE ${where}`
523
+ ),
524
+ signal
525
+ );
526
+ const order = builder.order(query.orderBy), names = [.../* @__PURE__ */ new Set([schema.idField, ...query.select])];
527
+ names.forEach((name) => {
528
+ if (!isSelectable2(builder.definition(name)) && name !== schema.idField)
529
+ throw Error(`Field ${name} is not selectable.`);
530
+ });
531
+ const rows = await this.run(
532
+ builder.statement(
533
+ `SELECT ${names.map(quoteIdentifier).join(", ")} FROM ${quoteIdentifier(relation)} WHERE ${where} ${order} LIMIT ${builder.bind(query.limit)} OFFSET ${builder.bind(query.offset)}`
534
+ ),
535
+ signal
536
+ );
537
+ return { rows: rows.map((row) => this.formatRow(row, schema)), total };
538
+ }
539
+ formatRow(row, schema) {
540
+ return Object.fromEntries(
541
+ Object.entries(row).map(([name, value]) => {
542
+ const field = schema.fields.find((field2) => field2.name === name);
543
+ if (value != null && field?.type === "datetime") {
544
+ const text = String(value);
545
+ const canonical = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(text) ? `${text.replace(" ", "T")}Z` : text;
546
+ const date = typeof value === "number" ? new Date(value) : new Date(canonical);
547
+ if (!Number.isFinite(date.getTime()))
548
+ throw Error(`Invalid datetime returned for ${name}.`);
549
+ value = date.toISOString();
550
+ }
551
+ return [name, value];
552
+ })
553
+ );
554
+ }
555
+ validateRequest(request) {
556
+ if (request.version !== 3 || request.profile !== DUCKDB_PROFILE || request.diagnostics.length)
557
+ throw Error("Invalid DuckDB execution request or profile.");
558
+ if (!Number.isSafeInteger(request.now) || Math.abs(request.now) > 864e13)
559
+ throw Error("Invalid query clock.");
560
+ if ((request.planToken ?? "") !== (Object.keys(this.execution.fields).length ? this.execution.planToken : "") && request.planToken !== this.execution.planToken)
561
+ throw Error("DuckDB execution handshake changed.");
562
+ if ((request.snapshot ?? "") !== (this.options.snapshot ?? ""))
563
+ throw Error("DuckDB snapshot changed.");
564
+ for (const [key, revision] of Object.entries(request.expectedRevisions))
565
+ if (this.execution.resolvedRevisions[key] !== revision)
566
+ throw Error("Computed definitions changed; refresh the handshake.");
567
+ if (request.metrics.length > 20)
568
+ throw Error("At most 20 metrics are allowed.");
569
+ this.window(request.result);
570
+ const model = createGroupingModel(this.schema);
571
+ if (request.grouping) model.validate(request.grouping);
572
+ const resultSchema = request.grouping ? model.resultSchema(request.grouping.keys, request.grouping.summaries) : this.schema;
573
+ this.builder(this.schema, request.now).where(request.source.where);
574
+ const result = this.builder(resultSchema, request.now);
575
+ result.where(request.result.where);
576
+ result.order(request.result.orderBy);
577
+ request.result.select.forEach((name) => result.definition(name));
578
+ }
579
+ async execute(request, signal) {
580
+ this.validateRequest(request);
581
+ return this.transaction(async (tables) => {
582
+ const source = await this.stage(
583
+ request.source,
584
+ request.now,
585
+ tables,
586
+ signal
587
+ );
588
+ let relation = source.name, schema = this.schema;
589
+ if (request.grouping) {
590
+ const grouping = request.grouping, model = createGroupingModel(schema), builder = this.builder(schema, request.now);
591
+ const keys = grouping.keys.map((name) => builder.field(name, false)), names = grouping.keys.map(quoteIdentifier);
592
+ const representative = builder.order(
593
+ grouping.representative?.orderBy ?? [
594
+ { field: schema.idField, dir: "desc" }
595
+ ],
596
+ false
597
+ ).replace(/^ORDER BY /, "");
598
+ const tie = grouping.representative?.orderBy.some(
599
+ (term) => term.field === schema.idField
600
+ ) ? "" : `, ${builder.field(schema.idField)} DESC NULLS LAST`;
601
+ const summaries = grouping.summaries.map(
602
+ (summary) => `${summary.op === "row" ? `first(${builder.field(summary.field, false)} ORDER BY ${representative}${tie})` : builder.expression(model.summaryExpression(summary), true).sql} AS ${quoteIdentifier(summary.id)}`
603
+ );
604
+ relation = this.temp(tables);
605
+ const identity = `CAST(to_json(struct_pack(${keys.map((key, i) => `k${i} := ${key}`).join(", ")})) AS VARCHAR)`;
606
+ await this.run(
607
+ builder.statement(
608
+ `CREATE TEMP TABLE ${quoteIdentifier(relation)} AS SELECT ${identity} AS ${quoteIdentifier(GROUP_ID_FIELD)}, ${keys.map((key, i) => `${key} AS ${names[i]}`).join(", ")}${summaries.length ? `, ${summaries.join(", ")}` : ""} FROM ${quoteIdentifier(source.name)} GROUP BY ${keys.join(", ")}`
609
+ ),
610
+ signal
611
+ );
612
+ const [{ n }] = await this.sql(
613
+ `SELECT COUNT(*) AS n FROM ${quoteIdentifier(relation)}`,
614
+ [],
615
+ signal
616
+ );
617
+ if (n > this.capabilities.maxGroups)
618
+ throw Error("DuckDB grouped row budget exceeded.");
619
+ schema = model.resultSchema(grouping.keys, grouping.summaries);
620
+ }
621
+ const page = await this.page(
622
+ relation,
623
+ schema,
624
+ request.result,
625
+ request.now,
626
+ signal
627
+ );
628
+ const metrics = await this.metrics(
629
+ relation,
630
+ schema,
631
+ request.result,
632
+ request.metrics,
633
+ request.now,
634
+ tables,
635
+ signal
636
+ );
637
+ return {
638
+ version: 3,
639
+ now: request.now,
640
+ sourceTotal: source.count,
641
+ rows: request.rows ? page.rows : [],
642
+ total: page.total,
643
+ schema: {
644
+ name: schema.name,
645
+ idField: schema.idField,
646
+ fields: schema.fields.map((field) => ({
647
+ name: field.name,
648
+ type: field.type
649
+ }))
650
+ },
651
+ metrics,
652
+ execution: { ...this.execution, planToken: request.planToken ?? "" }
653
+ };
654
+ }, signal);
655
+ }
656
+ async metrics(relation, schema, query, clauses, now, tables, signal) {
657
+ const output = [];
658
+ for (const clause of clauses) {
659
+ abort(signal);
660
+ try {
661
+ output.push(
662
+ await this.metric(
663
+ relation,
664
+ schema,
665
+ query,
666
+ clause,
667
+ now,
668
+ tables,
669
+ signal
670
+ )
671
+ );
672
+ } catch (error) {
673
+ abort(signal);
674
+ output.push({
675
+ id: clause.id,
676
+ buckets: [],
677
+ error: error instanceof Error ? error.message : String(error)
678
+ });
679
+ }
680
+ }
681
+ return output;
682
+ }
683
+ async metric(relation, schema, query, clause, now, tables, signal) {
684
+ if (clause.distribution && !clause.display)
685
+ clause = { ...clause, display: { kind: clause.distribution.kind } };
686
+ const issues = validateMetric(clause, schema);
687
+ if (issues.length)
688
+ throw Error(issues.map((issue) => issue.message).join(" "));
689
+ const builder = this.builder(schema, now), where = builder.where(query.where);
690
+ const window = clause.scope === "shownRows" ? `${builder.order(query.orderBy)} LIMIT ${builder.bind(query.limit)} OFFSET ${builder.bind(query.offset)}` : "";
691
+ let population = `(SELECT * FROM ${quoteIdentifier(relation)} WHERE ${where} ${window})`;
692
+ let dateMapping;
693
+ if (clause.dateGrouping) {
694
+ const config = clause.dateGrouping, source = builder.field(config.field, false);
695
+ const dates = await this.run(
696
+ builder.statement(
697
+ `SELECT DISTINCT CAST(${source} AS VARCHAR) AS value FROM ${population} r WHERE ${source} IS NOT NULL`
698
+ ),
699
+ signal
700
+ );
701
+ dateMapping = this.temp(tables);
702
+ await this.sql(
703
+ `CREATE TEMP TABLE ${quoteIdentifier(dateMapping)} (value TIMESTAMPTZ, bucket TIMESTAMPTZ)`,
704
+ [],
705
+ signal
706
+ );
707
+ for (let i = 0; i < dates.length; i += 250) {
708
+ const args = [];
709
+ const values = dates.slice(i, i + 250).map((row) => {
710
+ const value = /^\d{4}-\d{2}-\d{2}$/.test(String(row.value)) ? String(row.value) : this.formatRow({ [config.field]: row.value }, schema)[config.field];
711
+ args.push(
712
+ value,
713
+ new Date(
714
+ metricDateFloor(
715
+ metricDateTime(value, config.timeZone),
716
+ config.interval,
717
+ config.timeZone
718
+ )
719
+ ).toISOString()
720
+ );
721
+ return `(CAST($${args.length - 1} AS TIMESTAMPTZ), CAST($${args.length} AS TIMESTAMPTZ))`;
722
+ });
723
+ if (values.length)
724
+ await this.sql(
725
+ `INSERT INTO ${quoteIdentifier(dateMapping)} VALUES ${values.join(", ")}`,
726
+ args,
727
+ signal
728
+ );
729
+ }
730
+ population = `(SELECT r.* EXCLUDE (${quoteIdentifier(config.field)}), m.bucket AS ${quoteIdentifier(config.field)} FROM ${population} r LEFT JOIN ${quoteIdentifier(dateMapping)} m ON CAST(r.${quoteIdentifier(config.field)} AS TIMESTAMPTZ) = m.value)`;
731
+ }
732
+ const keys = clause.groupBy.map((name) => builder.field(name, false)), groups = keys.length ? `GROUP BY ${keys.join(", ")}` : "";
733
+ const distribution = clause.distribution && ["box", "histogram"].includes(clause.display?.kind ?? "") ? clause.distribution : void 0;
734
+ const expression = distribution ? builder.expression(distribution.input).sql : builder.expression(metricExpression(clause), true).sql;
735
+ const projections = keys.map(
736
+ (key, index) => `${key} AS ${quoteIdentifier(`g${index}`)}`
737
+ );
738
+ projections.push(
739
+ "COUNT(*) AS count",
740
+ distribution ? `list(${expression}) FILTER (WHERE ${expression} IS NOT NULL) AS samples` : `${expression} AS value`
741
+ );
742
+ if (!distribution && clause.display?.kind === "scatter")
743
+ projections.push(
744
+ `${builder.expression(clause.expressionY ?? "COUNT()", true).sql} AS y`
745
+ );
746
+ const rows = await this.run(
747
+ builder.statement(
748
+ `SELECT ${projections.join(", ")} FROM ${population} r ${groups} LIMIT ${this.capabilities.maxGroups + 1}`
749
+ ),
750
+ signal
751
+ );
752
+ if (rows.length > this.capabilities.maxGroups)
753
+ throw Error("DuckDB metric group budget exceeded.");
754
+ if (distribution && rows.some(
755
+ (row) => row.samples?.length && row.samples.length > this.maxSamples
756
+ ))
757
+ throw Error("DuckDB exact sample budget exceeded.");
758
+ const edges = distribution?.kind === "histogram" ? histogramEdges(
759
+ rows.flatMap((row) => row.samples ?? []),
760
+ distribution.bins ?? 10
761
+ ) : [];
762
+ const buckets = rows.map((row) => {
763
+ const samples = row.samples ?? [];
764
+ return {
765
+ keys: clause.groupBy.map((name, index) => {
766
+ const value = row[`g${index}`] ?? null;
767
+ return schema.fields.find((field) => field.name === name)?.type === "datetime" && value !== null ? this.formatRow({ [name]: value }, schema)[name] : value;
768
+ }),
769
+ count: row.count,
770
+ value: distribution ? null : row.value,
771
+ ...row.y !== void 0 ? { y: row.y } : {},
772
+ ...distribution ? {
773
+ nullCount: row.count - samples.length,
774
+ distribution: distribution.kind === "box" ? {
775
+ kind: "box",
776
+ summary: boxSummary(samples, distribution.whiskers)
777
+ } : {
778
+ kind: "histogram",
779
+ n: samples.length,
780
+ edges,
781
+ counts: histogramCounts(samples, edges)
782
+ }
783
+ } : {}
784
+ };
785
+ });
786
+ if (clause.dateGrouping?.includeEmpty) {
787
+ const config = clause.dateGrouping, observed = buckets.filter((bucket) => bucket.keys[0] !== null).map((bucket) => metricDateTime(bucket.keys[0]));
788
+ const range = metricDateRange(
789
+ config,
790
+ {
791
+ ...query,
792
+ select: query.select.map((field) => ({ field }))
793
+ },
794
+ observed,
795
+ now,
796
+ (name) => name
797
+ );
798
+ const series = new Map(
799
+ buckets.filter((bucket) => bucket.keys[0] !== null).map((bucket) => [
800
+ JSON.stringify(bucket.keys.slice(1)),
801
+ bucket.keys.slice(1)
802
+ ])
803
+ );
804
+ if (keys.length === 1 && !series.size) series.set("[]", []);
805
+ const existing = new Set(
806
+ buckets.map((bucket) => JSON.stringify(bucket.keys))
807
+ );
808
+ if (range)
809
+ for (let time = range[0]; time <= range[1]; time = metricDateNext(time, config.interval, config.timeZone)) {
810
+ for (const tuple of series.values()) {
811
+ const values = [new Date(time).toISOString(), ...tuple];
812
+ if (!existing.has(JSON.stringify(values))) {
813
+ if (buckets.length >= this.capabilities.maxGroups)
814
+ throw Error("DuckDB calendar group budget exceeded.");
815
+ buckets.push({
816
+ keys: values,
817
+ count: 0,
818
+ value: !clause.expression && ["count", "count_distinct", "sum"].includes(clause.op) || /^(COUNT|COUNT_DISTINCT|SUM)\([^)]*\)$/.test(
819
+ metricExpression(clause)
820
+ ) ? 0 : null
821
+ });
822
+ }
823
+ }
824
+ }
825
+ }
826
+ const sort = clause.sort ?? [
827
+ { key: "value", dir: "desc" }
828
+ ];
829
+ buckets.sort((a, b) => {
830
+ for (const term of sort) {
831
+ const value = (bucket) => term.key.startsWith("group") ? bucket.keys[Number(term.key.slice(5))] ?? null : term.key === "samples" ? bucket.distribution?.kind === "box" ? bucket.distribution.summary?.n ?? 0 : bucket.distribution?.n ?? 0 : bucket[term.key] ?? null;
832
+ const av = value(a), bv = value(b);
833
+ if (av === null || bv === null) {
834
+ if (av !== bv)
835
+ return (av === null ? 1 : -1) * (term.nulls === "first" ? -1 : 1);
836
+ } else {
837
+ const result = compareMetricValues(av, bv);
838
+ if (result) return result * (term.dir === "asc" ? 1 : -1);
839
+ }
840
+ }
841
+ return JSON.stringify(a.keys).localeCompare(JSON.stringify(b.keys));
842
+ });
843
+ const limit = clause.groupLimit ?? 1e4;
844
+ return {
845
+ id: clause.id,
846
+ buckets: buckets.slice(0, limit),
847
+ groupCount: buckets.length,
848
+ coverage: buckets.length > limit ? "partial" : "exact",
849
+ ...clause.scope ? { scope: clause.scope } : {}
850
+ };
851
+ }
852
+ transport() {
853
+ return {
854
+ queryCapabilities: this.capabilities,
855
+ metricCapabilities: {
856
+ version: 2,
857
+ expressions: true,
858
+ shownRows: true,
859
+ distributions: true,
860
+ computedFields: this.computed.size > 0,
861
+ profile: DUCKDB_PROFILE,
862
+ maxGroups: this.capabilities.maxGroups
863
+ },
864
+ fetchQueryV3: (request, signal) => this.execute(request, signal),
865
+ fetchRows: (query, signal) => this.transaction(async (tables) => {
866
+ this.window(query);
867
+ const source = await this.stage(query, Date.now(), tables, signal);
868
+ return this.page(
869
+ source.name,
870
+ this.schema,
871
+ { ...query, where: [] },
872
+ Date.now(),
873
+ signal
874
+ );
875
+ }, signal),
876
+ fetchMetrics: (query, signal) => {
877
+ if (query.diagnostics.length || query.profile && query.profile !== DUCKDB_PROFILE || query.metrics.length > 20)
878
+ return Promise.reject(Error("Invalid DuckDB metric request."));
879
+ return this.transaction(async (tables) => {
880
+ const now = Date.now(), server = { ...query, select: [] };
881
+ const source = await this.stage(server, now, tables, signal);
882
+ return {
883
+ metrics: await this.metrics(
884
+ source.name,
885
+ this.schema,
886
+ server,
887
+ query.metrics,
888
+ now,
889
+ tables,
890
+ signal
891
+ )
892
+ };
893
+ }, signal);
894
+ },
895
+ fetchAggregations: (query, signal) => this.transaction(async (tables) => {
896
+ if (query.diagnostics?.length || query.aggregations.length > 20)
897
+ throw Error("Invalid DuckDB aggregation request.");
898
+ const server = {
899
+ select: [],
900
+ where: query.where,
901
+ orderBy: [],
902
+ limit: 100,
903
+ offset: 0
904
+ }, now = Date.now();
905
+ const source = await this.stage(server, now, tables, signal);
906
+ return {
907
+ metrics: await this.metrics(
908
+ source.name,
909
+ this.schema,
910
+ { ...server, where: [] },
911
+ query.aggregations,
912
+ now,
913
+ tables,
914
+ signal
915
+ )
916
+ };
917
+ }, signal),
918
+ fetchDistinctValues: (query, signal) => this.serial(async () => {
919
+ const builder = this.builder(), field = builder.definition(query.field), limit = positive(query.limit, 50, 500);
920
+ if (!isFilterable2(field))
921
+ throw Error(`Field ${field.name} is not filterable.`);
922
+ if (query.search.length > 2e3)
923
+ throw Error("Distinct search exceeds its budget.");
924
+ const source = this.projectedSource(Date.now()), value = builder.field(query.field), array = field.type === "textarray";
925
+ builder.args.push(...source.args);
926
+ const param = builder.bind(query.search);
927
+ const rows = await this.run(
928
+ builder.statement(
929
+ `SELECT CAST(value AS VARCHAR) AS value, COUNT(*) AS n FROM (SELECT ${array ? `unnest(${value})` : value} AS value FROM (${source.sql}) r) v WHERE value IS NOT NULL AND contains(lower(CAST(value AS VARCHAR)), lower(${param})) GROUP BY value ORDER BY n DESC, value ASC LIMIT ${limit + 1}`
930
+ ),
931
+ signal
932
+ );
933
+ const [{ n }] = await this.sql(
934
+ `SELECT COUNT(*) AS n FROM (${source.sql}) r WHERE ${array ? `COALESCE(len(${value}),0) = 0` : `${value} IS NULL`}`,
935
+ source.args,
936
+ signal
937
+ );
938
+ return {
939
+ values: rows.slice(0, limit).map((row) => String(row.value)),
940
+ hasMore: rows.length > limit,
941
+ hasNull: n > 0
942
+ };
943
+ }, signal),
944
+ fetchRow: (identity, signal) => this.serial(async () => {
945
+ const source = this.projectedSource(Date.now());
946
+ const rows = await this.sql(
947
+ `SELECT * FROM (${source.sql}) r WHERE ${quoteIdentifier(this.schema.idField)} = $${source.args.length + 1} LIMIT 2`,
948
+ [...source.args, identity],
949
+ signal
950
+ );
951
+ if (rows.length > 1) throw Error("DuckDB identity must be unique.");
952
+ return rows[0] ? this.formatRow(rows[0], this.schema) : null;
953
+ }, signal),
954
+ fetchFieldStats: (fields, signal) => this.serial(async () => {
955
+ if (fields.length > 64)
956
+ throw Error("At most 64 field stats can be requested.");
957
+ const stats = {};
958
+ for (const name of fields) {
959
+ const builder = this.builder(), field = builder.definition(name), source = this.projectedSource(Date.now()), value = builder.field(name);
960
+ const rows = await this.sql(
961
+ field.type === "textarray" ? `SELECT COUNT(DISTINCT value) AS "distinct" FROM (SELECT unnest(${value}) AS value FROM (${source.sql}) r)` : `SELECT COUNT(DISTINCT ${value}) AS "distinct", MIN(${value}) AS min, MAX(${value}) AS max FROM (${source.sql}) r`,
962
+ source.args,
963
+ signal
964
+ );
965
+ const result = rows[0];
966
+ stats[name] = {
967
+ distinct: result.distinct,
968
+ ...typeof result.min === "number" || typeof result.min === "string" ? { min: result.min } : {},
969
+ ...typeof result.max === "number" || typeof result.max === "string" ? { max: result.max } : {}
970
+ };
971
+ }
972
+ return stats;
973
+ }, signal)
974
+ };
975
+ }
976
+ };
977
+ function createDuckDBTransport(options) {
978
+ return new DuckDBBackend(options).transport();
979
+ }
980
+
981
+ // src/wasm.ts
982
+ function fromDuckDBWasmConnection(connection) {
983
+ return {
984
+ async query(sql, args) {
985
+ if (!args.length)
986
+ return (await connection.query(sql)).toArray().map((row) => row.toJSON());
987
+ const statement = await connection.prepare(sql);
988
+ try {
989
+ return (await statement.query(...args)).toArray().map((row) => row.toJSON());
990
+ } finally {
991
+ await statement.close();
992
+ }
993
+ }
994
+ };
995
+ }
996
+
997
+ // src/schema.ts
998
+ import {
999
+ loadSchema
1000
+ } from "@pythia-software/query-table-core";
1001
+ function loadDuckDBSchema(document) {
1002
+ const schema = loadSchema(document);
1003
+ const raw = document;
1004
+ const bindings = /* @__PURE__ */ Object.create(null);
1005
+ for (const field of schema.fields) {
1006
+ if (field.source.kind !== "backend") continue;
1007
+ const binding = raw.fields.find((item) => item.name === field.name)?.bindings?.duckdb;
1008
+ if (!binding || typeof binding.expr !== "string" || !binding.expr.trim() || binding.expr.includes("\0"))
1009
+ throw Error(
1010
+ `Field ${field.name} requires an explicit bindings.duckdb.expr.`
1011
+ );
1012
+ bindings[field.name] = binding.expr;
1013
+ }
1014
+ return { schema, bindings };
1015
+ }
1016
+ export {
1017
+ DUCKDB_PROFILE,
1018
+ DuckDBBackend,
1019
+ SQLBuilder,
1020
+ compileDuckDBRows,
1021
+ createDuckDBTransport,
1022
+ fromDuckDBWasmConnection,
1023
+ loadDuckDBSchema,
1024
+ normalizeDuckDBValue,
1025
+ quoteIdentifier,
1026
+ quoteRelation
1027
+ };
1028
+ //# sourceMappingURL=index.js.map