@gscdump/engine-duckdb-wasm 0.25.14 → 0.26.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.d.mts CHANGED
@@ -4,6 +4,7 @@ import { AnyRelations, EmptyRelations, ExtractTablesWithRelations, Schema as Sch
4
4
  import { DrizzleSchema as Schema, countries, dates, drizzleSchema as schema, hourly_pages, page_queries, pages, queries } from "@gscdump/engine/schema";
5
5
  import { ScopedRunnerOptions, TableScope } from "@gscdump/engine/scope";
6
6
  import { AnalyzerRegistry } from "@gscdump/engine/analyzer";
7
+ import { Result } from "gscdump/result";
7
8
  import { ComparisonMode, ResolveWindowOptions, ResolvedWindow, WindowPreset, resolveWindow } from "@gscdump/engine/period";
8
9
  import { ArchetypeQuery } from "@gscdump/sdk";
9
10
  import { AsyncDuckDB, AsyncDuckDBConnection, DuckDBBundles, DuckDBConfig } from "@duckdb/duckdb-wasm";
@@ -217,6 +218,14 @@ interface DuckDBWasmBootResult {
217
218
  conn: AsyncDuckDBConnection;
218
219
  }
219
220
  interface BootDuckDBWasmOptions {
221
+ /**
222
+ * DuckDB-WASM logger. Defaults to a `ConsoleLogger` thresholded at
223
+ * `LogLevel.WARNING`, so real warnings/errors still surface but the per-query
224
+ * INFO events (START/OK/RUN) — which DuckDB's default `ConsoleLogger()` emits
225
+ * as raw objects, flooding the host console with dozens of lines per render —
226
+ * are dropped. Pass `new ConsoleLogger(LogLevel.DEBUG)` to see everything, or
227
+ * `new VoidLogger()` to silence it entirely.
228
+ */
220
229
  logger?: unknown;
221
230
  /**
222
231
  * Override the jsDelivr-hosted bundle map. Required in environments where
@@ -354,12 +363,40 @@ interface BrowserAnalysisRuntime {
354
363
  declare class BrowserAttachBudgetExceededError extends Error {
355
364
  name: string;
356
365
  }
366
+ interface BrowserAttachError {
367
+ kind: 'browser-attach-budget-exceeded';
368
+ message: string;
369
+ /** Which budget tripped: the file count, the hinted byte plan, or the running byte plan. */
370
+ budget: 'maxFiles' | 'maxBytes';
371
+ }
372
+ declare const browserAttachErrors: {
373
+ readonly maxFilesExceeded: (files: number, maxFiles: number) => BrowserAttachError;
374
+ readonly hintedBytesExceeded: (hintedBytes: number, maxBytes: number) => BrowserAttachError;
375
+ readonly plannedBytesExceeded: (plannedBytes: number, maxBytes: number) => BrowserAttachError;
376
+ };
377
+ declare function isBrowserAttachError(value: unknown): value is BrowserAttachError;
357
378
  declare function bootDuckDBWasm(options?: BootDuckDBWasmOptions): Promise<DuckDBWasmBootResult>;
358
379
  declare function attachParquetTables(options: AttachParquetTablesOptions): Promise<void>;
380
+ /**
381
+ * Errors-as-values core for {@link attachParquetUrlTables}: returns a typed
382
+ * {@link BrowserAttachError} when the requested file set blows the local file-
383
+ * count / byte budget, so callers can branch (route the affected tables
384
+ * server-side) instead of catching an untyped throw. WASM/DuckDB/HTTP IO
385
+ * failures stay defects and propagate. `attachParquetUrlTables` is the thin
386
+ * throwing wrapper preserving the historical `BrowserAttachBudgetExceededError`.
387
+ */
388
+ declare function attachParquetUrlTablesResult(options: AttachParquetUrlTablesOptions): Promise<Result<AttachedTablesHandle, BrowserAttachError>>;
389
+ /**
390
+ * Attach browser parquet URL tables, throwing
391
+ * {@link BrowserAttachBudgetExceededError} when the requested set blows the
392
+ * file-count / byte budget. Thin throwing wrapper over
393
+ * {@link attachParquetUrlTablesResult}; existing call sites and their
394
+ * `instanceof BrowserAttachBudgetExceededError` checks keep holding.
395
+ */
359
396
  declare function attachParquetUrlTables(options: AttachParquetUrlTablesOptions): Promise<AttachedTablesHandle>;
360
397
  declare function createBrowserAnalysisRuntime(boot: DuckDBWasmBootResult, options?: {
361
398
  schema?: string;
362
399
  version?: number | string;
363
400
  attachedTables?: readonly string[];
364
401
  }): BrowserAnalysisRuntime;
365
- export { type AnalyzeResult, type AttachOpfsTablesOptions, type AttachParquetTablesOptions, type AttachParquetUrlTablesOptions, type AttachedTablesHandle, type BootDuckDBWasmOptions, type BrowserAnalysisRuntime, BrowserAttachBudgetExceededError, type BrowserParquetFile, type BrowserParquetTable, type BrowserParquetUrlTable, type ComparisonMode, type CompiledArchetypeSql, type DuckDBWasmBootResult, type DuckDBWasmClient, DuckDBWasmDatabase, type DuckDBWasmDrizzleDatabase, type InsightRunner, type InsightRunnerOptions, type OpfsAttachedHandle, type OpfsFileProgress, type OpfsParquetFile, type OpfsParquetTable, OpfsQuotaExceededError, type QueryResult, type ResolveWindowOptions, type ResolvedWindow, type Schema, type ScopedRunnerOptions, type StrikingMomentumOptions, type StrikingMomentumRow, type TableScope, type WindowPreset, attachOpfsParquetTables, attachParquetTables, attachParquetUrlTables, bootDuckDBWasm, clearOpfsSnapshotCache, compileArchetypeSql, countries, createBrowserAnalysisRuntime, createClient, createInsightRunner, dates, drizzle, estimateOpfsStorage, hourly_pages, mergeScope, page_queries, pages, queries, requestPersistentStorage, resolveWindow, schema, scopeFor, strikingMomentum, tableForArchetype };
402
+ export { type AnalyzeResult, type AttachOpfsTablesOptions, type AttachParquetTablesOptions, type AttachParquetUrlTablesOptions, type AttachedTablesHandle, type BootDuckDBWasmOptions, type BrowserAnalysisRuntime, BrowserAttachBudgetExceededError, type BrowserAttachError, type BrowserParquetFile, type BrowserParquetTable, type BrowserParquetUrlTable, type ComparisonMode, type CompiledArchetypeSql, type DuckDBWasmBootResult, type DuckDBWasmClient, DuckDBWasmDatabase, type DuckDBWasmDrizzleDatabase, type InsightRunner, type InsightRunnerOptions, type OpfsAttachedHandle, type OpfsFileProgress, type OpfsParquetFile, type OpfsParquetTable, OpfsQuotaExceededError, type QueryResult, type ResolveWindowOptions, type ResolvedWindow, type Schema, type ScopedRunnerOptions, type StrikingMomentumOptions, type StrikingMomentumRow, type TableScope, type WindowPreset, attachOpfsParquetTables, attachParquetTables, attachParquetUrlTables, attachParquetUrlTablesResult, bootDuckDBWasm, browserAttachErrors, clearOpfsSnapshotCache, compileArchetypeSql, countries, createBrowserAnalysisRuntime, createClient, createInsightRunner, dates, drizzle, estimateOpfsStorage, hourly_pages, isBrowserAttachError, mergeScope, page_queries, pages, queries, requestPersistentStorage, resolveWindow, schema, scopeFor, strikingMomentum, tableForArchetype };
package/dist/index.mjs CHANGED
@@ -9,6 +9,7 @@ import { runAnalyzerFromSource } from "@gscdump/engine/analyzer";
9
9
  import { pgResolverAdapter } from "@gscdump/engine/resolver";
10
10
  import { createAttachedTableSource } from "@gscdump/engine/source";
11
11
  import { sqlEscape } from "@gscdump/engine/sql";
12
+ import { err, ok, unwrapResult } from "gscdump/result";
12
13
  import { resolveWindow } from "@gscdump/engine/period";
13
14
  const METRIC_SQL = {
14
15
  clicks: "SUM(clicks)",
@@ -278,7 +279,7 @@ async function createClient(db, conn) {
278
279
  try {
279
280
  return arrowToRows(await stmt.query(...params));
280
281
  } finally {
281
- stmt.close();
282
+ await stmt.close();
282
283
  }
283
284
  },
284
285
  async close() {
@@ -583,7 +584,7 @@ function readParquetViewSql$1(schema, table, files) {
583
584
  function readParquetViewWithOverlaySql(schema, table, lakeFiles, overlayFile) {
584
585
  const overlay = `SELECT * REPLACE (CAST(date AS DATE) AS date) FROM read_parquet(['${overlayFile.replace(/'/g, "''")}'], union_by_name = true)`;
585
586
  if (lakeFiles.length === 0) return `CREATE OR REPLACE VIEW ${schema}.${table} AS ${overlay}`;
586
- return `CREATE OR REPLACE VIEW ${schema}.${table} AS ${lakeSelect(lakeFiles)} UNION ALL BY NAME ${overlay} WHERE CAST(date AS DATE) NOT IN (SELECT DISTINCT CAST(date AS DATE) FROM read_parquet([${quoteList(lakeFiles)}], union_by_name = true))`;
587
+ return `CREATE OR REPLACE VIEW ${schema}.${table} AS WITH lake AS MATERIALIZED (${lakeSelect(lakeFiles)}), lake_dates AS (SELECT DISTINCT date FROM lake) SELECT * FROM lake UNION ALL BY NAME SELECT * FROM (${overlay}) AS overlay WHERE overlay.date NOT IN (SELECT date FROM lake_dates)`;
587
588
  }
588
589
  async function runWithConcurrency$1(items, concurrency, fn) {
589
590
  let next = 0;
@@ -781,6 +782,37 @@ let nextAttachId = 0;
781
782
  var BrowserAttachBudgetExceededError = class extends Error {
782
783
  name = "BrowserAttachBudgetExceededError";
783
784
  };
785
+ const browserAttachErrors = {
786
+ maxFilesExceeded(files, maxFiles) {
787
+ return {
788
+ kind: "browser-attach-budget-exceeded",
789
+ budget: "maxFiles",
790
+ message: `browser parquet attach requires ${files} files, above maxFiles=${maxFiles}`
791
+ };
792
+ },
793
+ hintedBytesExceeded(hintedBytes, maxBytes) {
794
+ return {
795
+ kind: "browser-attach-budget-exceeded",
796
+ budget: "maxBytes",
797
+ message: `browser parquet attach requires ${hintedBytes} hinted bytes, above maxBytes=${maxBytes}`
798
+ };
799
+ },
800
+ plannedBytesExceeded(plannedBytes, maxBytes) {
801
+ return {
802
+ kind: "browser-attach-budget-exceeded",
803
+ budget: "maxBytes",
804
+ message: `browser parquet attach planned ${plannedBytes} bytes, above maxBytes=${maxBytes}`
805
+ };
806
+ }
807
+ };
808
+ function isBrowserAttachError(value) {
809
+ return typeof value === "object" && value !== null && value.kind === "browser-attach-budget-exceeded" && typeof value.message === "string";
810
+ }
811
+ function browserAttachErrorToException(error) {
812
+ const exception = new BrowserAttachBudgetExceededError(error.message);
813
+ exception.browserAttachError = error;
814
+ return exception;
815
+ }
784
816
  function fileName(table, index, provided) {
785
817
  return provided ?? `${table}_${index}.parquet`;
786
818
  }
@@ -909,11 +941,11 @@ async function dropAttachedResources(db, conn, schema, tables, files) {
909
941
  if (files.length > 0) await db.dropFiles([...files]);
910
942
  }
911
943
  async function bootDuckDBWasm(options = {}) {
912
- const { getJsDelivrBundles, selectBundle, AsyncDuckDB, ConsoleLogger } = await import("@duckdb/duckdb-wasm");
944
+ const { getJsDelivrBundles, selectBundle, AsyncDuckDB, ConsoleLogger, LogLevel } = await import("@duckdb/duckdb-wasm");
913
945
  const bundle = await selectBundle(options.bundles ?? getJsDelivrBundles());
914
946
  const workerUrl = URL.createObjectURL(new Blob([`importScripts("${bundle.mainWorker}");`], { type: "text/javascript" }));
915
947
  const worker = new Worker(workerUrl);
916
- const db = new AsyncDuckDB(options.logger ?? new ConsoleLogger(), worker);
948
+ const db = new AsyncDuckDB(options.logger ?? new ConsoleLogger(LogLevel.WARNING), worker);
917
949
  try {
918
950
  await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
919
951
  await db.open(rangeOnlyConfig(options.config));
@@ -941,7 +973,7 @@ async function attachParquetTables(options) {
941
973
  await conn.query(readParquetViewSql(schema, table.table, names));
942
974
  }
943
975
  }
944
- async function attachParquetUrlTables(options) {
976
+ async function attachParquetUrlTablesResult(options) {
945
977
  const { db, conn, tables, fetch: fetchImpl = globalThis.fetch.bind(globalThis), schema = "main", fetchInit, fetchConcurrency, maxFiles, maxBytes, signal, version, onFileAttached, attachMode = "http", trustSizeHint = true } = options;
946
978
  const concurrency = positiveInteger(fetchConcurrency, DEFAULT_ATTACH_FETCH_CONCURRENCY, "fetchConcurrency");
947
979
  const fileBudget = positiveInteger(maxFiles, DEFAULT_ATTACH_MAX_FILES, "maxFiles");
@@ -958,52 +990,57 @@ async function attachParquetUrlTables(options) {
958
990
  index: i
959
991
  });
960
992
  }
961
- if (flat.length > fileBudget) throw new BrowserAttachBudgetExceededError(`browser parquet attach requires ${flat.length} files, above maxFiles=${fileBudget}`);
993
+ if (flat.length > fileBudget) return err(browserAttachErrors.maxFilesExceeded(flat.length, fileBudget));
962
994
  const hintedBytes = flat.reduce((acc, item) => {
963
995
  const hint = sizeHintFromUrl(item.url);
964
996
  return hint === null ? acc : acc + hint;
965
997
  }, 0);
966
- if (hintedBytes > byteBudget) throw new BrowserAttachBudgetExceededError(`browser parquet attach requires ${hintedBytes} hinted bytes, above maxBytes=${byteBudget}`);
998
+ if (hintedBytes > byteBudget) return err(browserAttachErrors.hintedBytesExceeded(hintedBytes, byteBudget));
967
999
  const tableFailures = /* @__PURE__ */ new Map();
968
1000
  const budgetController = new AbortController();
969
1001
  const effectiveSignal = mergeAbortSignals(signal, budgetController.signal);
970
1002
  let plannedBytes = 0;
971
1003
  const total = flat.length;
972
1004
  const prepared = [];
973
- await runWithConcurrency(flat, concurrency, async ({ table, url, index }) => {
974
- if (tableFailures.has(table)) return;
975
- effectiveSignal?.throwIfAborted();
976
- try {
977
- let bytes = null;
978
- let body = null;
979
- if (attachMode === "buffer") {
980
- const res = await fetchImpl(url, fetchInitFor(fetchInit, "GET", effectiveSignal));
981
- if (!res.ok) throw new Error(`GET ${url} failed: ${res.status}`);
982
- const buf = new Uint8Array(await res.arrayBuffer());
983
- body = buf;
984
- bytes = buf.byteLength;
985
- } else if (trustSizeHint && sizeHintFromUrl(url) !== null) bytes = sizeHintFromUrl(url);
986
- else bytes = await preflightHttpUrl(url, fetchImpl, fetchInit, effectiveSignal);
987
- plannedBytes += bytes;
988
- if (plannedBytes > byteBudget) {
989
- const err = new BrowserAttachBudgetExceededError(`browser parquet attach planned ${plannedBytes} bytes, above maxBytes=${byteBudget}`);
990
- budgetController.abort(err);
991
- throw err;
992
- }
1005
+ try {
1006
+ await runWithConcurrency(flat, concurrency, async ({ table, url, index }) => {
1007
+ if (tableFailures.has(table)) return;
993
1008
  effectiveSignal?.throwIfAborted();
994
- prepared.push({
995
- table,
996
- url,
997
- index,
998
- name: attachFileName(attachId, table, index),
999
- bytes,
1000
- body
1001
- });
1002
- } catch (err) {
1003
- if (effectiveSignal?.aborted || err instanceof BrowserAttachBudgetExceededError || isAbortError(err)) throw err;
1004
- tableFailures.set(table, err instanceof Error ? err : new Error(String(err)));
1005
- }
1006
- });
1009
+ try {
1010
+ let bytes = null;
1011
+ let body = null;
1012
+ if (attachMode === "buffer") {
1013
+ const res = await fetchImpl(url, fetchInitFor(fetchInit, "GET", effectiveSignal));
1014
+ if (!res.ok) throw new Error(`GET ${url} failed: ${res.status}`);
1015
+ const buf = new Uint8Array(await res.arrayBuffer());
1016
+ body = buf;
1017
+ bytes = buf.byteLength;
1018
+ } else if (trustSizeHint && sizeHintFromUrl(url) !== null) bytes = sizeHintFromUrl(url);
1019
+ else bytes = await preflightHttpUrl(url, fetchImpl, fetchInit, effectiveSignal);
1020
+ plannedBytes += bytes;
1021
+ if (plannedBytes > byteBudget) {
1022
+ const budgetErr = new BrowserAttachBudgetExceededError(`browser parquet attach planned ${plannedBytes} bytes, above maxBytes=${byteBudget}`);
1023
+ budgetController.abort(budgetErr);
1024
+ throw budgetErr;
1025
+ }
1026
+ effectiveSignal?.throwIfAborted();
1027
+ prepared.push({
1028
+ table,
1029
+ url,
1030
+ index,
1031
+ name: attachFileName(attachId, table, index),
1032
+ bytes,
1033
+ body
1034
+ });
1035
+ } catch (err) {
1036
+ if (effectiveSignal?.aborted || err instanceof BrowserAttachBudgetExceededError || isAbortError(err)) throw err;
1037
+ tableFailures.set(table, err instanceof Error ? err : new Error(String(err)));
1038
+ }
1039
+ });
1040
+ } catch (downloadErr) {
1041
+ if (downloadErr instanceof BrowserAttachBudgetExceededError && !signal?.aborted) return err(browserAttachErrors.plannedBytesExceeded(plannedBytes, byteBudget));
1042
+ throw downloadErr;
1043
+ }
1007
1044
  const { DuckDBDataProtocol } = await import("@duckdb/duckdb-wasm");
1008
1045
  const attached = [];
1009
1046
  const registeredFiles = [];
@@ -1036,7 +1073,7 @@ async function attachParquetUrlTables(options) {
1036
1073
  }
1037
1074
  if (tableFailures.size > 0) for (const [table, err] of tableFailures) console.warn(`[gscdump/engine-duckdb-wasm] dropped table "${table}" — ${err.message}`);
1038
1075
  let detached = false;
1039
- return {
1076
+ return ok({
1040
1077
  version,
1041
1078
  tables: attached,
1042
1079
  schema,
@@ -1045,7 +1082,10 @@ async function attachParquetUrlTables(options) {
1045
1082
  detached = true;
1046
1083
  await dropAttachedResources(db, conn, schema, attached, registeredFiles);
1047
1084
  }
1048
- };
1085
+ });
1086
+ }
1087
+ async function attachParquetUrlTables(options) {
1088
+ return unwrapResult(await attachParquetUrlTablesResult(options), browserAttachErrorToException);
1049
1089
  }
1050
1090
  function createBrowserAnalysisRuntime(boot, options = {}) {
1051
1091
  const { db, conn } = boot;
@@ -1147,4 +1187,4 @@ function createBrowserAnalysisRuntime(boot, options = {}) {
1147
1187
  }
1148
1188
  };
1149
1189
  }
1150
- export { BrowserAttachBudgetExceededError, DuckDBWasmDatabase, OpfsQuotaExceededError, attachOpfsParquetTables, attachParquetTables, attachParquetUrlTables, bootDuckDBWasm, clearOpfsSnapshotCache, compileArchetypeSql, countries, createBrowserAnalysisRuntime, createClient, createInsightRunner, dates, drizzle, estimateOpfsStorage, hourly_pages, mergeScope, page_queries, pages, queries, requestPersistentStorage, resolveWindow, schema, scopeFor, strikingMomentum, tableForArchetype };
1190
+ export { BrowserAttachBudgetExceededError, DuckDBWasmDatabase, OpfsQuotaExceededError, attachOpfsParquetTables, attachParquetTables, attachParquetUrlTables, attachParquetUrlTablesResult, bootDuckDBWasm, browserAttachErrors, clearOpfsSnapshotCache, compileArchetypeSql, countries, createBrowserAnalysisRuntime, createClient, createInsightRunner, dates, drizzle, estimateOpfsStorage, hourly_pages, isBrowserAttachError, mergeScope, page_queries, pages, queries, requestPersistentStorage, resolveWindow, schema, scopeFor, strikingMomentum, tableForArchetype };
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.14",
4
+ "version": "0.26.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
- "@gscdump/engine": "0.25.14",
49
- "@gscdump/sdk": "0.25.14",
50
- "gscdump": "0.25.14"
48
+ "@gscdump/engine": "0.26.0",
49
+ "@gscdump/sdk": "0.26.0",
50
+ "gscdump": "0.26.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@duckdb/duckdb-wasm": "^1.32.0",