@gscdump/engine-duckdb-wasm 0.32.11 → 0.32.12
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 +80 -20
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -525,13 +525,26 @@ function createOpfsHandleRegistry(backend) {
|
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
527
|
const viewRefs = /* @__PURE__ */ new Map();
|
|
528
|
-
function acquireView(key) {
|
|
529
|
-
|
|
528
|
+
function acquireView(key, signature) {
|
|
529
|
+
const existing = viewRefs.get(key);
|
|
530
|
+
if (existing) {
|
|
531
|
+
if (existing.signature !== signature) throw new Error(`view ${key} is already attached with a different signature`);
|
|
532
|
+
existing.refs++;
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
viewRefs.set(key, {
|
|
536
|
+
refs: 1,
|
|
537
|
+
signature
|
|
538
|
+
});
|
|
530
539
|
}
|
|
531
540
|
async function releaseView(key, drop) {
|
|
532
|
-
const
|
|
541
|
+
const existing = viewRefs.get(key);
|
|
542
|
+
const next = (existing?.refs ?? 0) - 1;
|
|
533
543
|
if (next > 0) {
|
|
534
|
-
viewRefs.set(key,
|
|
544
|
+
viewRefs.set(key, {
|
|
545
|
+
refs: next,
|
|
546
|
+
signature: existing?.signature
|
|
547
|
+
});
|
|
535
548
|
return;
|
|
536
549
|
}
|
|
537
550
|
viewRefs.delete(key);
|
|
@@ -544,7 +557,8 @@ function createOpfsHandleRegistry(backend) {
|
|
|
544
557
|
refs: (name) => entries.get(name)?.refs ?? 0,
|
|
545
558
|
acquireView,
|
|
546
559
|
releaseView,
|
|
547
|
-
viewRefs: (key) => viewRefs.get(key) ?? 0
|
|
560
|
+
viewRefs: (key) => viewRefs.get(key)?.refs ?? 0,
|
|
561
|
+
viewSignature: (key) => viewRefs.get(key)?.signature
|
|
548
562
|
};
|
|
549
563
|
}
|
|
550
564
|
function overlayViewBody(args) {
|
|
@@ -569,6 +583,7 @@ const OPFS_PREFIX = "gscdump-snapshot__";
|
|
|
569
583
|
const RECOVER_BUFFER_PREFIX = "gscdump-recover__";
|
|
570
584
|
const MAX_CONTENT_HASH_SLUG_CACHE = 4096;
|
|
571
585
|
const contentHashSlugCache = /* @__PURE__ */ new Map();
|
|
586
|
+
const IDENT_RE = /^[A-Z_][\w$]*$/i;
|
|
572
587
|
function nowMs() {
|
|
573
588
|
return globalThis.performance?.now?.() ?? Date.now();
|
|
574
589
|
}
|
|
@@ -603,6 +618,23 @@ function getOpfsRegistry(db, protocol) {
|
|
|
603
618
|
}
|
|
604
619
|
return registry;
|
|
605
620
|
}
|
|
621
|
+
const dbBufferRegistries = /* @__PURE__ */ new WeakMap();
|
|
622
|
+
function getBufferRegistry(db) {
|
|
623
|
+
let registry = dbBufferRegistries.get(db);
|
|
624
|
+
if (!registry) {
|
|
625
|
+
registry = createOpfsHandleRegistry({
|
|
626
|
+
register: (name, handle) => db.registerFileBuffer(name, handle),
|
|
627
|
+
drop: async (name) => {
|
|
628
|
+
await db.dropFile(name);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
dbBufferRegistries.set(db, registry);
|
|
632
|
+
}
|
|
633
|
+
return registry;
|
|
634
|
+
}
|
|
635
|
+
function assertSqlIdentifier(kind, value) {
|
|
636
|
+
if (!IDENT_RE.test(value)) throw new TypeError(`[engine-duckdb-wasm/opfs] invalid ${kind} identifier ${JSON.stringify(value)}`);
|
|
637
|
+
}
|
|
606
638
|
function isQuotaError(err) {
|
|
607
639
|
if (typeof err !== "object" || err === null) return false;
|
|
608
640
|
return err.name === "QuotaExceededError" || err.code === 22;
|
|
@@ -743,6 +775,28 @@ function readParquetViewWithOverlaySql(schema, table, lakeFiles, overlayFile) {
|
|
|
743
775
|
materializeLake: true
|
|
744
776
|
})}`;
|
|
745
777
|
}
|
|
778
|
+
function viewKey(schema, table) {
|
|
779
|
+
return `${schema}.${table}`;
|
|
780
|
+
}
|
|
781
|
+
function viewSignature(lakeFiles, overlayFile) {
|
|
782
|
+
return JSON.stringify({
|
|
783
|
+
lake: [...lakeFiles].sort(),
|
|
784
|
+
overlay: overlayFile ?? null
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async function ensureView(registry, conn, schema, table, signature, sql) {
|
|
788
|
+
const key = viewKey(schema, table);
|
|
789
|
+
if (registry.viewSignature(key) === signature) {
|
|
790
|
+
registry.acquireView(key, signature);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (registry.viewRefs(key) > 0) throw new Error(`[engine-duckdb-wasm/opfs] view ${key} is already attached with a different fileset`);
|
|
794
|
+
await conn.query(sql);
|
|
795
|
+
registry.acquireView(key, signature);
|
|
796
|
+
}
|
|
797
|
+
async function recoveredBufferName(table, file, index) {
|
|
798
|
+
return `${RECOVER_BUFFER_PREFIX}${table}_${file.contentHash ? await contentHashSlug(file.contentHash) : String(index)}.parquet`;
|
|
799
|
+
}
|
|
746
800
|
async function runWithConcurrency$1(items, concurrency, fn) {
|
|
747
801
|
let next = 0;
|
|
748
802
|
let failed;
|
|
@@ -757,10 +811,13 @@ async function runWithConcurrency$1(items, concurrency, fn) {
|
|
|
757
811
|
}
|
|
758
812
|
}
|
|
759
813
|
}
|
|
760
|
-
await Promise.
|
|
814
|
+
const rejected = (await Promise.allSettled(Array.from({ length: Math.min(concurrency, items.length) }, worker))).find((r) => r.status === "rejected");
|
|
815
|
+
if (rejected) throw rejected.reason;
|
|
761
816
|
}
|
|
762
817
|
async function attachOpfsParquetTables(options) {
|
|
763
818
|
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;
|
|
819
|
+
assertSqlIdentifier("schema", schema);
|
|
820
|
+
for (const table of tables) assertSqlIdentifier("table", table.table);
|
|
764
821
|
const totalStarted = nowMs();
|
|
765
822
|
await timed(onTiming, "persist", {}, requestPersistentStorage);
|
|
766
823
|
const root = await timed(onTiming, "root", {}, getOpfsRoot);
|
|
@@ -799,6 +856,7 @@ async function attachOpfsParquetTables(options) {
|
|
|
799
856
|
const expectedCount = new Map(tables.map((t) => [t.table, t.files.length + (t.overlay ? 1 : 0)]));
|
|
800
857
|
const { DuckDBDataProtocol } = await timed(onTiming, "duckdb-import", {}, () => import("@duckdb/duckdb-wasm"));
|
|
801
858
|
const registry = getOpfsRegistry(db, DuckDBDataProtocol.BROWSER_FSACCESS);
|
|
859
|
+
const bufferRegistry = getBufferRegistry(db);
|
|
802
860
|
await timed(onTiming, "sweep", {
|
|
803
861
|
files: total,
|
|
804
862
|
tables: tables.length
|
|
@@ -915,20 +973,21 @@ async function attachOpfsParquetTables(options) {
|
|
|
915
973
|
continue;
|
|
916
974
|
}
|
|
917
975
|
const overlayName = overlayNames.get(t.table);
|
|
918
|
-
const lakeNames = overlayName ? files.map((f) => f.name).filter((n) => n !== overlayName) : files.map((f) => f.name);
|
|
976
|
+
const lakeNames = (overlayName ? files.map((f) => f.name).filter((n) => n !== overlayName) : files.map((f) => f.name)).sort();
|
|
977
|
+
const signature = viewSignature(lakeNames, overlayName);
|
|
919
978
|
try {
|
|
920
979
|
signal?.throwIfAborted();
|
|
921
980
|
await timed(onTiming, "view", {
|
|
922
981
|
table: t.table,
|
|
923
982
|
files: files.length
|
|
924
|
-
}, () => withDb(() => conn.
|
|
983
|
+
}, () => withDb(() => ensureView(registry, conn, schema, t.table, signature, overlayName ? readParquetViewWithOverlaySql(schema, t.table, lakeNames, overlayName) : readParquetViewSql$1(schema, t.table, lakeNames))));
|
|
925
984
|
} catch (err) {
|
|
926
985
|
if (isAbortError$1(err)) {
|
|
927
986
|
await withDb(() => detachOpfs(registry, conn, schema, attached, [...acquiredNames])).catch(() => {});
|
|
928
987
|
throw err;
|
|
929
988
|
}
|
|
930
989
|
if (isOpfsAccessHandleConflict(err)) {
|
|
931
|
-
if (registry.viewRefs(
|
|
990
|
+
if (registry.viewRefs(viewKey(schema, t.table)) === 0) await withDb(() => conn.query(`DROP VIEW IF EXISTS ${schema}.${t.table}`)).catch(() => {});
|
|
932
991
|
degraded.add(t.table);
|
|
933
992
|
degradeReason.set(t.table, "contention");
|
|
934
993
|
await releaseTable(t.table);
|
|
@@ -938,7 +997,6 @@ async function attachOpfsParquetTables(options) {
|
|
|
938
997
|
throw err;
|
|
939
998
|
}
|
|
940
999
|
attached.push(t.table);
|
|
941
|
-
registry.acquireView(`${schema}.${t.table}`);
|
|
942
1000
|
for (const f of files) registeredNames.push(f.name);
|
|
943
1001
|
}
|
|
944
1002
|
const recoverTableToBuffers = async (t) => {
|
|
@@ -959,28 +1017,30 @@ async function attachOpfsParquetTables(options) {
|
|
|
959
1017
|
};
|
|
960
1018
|
const lakeNames = [];
|
|
961
1019
|
for (let i = 0; i < t.files.length; i++) {
|
|
962
|
-
const
|
|
963
|
-
const name =
|
|
964
|
-
|
|
1020
|
+
const file = t.files[i];
|
|
1021
|
+
const name = await recoveredBufferName(t.table, file, i);
|
|
1022
|
+
const buf = await readOne(file, i);
|
|
1023
|
+
await withDb(() => bufferRegistry.acquire(name, () => buf));
|
|
965
1024
|
bufferFiles.push(name);
|
|
966
1025
|
lakeNames.push(name);
|
|
967
|
-
bytesAttached +=
|
|
1026
|
+
bytesAttached += file.bytes;
|
|
968
1027
|
}
|
|
969
1028
|
let overlayName;
|
|
970
1029
|
if (t.overlay) {
|
|
1030
|
+
overlayName = await recoveredBufferName(t.table, t.overlay, t.files.length);
|
|
971
1031
|
const buf = await readOne(t.overlay, t.files.length);
|
|
972
|
-
overlayName
|
|
973
|
-
await withDb(() => db.registerFileBuffer(overlayName, buf));
|
|
1032
|
+
await withDb(() => bufferRegistry.acquire(overlayName, () => buf));
|
|
974
1033
|
bufferFiles.push(overlayName);
|
|
975
1034
|
bytesAttached += t.overlay.bytes;
|
|
976
1035
|
}
|
|
1036
|
+
lakeNames.sort();
|
|
977
1037
|
const body = overlayViewBody({
|
|
978
1038
|
lakeSelect: lakeNames.length ? lakeSelect(lakeNames) : null,
|
|
979
1039
|
overlaySelect: overlayName ? `SELECT * REPLACE (CAST(date AS DATE) AS date) FROM read_parquet(['${overlayName.replace(/'/g, "''")}'], union_by_name = true)` : null,
|
|
980
1040
|
materializeLake: false
|
|
981
1041
|
});
|
|
982
1042
|
if (!body) return false;
|
|
983
|
-
await withDb(() => conn.
|
|
1043
|
+
await withDb(() => ensureView(registry, conn, schema, t.table, viewSignature(lakeNames, overlayName), `CREATE OR REPLACE VIEW ${schema}.${t.table} AS ${body}`));
|
|
984
1044
|
bufferViews.push(t.table);
|
|
985
1045
|
return true;
|
|
986
1046
|
});
|
|
@@ -990,7 +1050,7 @@ async function attachOpfsParquetTables(options) {
|
|
|
990
1050
|
const before = bufferFiles.length;
|
|
991
1051
|
if (await recoverTableToBuffers(t).catch(() => false)) degraded.delete(t.table);
|
|
992
1052
|
else {
|
|
993
|
-
|
|
1053
|
+
await withDb(() => bufferRegistry.release(bufferFiles.slice(before))).catch(() => {});
|
|
994
1054
|
bufferFiles.length = before;
|
|
995
1055
|
}
|
|
996
1056
|
}
|
|
@@ -1012,8 +1072,8 @@ async function attachOpfsParquetTables(options) {
|
|
|
1012
1072
|
if (detached) return;
|
|
1013
1073
|
detached = true;
|
|
1014
1074
|
await detachOpfs(registry, conn, schema, attached, registeredNames);
|
|
1015
|
-
for (const v of bufferViews) await conn.query(`DROP VIEW IF EXISTS ${schema}.${v}`).
|
|
1016
|
-
|
|
1075
|
+
for (const v of bufferViews) await registry.releaseView(viewKey(schema, v), () => conn.query(`DROP VIEW IF EXISTS ${schema}.${v}`).then(() => {}));
|
|
1076
|
+
await bufferRegistry.release(bufferFiles);
|
|
1017
1077
|
}
|
|
1018
1078
|
};
|
|
1019
1079
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/engine-duckdb-wasm",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.32.
|
|
4
|
+
"version": "0.32.12",
|
|
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
|
-
"@gscdump/
|
|
49
|
-
"@gscdump/
|
|
50
|
-
"gscdump": "0.32.
|
|
48
|
+
"@gscdump/engine": "0.32.12",
|
|
49
|
+
"@gscdump/contracts": "0.32.12",
|
|
50
|
+
"gscdump": "0.32.12"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@duckdb/duckdb-wasm": "^1.32.0",
|