@xenosystem/blocks 0.4.1 → 0.5.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/auth/index.js +2 -0
- package/dist/chunk-2KG3PWR4.js +17 -0
- package/dist/chunk-DJJGZ6U4.js +215 -0
- package/dist/data/duckdb.d.ts +123 -0
- package/dist/data/duckdb.js +142 -0
- package/dist/data/index.d.ts +3255 -0
- package/dist/data/index.js +7185 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/ops/index.js +2 -1
- package/dist/time/index.d.ts +2261 -0
- package/dist/time/index.js +4658 -0
- package/dist/transport-B1cdciP8.d.ts +787 -0
- package/dist/trust/index.js +2 -0
- package/dist/{xterm-R3GIKSHA.js → xterm-R3ZKBPPC.js} +1 -0
- package/package.json +23 -3
package/dist/auth/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __copyProps = (to, from, except, desc) => {
|
|
6
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
7
|
+
for (let key of __getOwnPropNames(from))
|
|
8
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
9
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
10
|
+
}
|
|
11
|
+
return to;
|
|
12
|
+
};
|
|
13
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
__reExport
|
|
17
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// src/data/connectors/executors/sqlBuilder.ts
|
|
2
|
+
import { isFilterGroup } from "@xenosystem/data-core";
|
|
3
|
+
function quoteIdent(name) {
|
|
4
|
+
return `"${String(name).replace(/"/g, '""')}"`;
|
|
5
|
+
}
|
|
6
|
+
function quoteLiteral(value) {
|
|
7
|
+
if (value === null || value === void 0) return "NULL";
|
|
8
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
9
|
+
if (typeof value === "number") {
|
|
10
|
+
return Number.isFinite(value) ? String(value) : "NULL";
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(value)) return `(${value.map((v) => quoteLiteral(v)).join(", ")})`;
|
|
13
|
+
if (typeof value === "object") return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
|
14
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
15
|
+
}
|
|
16
|
+
function isSafeTableName(name) {
|
|
17
|
+
return /^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(name);
|
|
18
|
+
}
|
|
19
|
+
function buildSelect(options) {
|
|
20
|
+
const { table, query, limit } = options;
|
|
21
|
+
if (!isSafeTableName(table)) return { error: `"${table}" is not a usable table name.` };
|
|
22
|
+
const known = options.columns ? new Set(options.columns) : null;
|
|
23
|
+
const unknown = (id) => known !== null && !known.has(id);
|
|
24
|
+
const groupBy = query.groupBy ?? [];
|
|
25
|
+
const aggregates = query.aggregates ?? [];
|
|
26
|
+
const projection = [];
|
|
27
|
+
if (groupBy.length > 0 || aggregates.length > 0) {
|
|
28
|
+
for (const level of groupBy) {
|
|
29
|
+
if (unknown(level.columnId)) return { error: `There is no column called "${level.columnId}".` };
|
|
30
|
+
projection.push(quoteIdent(level.columnId));
|
|
31
|
+
}
|
|
32
|
+
for (const aggregate of aggregates) {
|
|
33
|
+
const rendered = renderAggregate(aggregate, unknown);
|
|
34
|
+
if ("error" in rendered) return rendered;
|
|
35
|
+
projection.push(`${rendered.sql} AS ${quoteIdent(aggregate.id)}`);
|
|
36
|
+
}
|
|
37
|
+
} else if (query.select && query.select.length > 0) {
|
|
38
|
+
for (const columnId of query.select) {
|
|
39
|
+
if (unknown(columnId)) return { error: `There is no column called "${columnId}".` };
|
|
40
|
+
projection.push(quoteIdent(columnId));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const parts = [`SELECT ${projection.length > 0 ? projection.join(", ") : "*"}`];
|
|
44
|
+
parts.push(`FROM ${quoteIdent(table)}`);
|
|
45
|
+
if (query.filters) {
|
|
46
|
+
const where = renderFilterGroup(query.filters, unknown);
|
|
47
|
+
if ("error" in where) return where;
|
|
48
|
+
if (where.sql !== "") parts.push(`WHERE ${where.sql}`);
|
|
49
|
+
}
|
|
50
|
+
if (groupBy.length > 0) {
|
|
51
|
+
parts.push(`GROUP BY ${groupBy.map((level) => quoteIdent(level.columnId)).join(", ")}`);
|
|
52
|
+
}
|
|
53
|
+
if (query.sorts && query.sorts.length > 0) {
|
|
54
|
+
const terms = [];
|
|
55
|
+
for (const sort of query.sorts) {
|
|
56
|
+
const isAlias = aggregates.some((a) => a.id === sort.columnId);
|
|
57
|
+
if (!isAlias && unknown(sort.columnId)) {
|
|
58
|
+
return { error: `There is no column called "${sort.columnId}".` };
|
|
59
|
+
}
|
|
60
|
+
const direction = sort.direction === "desc" ? "DESC" : "ASC";
|
|
61
|
+
const nulls = sort.nulls === "first" ? "NULLS FIRST" : "NULLS LAST";
|
|
62
|
+
terms.push(`${quoteIdent(sort.columnId)} ${direction} ${nulls}`);
|
|
63
|
+
}
|
|
64
|
+
parts.push(`ORDER BY ${terms.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
parts.push(`LIMIT ${Math.max(0, Math.floor(limit))}`);
|
|
67
|
+
if (options.offset && options.offset > 0) parts.push(`OFFSET ${Math.floor(options.offset)}`);
|
|
68
|
+
return { sql: parts.join(" ") };
|
|
69
|
+
}
|
|
70
|
+
function renderAggregate(aggregate, unknown) {
|
|
71
|
+
const column = quoteIdent(aggregate.columnId);
|
|
72
|
+
if (aggregate.fn !== "count" && unknown(aggregate.columnId)) {
|
|
73
|
+
return { error: `There is no column called "${aggregate.columnId}".` };
|
|
74
|
+
}
|
|
75
|
+
switch (aggregate.fn) {
|
|
76
|
+
case "count":
|
|
77
|
+
return { sql: "count(*)" };
|
|
78
|
+
case "countDistinct":
|
|
79
|
+
return { sql: `count(DISTINCT ${column})` };
|
|
80
|
+
case "countNumbers":
|
|
81
|
+
return { sql: `count(TRY_CAST(${column} AS DOUBLE))` };
|
|
82
|
+
case "sum":
|
|
83
|
+
return { sql: `sum(${column})` };
|
|
84
|
+
case "avg":
|
|
85
|
+
return { sql: `avg(${column})` };
|
|
86
|
+
case "min":
|
|
87
|
+
return { sql: `min(${column})` };
|
|
88
|
+
case "max":
|
|
89
|
+
return { sql: `max(${column})` };
|
|
90
|
+
case "median":
|
|
91
|
+
case "p50":
|
|
92
|
+
return { sql: `median(${column})` };
|
|
93
|
+
case "p90":
|
|
94
|
+
return { sql: `quantile_cont(${column}, 0.90)` };
|
|
95
|
+
case "p95":
|
|
96
|
+
return { sql: `quantile_cont(${column}, 0.95)` };
|
|
97
|
+
case "p99":
|
|
98
|
+
return { sql: `quantile_cont(${column}, 0.99)` };
|
|
99
|
+
case "stdev":
|
|
100
|
+
return { sql: `stddev_samp(${column})` };
|
|
101
|
+
case "var":
|
|
102
|
+
return { sql: `var_samp(${column})` };
|
|
103
|
+
case "product":
|
|
104
|
+
return { sql: `product(${column})` };
|
|
105
|
+
case "first":
|
|
106
|
+
return { sql: `first(${column})` };
|
|
107
|
+
case "last":
|
|
108
|
+
return { sql: `last(${column})` };
|
|
109
|
+
default:
|
|
110
|
+
return { error: `The aggregate "${String(aggregate.fn)}" is not supported by this source.` };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function renderFilterGroup(group, unknown = () => false) {
|
|
114
|
+
const parts = [];
|
|
115
|
+
for (const member of group.filters) {
|
|
116
|
+
const rendered = isFilterGroup(member) ? renderFilterGroup(member, unknown) : renderFilter(member, unknown);
|
|
117
|
+
if ("error" in rendered) return rendered;
|
|
118
|
+
if (rendered.sql !== "") parts.push(`(${rendered.sql})`);
|
|
119
|
+
}
|
|
120
|
+
if (parts.length === 0) return { sql: "" };
|
|
121
|
+
const joined = parts.join(group.combinator === "or" ? " OR " : " AND ");
|
|
122
|
+
return { sql: group.negate ? `NOT (${joined})` : joined };
|
|
123
|
+
}
|
|
124
|
+
function renderFilter(filter, unknown) {
|
|
125
|
+
if (unknown(filter.columnId)) return { error: `There is no column called "${filter.columnId}".` };
|
|
126
|
+
const column = quoteIdent(filter.columnId);
|
|
127
|
+
const insensitive = filter.caseInsensitive !== false;
|
|
128
|
+
const text = insensitive ? `lower(CAST(${column} AS VARCHAR))` : `CAST(${column} AS VARCHAR)`;
|
|
129
|
+
const like = (pattern) => `${text} LIKE ${quoteLiteral(insensitive ? pattern.toLowerCase() : pattern)}`;
|
|
130
|
+
const escaped = escapeLikePattern(String(filter.value ?? ""));
|
|
131
|
+
switch (filter.op) {
|
|
132
|
+
case "eq":
|
|
133
|
+
return { sql: filter.value === null ? `${column} IS NULL` : `${column} = ${quoteLiteral(filter.value)}` };
|
|
134
|
+
case "ne":
|
|
135
|
+
return { sql: filter.value === null ? `${column} IS NOT NULL` : `${column} IS DISTINCT FROM ${quoteLiteral(filter.value)}` };
|
|
136
|
+
case "gt":
|
|
137
|
+
return { sql: `${column} > ${quoteLiteral(filter.value)}` };
|
|
138
|
+
case "gte":
|
|
139
|
+
return { sql: `${column} >= ${quoteLiteral(filter.value)}` };
|
|
140
|
+
case "lt":
|
|
141
|
+
return { sql: `${column} < ${quoteLiteral(filter.value)}` };
|
|
142
|
+
case "lte":
|
|
143
|
+
return { sql: `${column} <= ${quoteLiteral(filter.value)}` };
|
|
144
|
+
case "contains":
|
|
145
|
+
return { sql: `${like(`%${escaped}%`)} ESCAPE '\\'` };
|
|
146
|
+
case "notContains":
|
|
147
|
+
return { sql: `NOT (${like(`%${escaped}%`)} ESCAPE '\\')` };
|
|
148
|
+
case "startsWith":
|
|
149
|
+
return { sql: `${like(`${escaped}%`)} ESCAPE '\\'` };
|
|
150
|
+
case "endsWith":
|
|
151
|
+
return { sql: `${like(`%${escaped}`)} ESCAPE '\\'` };
|
|
152
|
+
case "in":
|
|
153
|
+
case "notIn": {
|
|
154
|
+
const values = filter.values ?? [];
|
|
155
|
+
if (values.length === 0) return { sql: filter.op === "in" ? "FALSE" : "TRUE" };
|
|
156
|
+
const list = values.map((v) => quoteLiteral(v)).join(", ");
|
|
157
|
+
return { sql: `${column} ${filter.op === "in" ? "IN" : "NOT IN"} (${list})` };
|
|
158
|
+
}
|
|
159
|
+
case "between": {
|
|
160
|
+
const [low, high] = filter.values ?? [];
|
|
161
|
+
if (low === void 0 || high === void 0) {
|
|
162
|
+
return { error: `The "between" filter on "${filter.columnId}" needs two values.` };
|
|
163
|
+
}
|
|
164
|
+
return { sql: `${column} BETWEEN ${quoteLiteral(low)} AND ${quoteLiteral(high)}` };
|
|
165
|
+
}
|
|
166
|
+
case "isEmpty":
|
|
167
|
+
return { sql: `(${column} IS NULL OR CAST(${column} AS VARCHAR) = '')` };
|
|
168
|
+
case "isNotEmpty":
|
|
169
|
+
return { sql: `(${column} IS NOT NULL AND CAST(${column} AS VARCHAR) <> '')` };
|
|
170
|
+
case "relative": {
|
|
171
|
+
const n = Number(filter.n);
|
|
172
|
+
if (!Number.isFinite(n)) return { error: `The relative filter on "${filter.columnId}" has no window.` };
|
|
173
|
+
const unit = relativeUnit(filter.unit);
|
|
174
|
+
if (!unit) return { error: `The time unit "${String(filter.unit)}" is not supported by this source.` };
|
|
175
|
+
const sign = n < 0 ? "-" : "+";
|
|
176
|
+
const magnitude = Math.abs(Math.floor(n));
|
|
177
|
+
const bound = `now() ${sign} INTERVAL ${magnitude} ${unit}`;
|
|
178
|
+
return { sql: n < 0 ? `${column} BETWEEN ${bound} AND now()` : `${column} BETWEEN now() AND ${bound}` };
|
|
179
|
+
}
|
|
180
|
+
default:
|
|
181
|
+
return { error: `The filter "${String(filter.op)}" is not supported by this source.` };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function relativeUnit(unit) {
|
|
185
|
+
switch (unit) {
|
|
186
|
+
case "minute":
|
|
187
|
+
return "MINUTE";
|
|
188
|
+
case "hour":
|
|
189
|
+
return "HOUR";
|
|
190
|
+
case "day":
|
|
191
|
+
return "DAY";
|
|
192
|
+
case "week":
|
|
193
|
+
return "WEEK";
|
|
194
|
+
case "month":
|
|
195
|
+
return "MONTH";
|
|
196
|
+
case "quarter":
|
|
197
|
+
return "QUARTER";
|
|
198
|
+
case "year":
|
|
199
|
+
return "YEAR";
|
|
200
|
+
default:
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function escapeLikePattern(text) {
|
|
205
|
+
return text.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export {
|
|
209
|
+
quoteIdent,
|
|
210
|
+
quoteLiteral,
|
|
211
|
+
isSafeTableName,
|
|
212
|
+
buildSelect,
|
|
213
|
+
renderFilterGroup,
|
|
214
|
+
escapeLikePattern
|
|
215
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { X as XenoSqlEngine } from '../transport-B1cdciP8.js';
|
|
2
|
+
import '@xenosystem/data-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The DuckDB-WASM adapter — the ONLY file in this package that knows DuckDB exists.
|
|
6
|
+
*
|
|
7
|
+
* Ships on the `./duckdb` subpath, imports its engine through `await import(...)`, and declares
|
|
8
|
+
* `@duckdb/duckdb-wasm` as an **optional peer**. A consumer that never opens a SQL source installs
|
|
9
|
+
* nothing and pays nothing; a host that already owns a DuckDB instance implements the three-method
|
|
10
|
+
* {@link XenoSqlEngine} seam against its own and ignores this file entirely.
|
|
11
|
+
*
|
|
12
|
+
* The measured cost of getting that wrong: `duckdb-eh.wasm` is **34 MB raw / ~7.1 MB brotli**, plus
|
|
13
|
+
* a 0.74 MB worker. A static import would put that on every consumer of `@xenosystem/blocks/data`,
|
|
14
|
+
* including the twenty panels that never run SQL. The lucide incident already taught this package
|
|
15
|
+
* that lesson once.
|
|
16
|
+
*
|
|
17
|
+
* ## Four traps, each closed deliberately
|
|
18
|
+
*
|
|
19
|
+
* Every one of these is a documented, currently-open failure mode. They are why this adapter is
|
|
20
|
+
* ~200 lines rather than ~40.
|
|
21
|
+
*
|
|
22
|
+
* 1. **Never `insertArrowTable(tableFromJSON(rows))`.** Arrow's `tableFromJSON` infers
|
|
23
|
+
* `Dictionary<Utf8, Int32>` for string columns, and duckdb-wasm's IPC decoder does not accept
|
|
24
|
+
* dictionary-encoded streams. The failure is *silent*: no error, no rows, and then
|
|
25
|
+
* `Catalog Error: Table with name X does not exist` on the next query. Compounding it,
|
|
26
|
+
* `insertArrowTable` gates on `input instanceof Table`, which is `false` whenever the bundler
|
|
27
|
+
* resolves two copies of `apache-arrow` — and duckdb-wasm pins `^17` while npm serves 21.x.
|
|
28
|
+
* This adapter uses the JSON path instead, which has neither problem.
|
|
29
|
+
*
|
|
30
|
+
* 2. **Never `read_json_auto`.** DuckDB-WASM builds `json`, `parquet`, `icu` and `autocomplete` as
|
|
31
|
+
* `DONT_LINK`, so they are **autoloaded over the network** from `extensions.duckdb.org` at first
|
|
32
|
+
* use. In a sandbox, an offline app or a web export that is a hang or a CORS error — in the one
|
|
33
|
+
* code path whose entire selling point is that it needs no network.
|
|
34
|
+
* `insertJSONFromPath` is implemented in duckdb-wasm's own C++ against Arrow's JSON reader and
|
|
35
|
+
* touches no extension, so it works with zero network. Autoloading is then turned **off**, so a
|
|
36
|
+
* stray `read_parquet` fails loudly instead of hanging.
|
|
37
|
+
*
|
|
38
|
+
* 3. **`StructRowProxy.toJSON()` is shallow.** Nested structs stay proxies and list columns stay
|
|
39
|
+
* Arrow `Vector`s, so a naive `.toJSON()` hands the panel objects it cannot render and
|
|
40
|
+
* `JSON.stringify` cannot serialize. {@link unwrapArrowValue} descends.
|
|
41
|
+
*
|
|
42
|
+
* 4. **BIGINT arrives as `bigint`.** `count(*)` is BIGINT, so the single most common aggregate in
|
|
43
|
+
* existence makes `JSON.stringify` throw `Do not know how to serialize a BigInt`. DECIMAL arrives
|
|
44
|
+
* as a raw `Uint32Array` of 128-bit limbs; timestamps as epoch numbers, not `Date`. The config
|
|
45
|
+
* casts what it can and {@link unwrapArrowValue} handles the rest.
|
|
46
|
+
*
|
|
47
|
+
* @module
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The bundle assets, which the **host** must provide.
|
|
52
|
+
*
|
|
53
|
+
* There is deliberately no jsDelivr default. `getJsDelivrBundles()` would make the "zero
|
|
54
|
+
* capabilities, works offline, survives a web export" promise false the first time it ran, by
|
|
55
|
+
* fetching 34 MB from a third-party CDN. A host that wants the CDN can pass those URLs explicitly
|
|
56
|
+
* and will have made that choice on purpose.
|
|
57
|
+
*/
|
|
58
|
+
interface DuckDbBundle {
|
|
59
|
+
/** URL of `duckdb-eh.wasm` (or `duckdb-mvp.wasm`). */
|
|
60
|
+
mainModule: string;
|
|
61
|
+
/** URL of the matching worker script. */
|
|
62
|
+
mainWorker: string;
|
|
63
|
+
/** Only for the cross-origin-isolated bundle. */
|
|
64
|
+
pthreadWorker?: string | null;
|
|
65
|
+
}
|
|
66
|
+
/** Options for {@link createDuckDbEngine}. */
|
|
67
|
+
interface CreateDuckDbEngineOptions {
|
|
68
|
+
/** Where the wasm and worker live. Host-supplied — see {@link DuckDbBundle}. */
|
|
69
|
+
bundle: DuckDbBundle;
|
|
70
|
+
/**
|
|
71
|
+
* Pre-built module, for a host that already imported or vendored duckdb-wasm.
|
|
72
|
+
*
|
|
73
|
+
* Supplying this skips the dynamic import entirely, which is how an Electron host that bundles
|
|
74
|
+
* the package avoids a second copy.
|
|
75
|
+
*/
|
|
76
|
+
module?: unknown;
|
|
77
|
+
/** A ready `AsyncDuckDB`. Supplying it skips instantiation altogether. */
|
|
78
|
+
db?: unknown;
|
|
79
|
+
/** Log query text. Off by default — a raw statement can embed user data. */
|
|
80
|
+
debug?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build an {@link XenoSqlEngine} backed by DuckDB-WASM.
|
|
84
|
+
*
|
|
85
|
+
* @param options - The bundle URLs, or a pre-built instance.
|
|
86
|
+
* @returns The engine seam, ready to hand to `createConnectorsPanel({ transport: { sql } })`.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const sql = await createDuckDbEngine({
|
|
91
|
+
* bundle: { mainModule: wasmUrl, mainWorker: workerUrl },
|
|
92
|
+
* })
|
|
93
|
+
* const panel = createConnectorsPanel({ transport: { fetch: browserFetch(), sql } })
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function createDuckDbEngine(options: CreateDuckDbEngineOptions): Promise<XenoSqlEngine>;
|
|
97
|
+
/**
|
|
98
|
+
* Convert one Arrow row proxy into a plain object.
|
|
99
|
+
*
|
|
100
|
+
* @param row - A `StructRowProxy` from `Table.toArray()`.
|
|
101
|
+
* @returns A plain, serializable object.
|
|
102
|
+
*/
|
|
103
|
+
declare function unwrapArrowRow(row: unknown): Record<string, unknown>;
|
|
104
|
+
/**
|
|
105
|
+
* Recursively convert an Arrow value into something plain and serializable.
|
|
106
|
+
*
|
|
107
|
+
* TRAP 3 and the tail of TRAP 4 both live here. `toJSON()` on a row is **shallow**: a nested struct
|
|
108
|
+
* comes back as another proxy and a list column as an Arrow `Vector`, neither of which a panel can
|
|
109
|
+
* render or a `.xapp` can hold. And a `bigint` that survived the config cast — DuckDB's HUGEINT, or
|
|
110
|
+
* any build where the cast flag was ignored — would make `JSON.stringify` throw outright.
|
|
111
|
+
*
|
|
112
|
+
* @param value - Any value out of an Arrow row.
|
|
113
|
+
* @returns A plain value.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* unwrapArrowValue(123n) // → 123
|
|
118
|
+
* unwrapArrowValue(9007199254740993n) // → '9007199254740993' (precision kept, as text)
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
declare function unwrapArrowValue(value: unknown): unknown;
|
|
122
|
+
|
|
123
|
+
export { type CreateDuckDbEngineOptions, type DuckDbBundle, createDuckDbEngine, unwrapArrowRow, unwrapArrowValue };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSafeTableName
|
|
3
|
+
} from "../chunk-DJJGZ6U4.js";
|
|
4
|
+
import "../chunk-2KG3PWR4.js";
|
|
5
|
+
|
|
6
|
+
// src/data/connectors/duckdb.ts
|
|
7
|
+
async function createDuckDbEngine(options) {
|
|
8
|
+
const duckdb = options.module ?? await importDuckDb();
|
|
9
|
+
let db = options.db;
|
|
10
|
+
let worker = null;
|
|
11
|
+
if (!db) {
|
|
12
|
+
worker = new Worker(options.bundle.mainWorker);
|
|
13
|
+
const Logger = options.debug ? duckdb.ConsoleLogger : duckdb.VoidLogger ?? duckdb.ConsoleLogger;
|
|
14
|
+
db = new duckdb.AsyncDuckDB(new Logger(), worker);
|
|
15
|
+
await db.instantiate(options.bundle.mainModule, options.bundle.pthreadWorker ?? null);
|
|
16
|
+
}
|
|
17
|
+
await db.open({
|
|
18
|
+
query: {
|
|
19
|
+
// TRAP 4: without these, `count(*)` returns a `bigint` that `JSON.stringify` refuses, and a
|
|
20
|
+
// DECIMAL returns a raw Uint32Array of 128-bit limbs.
|
|
21
|
+
castBigIntToDouble: true,
|
|
22
|
+
castDecimalToDouble: true
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const connection = await db.connect();
|
|
26
|
+
await lockDown(connection);
|
|
27
|
+
const registered = /* @__PURE__ */ new Set();
|
|
28
|
+
return {
|
|
29
|
+
async register(tables) {
|
|
30
|
+
for (const table of tables) {
|
|
31
|
+
if (!isSafeTableName(table.name)) {
|
|
32
|
+
throw new Error(`"${table.name}" is not a usable table name.`);
|
|
33
|
+
}
|
|
34
|
+
const fileName = `${table.name}.json`;
|
|
35
|
+
await db.registerFileText(fileName, JSON.stringify(table.rows ?? []));
|
|
36
|
+
try {
|
|
37
|
+
await connection.insertJSONFromPath(fileName, {
|
|
38
|
+
schema: "main",
|
|
39
|
+
name: table.name,
|
|
40
|
+
// ROW_ARRAY: we know the shape, so inference is skipped.
|
|
41
|
+
shape: "row-array",
|
|
42
|
+
// Replace rather than append: a refresh must not double the rows.
|
|
43
|
+
create: true
|
|
44
|
+
});
|
|
45
|
+
registered.add(table.name);
|
|
46
|
+
} finally {
|
|
47
|
+
await db.dropFile(fileName).catch(() => {
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async query(sql) {
|
|
53
|
+
const table = await connection.query(sql);
|
|
54
|
+
return table.toArray().map((row) => unwrapArrowRow(row));
|
|
55
|
+
},
|
|
56
|
+
async dispose() {
|
|
57
|
+
for (const name of registered) {
|
|
58
|
+
await connection.query(`DROP TABLE IF EXISTS ${JSON.stringify(name)}`).catch(() => {
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
registered.clear();
|
|
62
|
+
await connection.close().catch(() => {
|
|
63
|
+
});
|
|
64
|
+
if (!options.db) await db?.terminate().catch(() => {
|
|
65
|
+
});
|
|
66
|
+
worker?.terminate();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function importDuckDb() {
|
|
71
|
+
try {
|
|
72
|
+
const specifier = "@duckdb/duckdb-wasm";
|
|
73
|
+
return await import(
|
|
74
|
+
/* @vite-ignore */
|
|
75
|
+
/* webpackIgnore: true */
|
|
76
|
+
specifier
|
|
77
|
+
);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"In-memory SQL needs @duckdb/duckdb-wasm, which is not installed. Install it, or supply a SQL engine through the transport seam. " + (error instanceof Error ? error.message : "")
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function lockDown(connection) {
|
|
85
|
+
const statements = [
|
|
86
|
+
"SET enable_external_access = false",
|
|
87
|
+
"SET autoinstall_known_extensions = false",
|
|
88
|
+
"SET autoload_known_extensions = false",
|
|
89
|
+
"SET allow_community_extensions = false",
|
|
90
|
+
// LAST. Anything after this cannot be changed.
|
|
91
|
+
"SET lock_configuration = true"
|
|
92
|
+
];
|
|
93
|
+
for (const statement of statements) {
|
|
94
|
+
await connection.query(statement).catch(() => {
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function unwrapArrowRow(row) {
|
|
99
|
+
if (row === null || row === void 0) return {};
|
|
100
|
+
const shallow = typeof row.toJSON === "function" ? row.toJSON() : row;
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const [key, value] of Object.entries(shallow)) out[key] = unwrapArrowValue(value);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function unwrapArrowValue(value) {
|
|
106
|
+
if (value === null || value === void 0) return null;
|
|
107
|
+
if (typeof value === "bigint") {
|
|
108
|
+
return value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
|
|
109
|
+
}
|
|
110
|
+
if (typeof value !== "object") return value;
|
|
111
|
+
if (value instanceof Date) return value.toISOString();
|
|
112
|
+
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
|
|
113
|
+
return Array.from(value, (item) => unwrapArrowValue(item));
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(value)) return value.map((item) => unwrapArrowValue(item));
|
|
116
|
+
const vector = value;
|
|
117
|
+
if (typeof vector.toArray === "function") {
|
|
118
|
+
return vector.toArray().map((item) => unwrapArrowValue(item));
|
|
119
|
+
}
|
|
120
|
+
const nested = value;
|
|
121
|
+
if (typeof nested.toJSON === "function") {
|
|
122
|
+
const plain = nested.toJSON();
|
|
123
|
+
if (plain !== null && typeof plain === "object" && !Array.isArray(plain)) {
|
|
124
|
+
const out2 = {};
|
|
125
|
+
for (const [key, item] of Object.entries(plain)) {
|
|
126
|
+
out2[key] = unwrapArrowValue(item);
|
|
127
|
+
}
|
|
128
|
+
return out2;
|
|
129
|
+
}
|
|
130
|
+
return plain;
|
|
131
|
+
}
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const [key, item] of Object.entries(value)) {
|
|
134
|
+
out[key] = unwrapArrowValue(item);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
createDuckDbEngine,
|
|
140
|
+
unwrapArrowRow,
|
|
141
|
+
unwrapArrowValue
|
|
142
|
+
};
|