@gscdump/engine-duckdb-wasm 0.25.13 → 0.25.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +51 -28
  2. package/package.json +7 -7
package/dist/index.mjs CHANGED
@@ -166,7 +166,8 @@ function compileArchetypeSql(query) {
166
166
  }
167
167
  const col = DIM_COLUMN[query.dimension];
168
168
  const facet = facetPredicate(query);
169
- let sql = `SELECT ${col} AS ${query.dimension}, ${metricSelectList(query.metrics)} FROM ${table} WHERE ${where.sql}${facet.sql} GROUP BY ${col} ORDER BY ${query.orderBy.metric} ${dir} LIMIT ?`;
169
+ const totalCol = query.includeTotal ? ", COUNT(*) OVER() AS __total" : "";
170
+ let sql = `SELECT ${col} AS ${query.dimension}, ${metricSelectList(query.metrics)}${totalCol} FROM ${table} WHERE ${where.sql}${facet.sql} GROUP BY ${col} ORDER BY ${query.orderBy.metric} ${dir} LIMIT ?`;
170
171
  const params = [
171
172
  ...where.params,
172
173
  ...facet.params,
@@ -489,12 +490,27 @@ function isOpfsAccessHandleConflict(err) {
489
490
  const msg = err instanceof Error ? err.message : String(err);
490
491
  return /createSyncAccessHandle|Access Handle/i.test(msg);
491
492
  }
492
- function opfsFileName(table, index, hashSlug) {
493
- const base = `${OPFS_PREFIX}${table}_${index}`;
494
- return hashSlug ? `${base}_${hashSlug}.parquet` : `${base}.parquet`;
493
+ function opfsFileName(table, hashSlug, index) {
494
+ return hashSlug ? `${OPFS_PREFIX}${table}_${hashSlug}.parquet` : `${OPFS_PREFIX}${table}_${index}.parquet`;
495
495
  }
496
- function opfsFileNamePrefix(table, index) {
497
- return `${OPFS_PREFIX}${table}_${index}`;
496
+ function escapeRegExp(s) {
497
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
498
+ }
499
+ function tableEntryMatcher(table) {
500
+ return new RegExp(`^${escapeRegExp(`${OPFS_PREFIX}${table}_`)}(?:[0-9a-f]{16}|\\d+(?:_[0-9a-f]{16})?)\\.parquet$`);
501
+ }
502
+ async function sweepStaleEntries(root, registry, expectedByTable) {
503
+ const dir = root;
504
+ if (!dir.keys) return;
505
+ const matchers = [...expectedByTable].map(([table, expected]) => ({
506
+ expected,
507
+ re: tableEntryMatcher(table)
508
+ }));
509
+ for await (const name of dir.keys()) {
510
+ const m = matchers.find((m) => m.re.test(name));
511
+ if (!m || m.expected.has(name) || registry.refs(name) > 0) continue;
512
+ await root.removeEntry(name).catch(() => {});
513
+ }
498
514
  }
499
515
  async function requestPersistentStorage() {
500
516
  const storage = globalThis.navigator?.storage;
@@ -523,7 +539,7 @@ async function getOpfsRoot() {
523
539
  if (!dir) throw new Error("[engine-duckdb-wasm/opfs] OPFS unavailable: navigator.storage.getDirectory missing");
524
540
  return dir.call(globalThis.navigator.storage);
525
541
  }
526
- async function materialiseFile(root, name, staleSweepPrefix, file, fetchImpl, fetchInit, signal) {
542
+ async function materialiseFile(root, name, file, fetchImpl, fetchInit, signal) {
527
543
  signal?.throwIfAborted();
528
544
  let handle;
529
545
  try {
@@ -533,12 +549,6 @@ async function materialiseFile(root, name, staleSweepPrefix, file, fetchImpl, fe
533
549
  outcome: "cache-hit"
534
550
  };
535
551
  } catch {}
536
- const dir = root;
537
- if (dir.keys) for await (const existing of dir.keys()) {
538
- if (existing === name || !existing.startsWith(staleSweepPrefix) || !existing.endsWith(".parquet")) continue;
539
- const next = existing.charAt(staleSweepPrefix.length);
540
- if (next === "." || next === "_") await root.removeEntry(existing).catch(() => {});
541
- }
542
552
  signal?.throwIfAborted();
543
553
  const resp = await fetchImpl(file.url, {
544
554
  ...fetchInit,
@@ -596,24 +606,39 @@ async function attachOpfsParquetTables(options) {
596
606
  await requestPersistentStorage();
597
607
  const root = await getOpfsRoot();
598
608
  const flat = [];
609
+ const expectedByTable = /* @__PURE__ */ new Map();
599
610
  for (const t of tables) {
600
- for (let i = 0; i < t.files.length; i++) flat.push({
601
- table: t.table,
602
- file: t.files[i],
603
- fileIndex: i
604
- });
605
- if (t.overlay) flat.push({
606
- table: t.table,
607
- file: t.overlay,
608
- fileIndex: t.files.length,
609
- overlay: true
610
- });
611
+ const expected = /* @__PURE__ */ new Set();
612
+ for (let i = 0; i < t.files.length; i++) {
613
+ const file = t.files[i];
614
+ const slug = file.contentHash ? await contentHashSlug(file.contentHash) : void 0;
615
+ const name = opfsFileName(t.table, slug, i);
616
+ flat.push({
617
+ table: t.table,
618
+ file,
619
+ name
620
+ });
621
+ expected.add(name);
622
+ }
623
+ if (t.overlay) {
624
+ const slug = t.overlay.contentHash ? await contentHashSlug(t.overlay.contentHash) : void 0;
625
+ const name = opfsFileName(t.table, slug, t.files.length);
626
+ flat.push({
627
+ table: t.table,
628
+ file: t.overlay,
629
+ name,
630
+ overlay: true
631
+ });
632
+ expected.add(name);
633
+ }
634
+ expectedByTable.set(t.table, expected);
611
635
  }
612
636
  const total = flat.length;
613
637
  const overlayNames = /* @__PURE__ */ new Map();
614
638
  const expectedCount = new Map(tables.map((t) => [t.table, t.files.length + (t.overlay ? 1 : 0)]));
615
639
  const { DuckDBDataProtocol } = await import("@duckdb/duckdb-wasm");
616
640
  const registry = getOpfsRegistry(db, DuckDBDataProtocol.BROWSER_FSACCESS);
641
+ await sweepStaleEntries(root, registry, expectedByTable).catch(() => {});
617
642
  const tableFiles = /* @__PURE__ */ new Map();
618
643
  const degraded = /* @__PURE__ */ new Set();
619
644
  const acquiredNames = /* @__PURE__ */ new Set();
@@ -626,12 +651,10 @@ async function attachOpfsParquetTables(options) {
626
651
  const runDownloads = () => runWithConcurrency$1(flat, Math.max(1, fetchConcurrency), async (item, index) => {
627
652
  if (degraded.has(item.table)) return;
628
653
  signal?.throwIfAborted();
629
- const hashSlug = item.file.contentHash ? await contentHashSlug(item.file.contentHash) : void 0;
630
- const name = opfsFileName(item.table, item.fileIndex, hashSlug);
631
- const sweepPrefix = opfsFileNamePrefix(item.table, item.fileIndex);
654
+ const name = item.name;
632
655
  let result;
633
656
  try {
634
- result = await materialiseFile(root, name, sweepPrefix, item.file, fetchImpl, fetchInit, signal);
657
+ result = await materialiseFile(root, name, item.file, fetchImpl, fetchInit, signal);
635
658
  } catch (err) {
636
659
  if (isAbortError$1(err)) throw err;
637
660
  if (isQuotaError(err)) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine-duckdb-wasm",
3
3
  "type": "module",
4
- "version": "0.25.13",
4
+ "version": "0.25.14",
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,16 +45,16 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "drizzle-orm": "1.0.0-rc.3",
48
- "@gscdump/engine": "0.25.13",
49
- "gscdump": "0.25.13",
50
- "@gscdump/sdk": "0.25.13"
48
+ "@gscdump/engine": "0.25.14",
49
+ "@gscdump/sdk": "0.25.14",
50
+ "gscdump": "0.25.14"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@duckdb/duckdb-wasm": "^1.32.0",
54
- "@vitest/browser": "^4.1.7",
55
- "@vitest/browser-playwright": "^4.1.7",
54
+ "@vitest/browser": "^4.1.8",
55
+ "@vitest/browser-playwright": "^4.1.8",
56
56
  "playwright": "^1.60.0",
57
- "vitest": "^4.1.7"
57
+ "vitest": "^4.1.8"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "obuild",