@gscdump/engine-duckdb-wasm 0.32.11 → 0.33.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.mjs +92 -27
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -20,11 +20,16 @@ const METRIC_SQL = {
|
|
|
20
20
|
const DIM_COLUMN = {
|
|
21
21
|
page: "url",
|
|
22
22
|
query: "query",
|
|
23
|
-
queryCanonical: "query_canonical",
|
|
24
23
|
country: "country",
|
|
25
24
|
date: "date",
|
|
26
25
|
searchAppearance: "search_appearance"
|
|
27
26
|
};
|
|
27
|
+
function queryCanonicalExpr(queryRef = "query") {
|
|
28
|
+
return `COALESCE((SELECT qd.query_canonical FROM query_dim qd WHERE qd.query = ${queryRef} LIMIT 1), ${queryRef})`;
|
|
29
|
+
}
|
|
30
|
+
function dimExpr(dim) {
|
|
31
|
+
return dim === "queryCanonical" ? queryCanonicalExpr() : DIM_COLUMN[dim] ?? dim;
|
|
32
|
+
}
|
|
28
33
|
function tableForDimensions(dims) {
|
|
29
34
|
const set = new Set(dims.filter((d) => d !== "date"));
|
|
30
35
|
if (set.has("page") && (set.has("query") || set.has("queryCanonical"))) return "page_queries";
|
|
@@ -128,7 +133,7 @@ function facetPredicate(query) {
|
|
|
128
133
|
const parts = [];
|
|
129
134
|
const params = [];
|
|
130
135
|
for (const f of facets) {
|
|
131
|
-
const col =
|
|
136
|
+
const col = dimExpr(f.column);
|
|
132
137
|
switch (f.op) {
|
|
133
138
|
case "eq":
|
|
134
139
|
parts.push(`${col} = ?`);
|
|
@@ -163,7 +168,7 @@ function compileArchetypeSql(query) {
|
|
|
163
168
|
}
|
|
164
169
|
case "entity-daily-timeseries": {
|
|
165
170
|
const table = tableForDimensions([query.entity.dimension]);
|
|
166
|
-
const col =
|
|
171
|
+
const col = dimExpr(query.entity.dimension);
|
|
167
172
|
const where = rangePredicate(query);
|
|
168
173
|
return {
|
|
169
174
|
table,
|
|
@@ -173,7 +178,7 @@ function compileArchetypeSql(query) {
|
|
|
173
178
|
}
|
|
174
179
|
case "entity-daily-sparkline": {
|
|
175
180
|
const table = tableForDimensions([query.dimension]);
|
|
176
|
-
const col =
|
|
181
|
+
const col = dimExpr(query.dimension);
|
|
177
182
|
const where = rangePredicate(query);
|
|
178
183
|
if (query.entities.length === 0) throw new Error("[archetype-sql] entity-daily-sparkline requires resolved entities");
|
|
179
184
|
const placeholders = query.entities.map(() => "?").join(", ");
|
|
@@ -231,7 +236,7 @@ function compileArchetypeSql(query) {
|
|
|
231
236
|
params
|
|
232
237
|
};
|
|
233
238
|
}
|
|
234
|
-
const col =
|
|
239
|
+
const col = dimExpr(query.dimension);
|
|
235
240
|
const facet = facetPredicate(query);
|
|
236
241
|
const totalCol = query.includeTotal ? ", COUNT(*) OVER() AS __total" : "";
|
|
237
242
|
if (cmp) {
|
|
@@ -281,7 +286,7 @@ function compileArchetypeSql(query) {
|
|
|
281
286
|
const matchParts = [];
|
|
282
287
|
const matchParams = [];
|
|
283
288
|
for (const [dim, value] of Object.entries(query.match)) {
|
|
284
|
-
const col =
|
|
289
|
+
const col = dimExpr(dim);
|
|
285
290
|
if (!col) throw new Error(`[archetype-sql] single-row-lookup: unknown dimension ${dim}`);
|
|
286
291
|
matchParts.push(`${col} = ?`);
|
|
287
292
|
matchParams.push(value);
|
|
@@ -305,7 +310,7 @@ function compileArchetypeSql(query) {
|
|
|
305
310
|
...where.params
|
|
306
311
|
]
|
|
307
312
|
};
|
|
308
|
-
const col =
|
|
313
|
+
const col = dimExpr(query.seriesDimension);
|
|
309
314
|
return {
|
|
310
315
|
table,
|
|
311
316
|
sql: `SELECT date, ${col} AS ${query.seriesDimension}, ${metricExpr(query.metric)} AS ${query.metric} FROM ${table} WHERE ${where.sql} GROUP BY date, ${col} ORDER BY date, ${col}`,
|
|
@@ -525,13 +530,26 @@ function createOpfsHandleRegistry(backend) {
|
|
|
525
530
|
}
|
|
526
531
|
}
|
|
527
532
|
const viewRefs = /* @__PURE__ */ new Map();
|
|
528
|
-
function acquireView(key) {
|
|
529
|
-
|
|
533
|
+
function acquireView(key, signature) {
|
|
534
|
+
const existing = viewRefs.get(key);
|
|
535
|
+
if (existing) {
|
|
536
|
+
if (existing.signature !== signature) throw new Error(`view ${key} is already attached with a different signature`);
|
|
537
|
+
existing.refs++;
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
viewRefs.set(key, {
|
|
541
|
+
refs: 1,
|
|
542
|
+
signature
|
|
543
|
+
});
|
|
530
544
|
}
|
|
531
545
|
async function releaseView(key, drop) {
|
|
532
|
-
const
|
|
546
|
+
const existing = viewRefs.get(key);
|
|
547
|
+
const next = (existing?.refs ?? 0) - 1;
|
|
533
548
|
if (next > 0) {
|
|
534
|
-
viewRefs.set(key,
|
|
549
|
+
viewRefs.set(key, {
|
|
550
|
+
refs: next,
|
|
551
|
+
signature: existing?.signature
|
|
552
|
+
});
|
|
535
553
|
return;
|
|
536
554
|
}
|
|
537
555
|
viewRefs.delete(key);
|
|
@@ -544,7 +562,8 @@ function createOpfsHandleRegistry(backend) {
|
|
|
544
562
|
refs: (name) => entries.get(name)?.refs ?? 0,
|
|
545
563
|
acquireView,
|
|
546
564
|
releaseView,
|
|
547
|
-
viewRefs: (key) => viewRefs.get(key) ?? 0
|
|
565
|
+
viewRefs: (key) => viewRefs.get(key)?.refs ?? 0,
|
|
566
|
+
viewSignature: (key) => viewRefs.get(key)?.signature
|
|
548
567
|
};
|
|
549
568
|
}
|
|
550
569
|
function overlayViewBody(args) {
|
|
@@ -569,6 +588,7 @@ const OPFS_PREFIX = "gscdump-snapshot__";
|
|
|
569
588
|
const RECOVER_BUFFER_PREFIX = "gscdump-recover__";
|
|
570
589
|
const MAX_CONTENT_HASH_SLUG_CACHE = 4096;
|
|
571
590
|
const contentHashSlugCache = /* @__PURE__ */ new Map();
|
|
591
|
+
const IDENT_RE = /^[A-Z_][\w$]*$/i;
|
|
572
592
|
function nowMs() {
|
|
573
593
|
return globalThis.performance?.now?.() ?? Date.now();
|
|
574
594
|
}
|
|
@@ -603,6 +623,23 @@ function getOpfsRegistry(db, protocol) {
|
|
|
603
623
|
}
|
|
604
624
|
return registry;
|
|
605
625
|
}
|
|
626
|
+
const dbBufferRegistries = /* @__PURE__ */ new WeakMap();
|
|
627
|
+
function getBufferRegistry(db) {
|
|
628
|
+
let registry = dbBufferRegistries.get(db);
|
|
629
|
+
if (!registry) {
|
|
630
|
+
registry = createOpfsHandleRegistry({
|
|
631
|
+
register: (name, handle) => db.registerFileBuffer(name, handle),
|
|
632
|
+
drop: async (name) => {
|
|
633
|
+
await db.dropFile(name);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
dbBufferRegistries.set(db, registry);
|
|
637
|
+
}
|
|
638
|
+
return registry;
|
|
639
|
+
}
|
|
640
|
+
function assertSqlIdentifier(kind, value) {
|
|
641
|
+
if (!IDENT_RE.test(value)) throw new TypeError(`[engine-duckdb-wasm/opfs] invalid ${kind} identifier ${JSON.stringify(value)}`);
|
|
642
|
+
}
|
|
606
643
|
function isQuotaError(err) {
|
|
607
644
|
if (typeof err !== "object" || err === null) return false;
|
|
608
645
|
return err.name === "QuotaExceededError" || err.code === 22;
|
|
@@ -743,6 +780,28 @@ function readParquetViewWithOverlaySql(schema, table, lakeFiles, overlayFile) {
|
|
|
743
780
|
materializeLake: true
|
|
744
781
|
})}`;
|
|
745
782
|
}
|
|
783
|
+
function viewKey(schema, table) {
|
|
784
|
+
return `${schema}.${table}`;
|
|
785
|
+
}
|
|
786
|
+
function viewSignature(lakeFiles, overlayFile) {
|
|
787
|
+
return JSON.stringify({
|
|
788
|
+
lake: [...lakeFiles].sort(),
|
|
789
|
+
overlay: overlayFile ?? null
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
async function ensureView(registry, conn, schema, table, signature, sql) {
|
|
793
|
+
const key = viewKey(schema, table);
|
|
794
|
+
if (registry.viewSignature(key) === signature) {
|
|
795
|
+
registry.acquireView(key, signature);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (registry.viewRefs(key) > 0) throw new Error(`[engine-duckdb-wasm/opfs] view ${key} is already attached with a different fileset`);
|
|
799
|
+
await conn.query(sql);
|
|
800
|
+
registry.acquireView(key, signature);
|
|
801
|
+
}
|
|
802
|
+
async function recoveredBufferName(table, file, index) {
|
|
803
|
+
return `${RECOVER_BUFFER_PREFIX}${table}_${file.contentHash ? await contentHashSlug(file.contentHash) : String(index)}.parquet`;
|
|
804
|
+
}
|
|
746
805
|
async function runWithConcurrency$1(items, concurrency, fn) {
|
|
747
806
|
let next = 0;
|
|
748
807
|
let failed;
|
|
@@ -757,10 +816,13 @@ async function runWithConcurrency$1(items, concurrency, fn) {
|
|
|
757
816
|
}
|
|
758
817
|
}
|
|
759
818
|
}
|
|
760
|
-
await Promise.
|
|
819
|
+
const rejected = (await Promise.allSettled(Array.from({ length: Math.min(concurrency, items.length) }, worker))).find((r) => r.status === "rejected");
|
|
820
|
+
if (rejected) throw rejected.reason;
|
|
761
821
|
}
|
|
762
822
|
async function attachOpfsParquetTables(options) {
|
|
763
823
|
const { db, conn, tables, schema = "main", fetch: fetchImpl = globalThis.fetch.bind(globalThis), fetchInit, fetchConcurrency = DEFAULT_CONCURRENCY, signal, version, onFileProgress, onTiming, withDb = (fn) => fn(), recoverContention = true } = options;
|
|
824
|
+
assertSqlIdentifier("schema", schema);
|
|
825
|
+
for (const table of tables) assertSqlIdentifier("table", table.table);
|
|
764
826
|
const totalStarted = nowMs();
|
|
765
827
|
await timed(onTiming, "persist", {}, requestPersistentStorage);
|
|
766
828
|
const root = await timed(onTiming, "root", {}, getOpfsRoot);
|
|
@@ -799,6 +861,7 @@ async function attachOpfsParquetTables(options) {
|
|
|
799
861
|
const expectedCount = new Map(tables.map((t) => [t.table, t.files.length + (t.overlay ? 1 : 0)]));
|
|
800
862
|
const { DuckDBDataProtocol } = await timed(onTiming, "duckdb-import", {}, () => import("@duckdb/duckdb-wasm"));
|
|
801
863
|
const registry = getOpfsRegistry(db, DuckDBDataProtocol.BROWSER_FSACCESS);
|
|
864
|
+
const bufferRegistry = getBufferRegistry(db);
|
|
802
865
|
await timed(onTiming, "sweep", {
|
|
803
866
|
files: total,
|
|
804
867
|
tables: tables.length
|
|
@@ -915,20 +978,21 @@ async function attachOpfsParquetTables(options) {
|
|
|
915
978
|
continue;
|
|
916
979
|
}
|
|
917
980
|
const overlayName = overlayNames.get(t.table);
|
|
918
|
-
const lakeNames = overlayName ? files.map((f) => f.name).filter((n) => n !== overlayName) : files.map((f) => f.name);
|
|
981
|
+
const lakeNames = (overlayName ? files.map((f) => f.name).filter((n) => n !== overlayName) : files.map((f) => f.name)).sort();
|
|
982
|
+
const signature = viewSignature(lakeNames, overlayName);
|
|
919
983
|
try {
|
|
920
984
|
signal?.throwIfAborted();
|
|
921
985
|
await timed(onTiming, "view", {
|
|
922
986
|
table: t.table,
|
|
923
987
|
files: files.length
|
|
924
|
-
}, () => withDb(() => conn.
|
|
988
|
+
}, () => withDb(() => ensureView(registry, conn, schema, t.table, signature, overlayName ? readParquetViewWithOverlaySql(schema, t.table, lakeNames, overlayName) : readParquetViewSql$1(schema, t.table, lakeNames))));
|
|
925
989
|
} catch (err) {
|
|
926
990
|
if (isAbortError$1(err)) {
|
|
927
991
|
await withDb(() => detachOpfs(registry, conn, schema, attached, [...acquiredNames])).catch(() => {});
|
|
928
992
|
throw err;
|
|
929
993
|
}
|
|
930
994
|
if (isOpfsAccessHandleConflict(err)) {
|
|
931
|
-
if (registry.viewRefs(
|
|
995
|
+
if (registry.viewRefs(viewKey(schema, t.table)) === 0) await withDb(() => conn.query(`DROP VIEW IF EXISTS ${schema}.${t.table}`)).catch(() => {});
|
|
932
996
|
degraded.add(t.table);
|
|
933
997
|
degradeReason.set(t.table, "contention");
|
|
934
998
|
await releaseTable(t.table);
|
|
@@ -938,7 +1002,6 @@ async function attachOpfsParquetTables(options) {
|
|
|
938
1002
|
throw err;
|
|
939
1003
|
}
|
|
940
1004
|
attached.push(t.table);
|
|
941
|
-
registry.acquireView(`${schema}.${t.table}`);
|
|
942
1005
|
for (const f of files) registeredNames.push(f.name);
|
|
943
1006
|
}
|
|
944
1007
|
const recoverTableToBuffers = async (t) => {
|
|
@@ -959,28 +1022,30 @@ async function attachOpfsParquetTables(options) {
|
|
|
959
1022
|
};
|
|
960
1023
|
const lakeNames = [];
|
|
961
1024
|
for (let i = 0; i < t.files.length; i++) {
|
|
962
|
-
const
|
|
963
|
-
const name =
|
|
964
|
-
|
|
1025
|
+
const file = t.files[i];
|
|
1026
|
+
const name = await recoveredBufferName(t.table, file, i);
|
|
1027
|
+
const buf = await readOne(file, i);
|
|
1028
|
+
await withDb(() => bufferRegistry.acquire(name, () => buf));
|
|
965
1029
|
bufferFiles.push(name);
|
|
966
1030
|
lakeNames.push(name);
|
|
967
|
-
bytesAttached +=
|
|
1031
|
+
bytesAttached += file.bytes;
|
|
968
1032
|
}
|
|
969
1033
|
let overlayName;
|
|
970
1034
|
if (t.overlay) {
|
|
1035
|
+
overlayName = await recoveredBufferName(t.table, t.overlay, t.files.length);
|
|
971
1036
|
const buf = await readOne(t.overlay, t.files.length);
|
|
972
|
-
overlayName
|
|
973
|
-
await withDb(() => db.registerFileBuffer(overlayName, buf));
|
|
1037
|
+
await withDb(() => bufferRegistry.acquire(overlayName, () => buf));
|
|
974
1038
|
bufferFiles.push(overlayName);
|
|
975
1039
|
bytesAttached += t.overlay.bytes;
|
|
976
1040
|
}
|
|
1041
|
+
lakeNames.sort();
|
|
977
1042
|
const body = overlayViewBody({
|
|
978
1043
|
lakeSelect: lakeNames.length ? lakeSelect(lakeNames) : null,
|
|
979
1044
|
overlaySelect: overlayName ? `SELECT * REPLACE (CAST(date AS DATE) AS date) FROM read_parquet(['${overlayName.replace(/'/g, "''")}'], union_by_name = true)` : null,
|
|
980
1045
|
materializeLake: false
|
|
981
1046
|
});
|
|
982
1047
|
if (!body) return false;
|
|
983
|
-
await withDb(() => conn.
|
|
1048
|
+
await withDb(() => ensureView(registry, conn, schema, t.table, viewSignature(lakeNames, overlayName), `CREATE OR REPLACE VIEW ${schema}.${t.table} AS ${body}`));
|
|
984
1049
|
bufferViews.push(t.table);
|
|
985
1050
|
return true;
|
|
986
1051
|
});
|
|
@@ -990,7 +1055,7 @@ async function attachOpfsParquetTables(options) {
|
|
|
990
1055
|
const before = bufferFiles.length;
|
|
991
1056
|
if (await recoverTableToBuffers(t).catch(() => false)) degraded.delete(t.table);
|
|
992
1057
|
else {
|
|
993
|
-
|
|
1058
|
+
await withDb(() => bufferRegistry.release(bufferFiles.slice(before))).catch(() => {});
|
|
994
1059
|
bufferFiles.length = before;
|
|
995
1060
|
}
|
|
996
1061
|
}
|
|
@@ -1012,8 +1077,8 @@ async function attachOpfsParquetTables(options) {
|
|
|
1012
1077
|
if (detached) return;
|
|
1013
1078
|
detached = true;
|
|
1014
1079
|
await detachOpfs(registry, conn, schema, attached, registeredNames);
|
|
1015
|
-
for (const v of bufferViews) await conn.query(`DROP VIEW IF EXISTS ${schema}.${v}`).
|
|
1016
|
-
|
|
1080
|
+
for (const v of bufferViews) await registry.releaseView(viewKey(schema, v), () => conn.query(`DROP VIEW IF EXISTS ${schema}.${v}`).then(() => {}));
|
|
1081
|
+
await bufferRegistry.release(bufferFiles);
|
|
1017
1082
|
}
|
|
1018
1083
|
};
|
|
1019
1084
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/engine-duckdb-wasm",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.33.0",
|
|
5
5
|
"description": "DuckDB-WASM engine adapter for @gscdump/analysis — typed browser analytics against parquet via R2.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"drizzle-orm": "1.0.0-rc.3",
|
|
48
|
-
"
|
|
49
|
-
"@gscdump/engine": "0.
|
|
50
|
-
"gscdump": "0.
|
|
48
|
+
"gscdump": "0.33.0",
|
|
49
|
+
"@gscdump/engine": "0.33.0",
|
|
50
|
+
"@gscdump/contracts": "0.33.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@duckdb/duckdb-wasm": "^1.32.0",
|