@gscdump/engine-duckdb-wasm 0.23.4 → 0.24.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 +6 -4
- package/dist/index.mjs +171 -80
- package/package.json +10 -6
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Assume, DrizzleConfig, entityKind } from "drizzle-orm";
|
|
2
|
-
import {
|
|
2
|
+
import { PgAsyncDatabase, PgAsyncPreparedQuery, PgAsyncSession, PgDialect, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
3
|
+
import { AnyRelations, EmptyRelations, ExtractTablesWithRelations, Schema as Schema$1 } from "drizzle-orm/relations";
|
|
3
4
|
import { DrizzleSchema as Schema, countries, dates, drizzleSchema as schema, hourly_pages, page_queries, pages, queries } from "@gscdump/engine/schema";
|
|
4
5
|
import { ScopedRunnerOptions, TableScope } from "@gscdump/engine/scope";
|
|
5
6
|
import { AnalyzerRegistry } from "@gscdump/engine/analyzer";
|
|
@@ -31,13 +32,14 @@ type Row = Record<string, unknown>;
|
|
|
31
32
|
interface DuckDBWasmQueryResultHKT extends PgQueryResultHKT {
|
|
32
33
|
type: Assume<this['row'], Row>[];
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
+
type SchemaRelations<TSchema extends Schema$1> = ExtractTablesWithRelations<Record<string, never>, TSchema>;
|
|
36
|
+
declare class DuckDBWasmDatabase<TRelations extends AnyRelations = EmptyRelations> extends PgAsyncDatabase<DuckDBWasmQueryResultHKT, TRelations> {
|
|
35
37
|
static readonly [entityKind]: string;
|
|
36
38
|
}
|
|
37
|
-
interface DuckDBWasmDrizzleDatabase<TSchema extends Record<string,
|
|
39
|
+
interface DuckDBWasmDrizzleDatabase<TSchema extends Schema$1 = Record<string, never>, TRelations extends AnyRelations = SchemaRelations<TSchema>> extends DuckDBWasmDatabase<TRelations> {
|
|
38
40
|
$client: Promise<DuckDBWasmClient>;
|
|
39
41
|
}
|
|
40
|
-
declare function drizzle<TSchema extends Record<string,
|
|
42
|
+
declare function drizzle<TSchema extends Schema$1 = Record<string, never>, TRelations extends AnyRelations = SchemaRelations<TSchema>>(client: Promise<DuckDBWasmClient> | DuckDBWasmClient, config?: DrizzleConfig<TSchema, TRelations>): DuckDBWasmDrizzleDatabase<TSchema, TRelations>;
|
|
41
43
|
interface InsightRunnerOptions {
|
|
42
44
|
db: AsyncDuckDB;
|
|
43
45
|
conn: AsyncDuckDBConnection;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { arrowToRows } from "@gscdump/engine/arrow";
|
|
2
|
-
import { DefaultLogger, NoopLogger,
|
|
3
|
-
import {
|
|
2
|
+
import { DefaultLogger, NoopLogger, entityKind, sql } from "drizzle-orm";
|
|
3
|
+
import { PgAsyncDatabase, PgAsyncPreparedQuery, PgAsyncSession, PgDialect } from "drizzle-orm/pg-core";
|
|
4
|
+
import { buildRelations } from "drizzle-orm/relations";
|
|
4
5
|
import { toIsoDate } from "gscdump";
|
|
5
6
|
import { countries, dates, drizzleSchema as schema, hourly_pages, page_queries, pages, queries } from "@gscdump/engine/schema";
|
|
6
7
|
import { createScopedHelpers } from "@gscdump/engine/scope";
|
|
@@ -263,69 +264,44 @@ async function createClient(db, conn) {
|
|
|
263
264
|
}
|
|
264
265
|
};
|
|
265
266
|
}
|
|
266
|
-
var
|
|
267
|
-
client;
|
|
268
|
-
logger;
|
|
269
|
-
static [entityKind] = "DuckDBWasmPreparedQuery";
|
|
270
|
-
constructor(client, query, logger, queryMetadata, cache) {
|
|
271
|
-
super(query, cache, queryMetadata);
|
|
272
|
-
this.client = client;
|
|
273
|
-
this.logger = logger;
|
|
274
|
-
}
|
|
275
|
-
async execute(placeholderValues = {}) {
|
|
276
|
-
const params = fillPlaceholders(this.query.params, placeholderValues);
|
|
277
|
-
this.logger.logQuery(this.query.sql, params);
|
|
278
|
-
return await (await this.client).query(this.query.sql, params);
|
|
279
|
-
}
|
|
280
|
-
async all(placeholderValues = {}) {
|
|
281
|
-
const params = fillPlaceholders(this.query.params, placeholderValues);
|
|
282
|
-
this.logger.logQuery(this.query.sql, params);
|
|
283
|
-
return await (await this.client).query(this.query.sql, params);
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
var DuckDBWasmSession = class extends PgSession {
|
|
267
|
+
var DuckDBWasmSession = class extends PgAsyncSession {
|
|
287
268
|
client;
|
|
288
269
|
options;
|
|
289
270
|
static [entityKind] = "DuckDBWasmSession";
|
|
290
271
|
logger;
|
|
291
272
|
cache;
|
|
292
|
-
constructor(client, dialect,
|
|
273
|
+
constructor(client, dialect, options = {}) {
|
|
293
274
|
super(dialect);
|
|
294
275
|
this.client = client;
|
|
295
276
|
this.options = options;
|
|
296
277
|
this.logger = options.logger ?? new NoopLogger();
|
|
297
278
|
this.cache = options.cache;
|
|
298
279
|
}
|
|
299
|
-
prepareQuery(query,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
280
|
+
prepareQuery(query, mode, _name, mapper, queryMetadata, cacheConfig) {
|
|
281
|
+
const client = this.client;
|
|
282
|
+
const executor = async (params = []) => {
|
|
283
|
+
const rows = await (await client).query(query.sql, params);
|
|
284
|
+
return mode === "arrays" ? rows.map((row) => Object.values(row)) : rows;
|
|
285
|
+
};
|
|
286
|
+
return new PgAsyncPreparedQuery(executor, query, mapper, mode, this.logger, this.cache, queryMetadata, cacheConfig);
|
|
305
287
|
}
|
|
306
|
-
transaction(
|
|
307
|
-
throw new Error("Transactions are not supported by the DuckDB-WASM drizzle adapter. The analytics workload is read-only; if transactions become necessary, implement
|
|
288
|
+
transaction() {
|
|
289
|
+
throw new Error("Transactions are not supported by the DuckDB-WASM drizzle adapter. The analytics workload is read-only; if transactions become necessary, implement PgAsyncSession.transaction() in @gscdump/engine-duckdb-wasm.");
|
|
308
290
|
}
|
|
309
291
|
};
|
|
310
|
-
var DuckDBWasmDatabase = class extends
|
|
292
|
+
var DuckDBWasmDatabase = class extends PgAsyncDatabase {
|
|
311
293
|
static [entityKind] = "DuckDBWasmDatabase";
|
|
312
294
|
};
|
|
313
295
|
function drizzle(client, config = {}) {
|
|
314
|
-
const dialect = new PgDialect(
|
|
296
|
+
const dialect = new PgDialect();
|
|
315
297
|
const clientPromise = Promise.resolve(client);
|
|
316
298
|
let logger;
|
|
317
299
|
if (config.logger === true) logger = new DefaultLogger();
|
|
318
300
|
else if (config.logger !== false) logger = config.logger;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
fullSchema: config.schema,
|
|
324
|
-
schema: tablesConfig.tables,
|
|
325
|
-
tableNamesMap: tablesConfig.tableNamesMap
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
const db = new DuckDBWasmDatabase(dialect, new DuckDBWasmSession(clientPromise, dialect, schema, { logger }), schema);
|
|
301
|
+
const db = new DuckDBWasmDatabase(dialect, new DuckDBWasmSession(clientPromise, dialect, {
|
|
302
|
+
logger,
|
|
303
|
+
cache: config.cache
|
|
304
|
+
}), config.relations ?? (config.schema ? buildRelations(config.schema, {}) : {}));
|
|
329
305
|
db.$client = clientPromise;
|
|
330
306
|
return db;
|
|
331
307
|
}
|
|
@@ -390,6 +366,73 @@ async function strikingMomentum(runner, opts = {}) {
|
|
|
390
366
|
`;
|
|
391
367
|
return await runner.db.execute(windowExpr);
|
|
392
368
|
}
|
|
369
|
+
function createOpfsHandleRegistry(backend) {
|
|
370
|
+
const entries = /* @__PURE__ */ new Map();
|
|
371
|
+
const pending = /* @__PURE__ */ new Map();
|
|
372
|
+
async function acquire(name, openHandle) {
|
|
373
|
+
const existing = entries.get(name);
|
|
374
|
+
if (existing) {
|
|
375
|
+
existing.refs++;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const inflight = pending.get(name);
|
|
379
|
+
if (inflight) {
|
|
380
|
+
await inflight.catch(() => {});
|
|
381
|
+
const settled = entries.get(name);
|
|
382
|
+
if (settled) {
|
|
383
|
+
settled.refs++;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
return acquire(name, openHandle);
|
|
387
|
+
}
|
|
388
|
+
const registration = (async () => {
|
|
389
|
+
const handle = await openHandle();
|
|
390
|
+
await backend.register(name, handle);
|
|
391
|
+
entries.set(name, {
|
|
392
|
+
handle,
|
|
393
|
+
refs: 1
|
|
394
|
+
});
|
|
395
|
+
})();
|
|
396
|
+
pending.set(name, registration);
|
|
397
|
+
try {
|
|
398
|
+
await registration;
|
|
399
|
+
} finally {
|
|
400
|
+
pending.delete(name);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function release(names) {
|
|
404
|
+
for (const name of names) {
|
|
405
|
+
const entry = entries.get(name);
|
|
406
|
+
if (!entry) continue;
|
|
407
|
+
entry.refs--;
|
|
408
|
+
if (entry.refs > 0) continue;
|
|
409
|
+
entries.delete(name);
|
|
410
|
+
await backend.drop(name).catch(() => {});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const viewRefs = /* @__PURE__ */ new Map();
|
|
414
|
+
function acquireView(key) {
|
|
415
|
+
viewRefs.set(key, (viewRefs.get(key) ?? 0) + 1);
|
|
416
|
+
}
|
|
417
|
+
async function releaseView(key, drop) {
|
|
418
|
+
const next = (viewRefs.get(key) ?? 0) - 1;
|
|
419
|
+
if (next > 0) {
|
|
420
|
+
viewRefs.set(key, next);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
viewRefs.delete(key);
|
|
424
|
+
await drop().catch(() => {});
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
acquire,
|
|
428
|
+
release,
|
|
429
|
+
size: () => entries.size,
|
|
430
|
+
refs: (name) => entries.get(name)?.refs ?? 0,
|
|
431
|
+
acquireView,
|
|
432
|
+
releaseView,
|
|
433
|
+
viewRefs: (key) => viewRefs.get(key) ?? 0
|
|
434
|
+
};
|
|
435
|
+
}
|
|
393
436
|
var OpfsQuotaExceededError = class extends Error {
|
|
394
437
|
name = "OpfsQuotaExceededError";
|
|
395
438
|
degradedTables;
|
|
@@ -400,17 +443,19 @@ var OpfsQuotaExceededError = class extends Error {
|
|
|
400
443
|
};
|
|
401
444
|
const DEFAULT_CONCURRENCY = 2;
|
|
402
445
|
const OPFS_PREFIX = "gscdump-snapshot__";
|
|
403
|
-
const
|
|
404
|
-
function
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
446
|
+
const dbRegistries = /* @__PURE__ */ new WeakMap();
|
|
447
|
+
function getOpfsRegistry(db, protocol) {
|
|
448
|
+
let registry = dbRegistries.get(db);
|
|
449
|
+
if (!registry) {
|
|
450
|
+
registry = createOpfsHandleRegistry({
|
|
451
|
+
register: (name, handle) => db.registerFileHandle(name, handle, protocol, true),
|
|
452
|
+
drop: async (name) => {
|
|
453
|
+
await db.dropFile(name);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
dbRegistries.set(db, registry);
|
|
412
457
|
}
|
|
413
|
-
|
|
458
|
+
return registry;
|
|
414
459
|
}
|
|
415
460
|
function isQuotaError(err) {
|
|
416
461
|
if (typeof err !== "object" || err === null) return false;
|
|
@@ -419,6 +464,10 @@ function isQuotaError(err) {
|
|
|
419
464
|
function isAbortError$1(err) {
|
|
420
465
|
return typeof err === "object" && err !== null && err.name === "AbortError";
|
|
421
466
|
}
|
|
467
|
+
function isOpfsAccessHandleConflict(err) {
|
|
468
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
469
|
+
return /createSyncAccessHandle|Access Handle/i.test(msg);
|
|
470
|
+
}
|
|
422
471
|
function opfsFileName(table, index, hashSlug) {
|
|
423
472
|
const base = `${OPFS_PREFIX}${table}_${index}`;
|
|
424
473
|
return hashSlug ? `${base}_${hashSlug}.parquet` : `${base}.parquet`;
|
|
@@ -522,10 +571,17 @@ async function attachOpfsParquetTables(options) {
|
|
|
522
571
|
});
|
|
523
572
|
const total = flat.length;
|
|
524
573
|
const { DuckDBDataProtocol } = await import("@duckdb/duckdb-wasm");
|
|
574
|
+
const registry = getOpfsRegistry(db, DuckDBDataProtocol.BROWSER_FSACCESS);
|
|
525
575
|
const tableFiles = /* @__PURE__ */ new Map();
|
|
526
576
|
const degraded = /* @__PURE__ */ new Set();
|
|
577
|
+
const acquiredNames = /* @__PURE__ */ new Set();
|
|
527
578
|
let bytesAttached = 0;
|
|
528
|
-
|
|
579
|
+
const releaseTable = async (table) => {
|
|
580
|
+
const names = (tableFiles.get(table) ?? []).map((f) => f.name);
|
|
581
|
+
for (const n of names) acquiredNames.delete(n);
|
|
582
|
+
await registry.release(names);
|
|
583
|
+
};
|
|
584
|
+
const runDownloads = () => runWithConcurrency$1(flat, Math.max(1, fetchConcurrency), async (item, index) => {
|
|
529
585
|
if (degraded.has(item.table)) return;
|
|
530
586
|
signal?.throwIfAborted();
|
|
531
587
|
const hashSlug = item.file.contentHash ? await contentHashSlug(item.file.contentHash) : void 0;
|
|
@@ -542,9 +598,16 @@ async function attachOpfsParquetTables(options) {
|
|
|
542
598
|
}
|
|
543
599
|
throw err;
|
|
544
600
|
}
|
|
545
|
-
|
|
546
|
-
await
|
|
547
|
-
|
|
601
|
+
try {
|
|
602
|
+
await registry.acquire(name, () => result.handle);
|
|
603
|
+
acquiredNames.add(name);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
if (isAbortError$1(err)) throw err;
|
|
606
|
+
if (isOpfsAccessHandleConflict(err)) {
|
|
607
|
+
degraded.add(item.table);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
throw err;
|
|
548
611
|
}
|
|
549
612
|
const list = tableFiles.get(item.table) ?? [];
|
|
550
613
|
list.push({
|
|
@@ -562,24 +625,45 @@ async function attachOpfsParquetTables(options) {
|
|
|
562
625
|
outcome: result.outcome
|
|
563
626
|
});
|
|
564
627
|
});
|
|
628
|
+
try {
|
|
629
|
+
await runDownloads();
|
|
630
|
+
} catch (err) {
|
|
631
|
+
await registry.release([...acquiredNames]).catch(() => {});
|
|
632
|
+
throw err;
|
|
633
|
+
}
|
|
565
634
|
const attached = [];
|
|
566
635
|
const registeredNames = [];
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
636
|
+
for (const t of tables) {
|
|
637
|
+
if (degraded.has(t.table)) {
|
|
638
|
+
await releaseTable(t.table);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const files = tableFiles.get(t.table) ?? [];
|
|
642
|
+
if (files.length !== t.files.length) {
|
|
643
|
+
degraded.add(t.table);
|
|
644
|
+
await releaseTable(t.table);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
signal?.throwIfAborted();
|
|
649
|
+
await conn.query(readParquetViewSql$1(schema, t.table, files.map((f) => f.name)));
|
|
650
|
+
} catch (err) {
|
|
651
|
+
if (isAbortError$1(err)) {
|
|
652
|
+
await detachOpfs(registry, conn, schema, attached, [...acquiredNames]).catch(() => {});
|
|
653
|
+
throw err;
|
|
654
|
+
}
|
|
655
|
+
if (isOpfsAccessHandleConflict(err)) {
|
|
656
|
+
if (registry.viewRefs(`${schema}.${t.table}`) === 0) await conn.query(`DROP VIEW IF EXISTS ${schema}.${t.table}`).catch(() => {});
|
|
572
657
|
degraded.add(t.table);
|
|
658
|
+
await releaseTable(t.table);
|
|
573
659
|
continue;
|
|
574
660
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
attached.push(t.table);
|
|
578
|
-
for (const f of files) registeredNames.push(f.name);
|
|
661
|
+
await detachOpfs(registry, conn, schema, attached, [...acquiredNames]).catch(() => {});
|
|
662
|
+
throw err;
|
|
579
663
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
664
|
+
attached.push(t.table);
|
|
665
|
+
registry.acquireView(`${schema}.${t.table}`);
|
|
666
|
+
for (const f of files) registeredNames.push(f.name);
|
|
583
667
|
}
|
|
584
668
|
let detached = false;
|
|
585
669
|
return {
|
|
@@ -591,12 +675,13 @@ async function attachOpfsParquetTables(options) {
|
|
|
591
675
|
async detach() {
|
|
592
676
|
if (detached) return;
|
|
593
677
|
detached = true;
|
|
594
|
-
await detachOpfs(
|
|
678
|
+
await detachOpfs(registry, conn, schema, attached, registeredNames);
|
|
595
679
|
}
|
|
596
680
|
};
|
|
597
681
|
}
|
|
598
|
-
async function detachOpfs(
|
|
599
|
-
for (const table of tables) await conn.query(`DROP VIEW IF EXISTS ${schema}.${table}`).
|
|
682
|
+
async function detachOpfs(registry, conn, schema, tables, files) {
|
|
683
|
+
for (const table of tables) await registry.releaseView(`${schema}.${table}`, () => conn.query(`DROP VIEW IF EXISTS ${schema}.${table}`).then(() => {}));
|
|
684
|
+
await registry.release(files);
|
|
600
685
|
}
|
|
601
686
|
async function clearOpfsSnapshotCache() {
|
|
602
687
|
const root = await getOpfsRoot().catch(() => null);
|
|
@@ -761,13 +846,19 @@ async function bootDuckDBWasm(options = {}) {
|
|
|
761
846
|
const workerUrl = URL.createObjectURL(new Blob([`importScripts("${bundle.mainWorker}");`], { type: "text/javascript" }));
|
|
762
847
|
const worker = new Worker(workerUrl);
|
|
763
848
|
const db = new AsyncDuckDB(options.logger ?? new ConsoleLogger(), worker);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
849
|
+
try {
|
|
850
|
+
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
|
|
851
|
+
await db.open(rangeOnlyConfig(options.config));
|
|
852
|
+
return {
|
|
853
|
+
db,
|
|
854
|
+
conn: await db.connect()
|
|
855
|
+
};
|
|
856
|
+
} catch (err) {
|
|
857
|
+
worker.terminate();
|
|
858
|
+
throw err;
|
|
859
|
+
} finally {
|
|
860
|
+
URL.revokeObjectURL(workerUrl);
|
|
861
|
+
}
|
|
771
862
|
}
|
|
772
863
|
async function attachParquetTables(options) {
|
|
773
864
|
const { db, conn, tables, schema = "main" } = options;
|
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.24.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",
|
|
@@ -44,19 +44,23 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"drizzle-orm": "
|
|
48
|
-
"@gscdump/
|
|
49
|
-
"
|
|
50
|
-
"gscdump": "0.
|
|
47
|
+
"drizzle-orm": "1.0.0-rc.3",
|
|
48
|
+
"@gscdump/sdk": "0.24.0",
|
|
49
|
+
"gscdump": "0.24.0",
|
|
50
|
+
"@gscdump/engine": "0.24.0"
|
|
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",
|
|
56
|
+
"playwright": "^1.60.0",
|
|
54
57
|
"vitest": "^4.1.7"
|
|
55
58
|
},
|
|
56
59
|
"scripts": {
|
|
57
60
|
"build": "obuild",
|
|
58
61
|
"dev": "obuild --stub",
|
|
59
62
|
"typecheck": "tsc --noEmit",
|
|
60
|
-
"test": "vitest"
|
|
63
|
+
"test": "vitest",
|
|
64
|
+
"test:browser": "GSCDUMP_E2E=1 vitest --run --config vitest.browser.config.ts"
|
|
61
65
|
}
|
|
62
66
|
}
|