@gscdump/engine 0.38.2 → 0.40.1

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/README.md CHANGED
@@ -37,21 +37,20 @@ Optional peers (install only what your runtime needs):
37
37
  | `@gscdump/engine/scope` | Multi-tenant scope predicates. |
38
38
  | `@gscdump/engine/arrow` | Apache Arrow utilities for engine result conversion. |
39
39
  | `@gscdump/engine/node` | Node-only DuckDB handle. |
40
- | `@gscdump/engine/node-harness` | Node test harness for engine integration tests. |
41
40
  | `@gscdump/engine/filesystem` | Node-only `DataSource` + `ManifestStore` adapters. |
42
- | `@gscdump/engine/http` | Read-only HTTP `DataSource` (signed URLs, Range requests). |
43
41
  | `@gscdump/engine/hyparquet` | Pure-JS `ParquetCodec`. |
44
42
  | `@gscdump/engine/r2` | Cloudflare R2 `DataSource` (structurally typed against `R2Bucket`). |
45
43
  | `@gscdump/engine/r2-manifest` | R2-backed `ManifestStore` for hosted deployments. |
46
- | `@gscdump/engine/inspection-sqlite-node` | Node SQLite adapter for URL-inspection cache. |
47
- | `@gscdump/engine/inspection-sqlite-browser` | Browser (wa-sqlite) adapter for URL-inspection cache. |
44
+ | `@gscdump/engine/iceberg` | Edge-safe GSC Iceberg schema/catalog/append sink. |
45
+ | `@gscdump/engine/sink-node` | Node-only Iceberg overwrite/delete recovery writer. |
46
+ | `@gscdump/engine/source` | Query-source contracts and factories. |
48
47
 
49
48
  ## Stability
50
49
 
51
50
  | Surface | Stability |
52
51
  |---|---|
53
52
  | `createStorageEngine` and storage contracts (`StorageEngine`, `Row`, `WriteCtx`, ...) | Public |
54
- | Adapters (`/node`, `/filesystem`, `/http`, `/hyparquet`, `/r2`) | Public |
53
+ | Adapters (`/node`, `/filesystem`, `/hyparquet`, `/r2`, `/r2-manifest`) | Public |
55
54
  | Planner (`resolveToSQL`, `enumeratePartitions`) | Public |
56
55
  | Schema (`SCHEMAS`, `allTables`, ...) | Public |
57
56
  | Internals reached through `@gscdump/engine/<file>` paths not listed above | Private — may break between minors |
@@ -60,7 +59,6 @@ Optional peers (install only what your runtime needs):
60
59
 
61
60
  - [`gscdump`](../gscdump) — REST client + query builder (edge-safe peer dep).
62
61
  - [`@gscdump/analysis`](../analysis) — analyzers; consumes `StorageEngine` via `createEngine` factories.
63
- - [`@gscdump/engine-duckdb-node`](../engine-duckdb-node) — Node DuckDB analyzer adapter.
64
62
  - [`@gscdump/engine-duckdb-wasm`](../engine-duckdb-wasm) — DuckDB-WASM browser adapter.
65
63
  - [`@gscdump/engine-sqlite`](../engine-sqlite) — SQLite / D1 adapter.
66
64
  - [`@gscdump/cli`](../cli) — CLI wrapping engine + analysis.
@@ -189,42 +189,46 @@ function splitOverlappingTiers(entries, queryRange) {
189
189
  const spanned = [];
190
190
  const kept = [];
191
191
  const subsumed = [];
192
+ const clampToRange = Number.isFinite(rangeStartMs) && Number.isFinite(rangeEndMs);
192
193
  for (const entry of entries) {
193
194
  const span = partitionSpan(entry.partition);
194
195
  if (!span) {
195
196
  kept.push(entry);
196
197
  continue;
197
198
  }
198
- const days = [];
199
- for (let t = span.startMs; t <= span.endMs; t += MS_PER_DAY) {
200
- if (rangeStartMs !== void 0 && (t < rangeStartMs || t > rangeEndMs)) continue;
201
- days.push(t);
202
- }
203
- if (queryRange && days.length === 0) {
199
+ const startMs = clampToRange ? Math.max(span.startMs, rangeStartMs) : span.startMs;
200
+ const endMs = clampToRange ? Math.min(span.endMs, rangeEndMs) : span.endMs;
201
+ if (queryRange && startMs > endMs) {
204
202
  subsumed.push(entry);
205
203
  continue;
206
204
  }
207
205
  spanned.push({
208
206
  entry,
209
207
  rank: span.rank,
210
- days
208
+ startMs,
209
+ endMs
211
210
  });
212
211
  }
213
212
  spanned.sort((a, b) => a.rank - b.rank || b.entry.createdAt - a.entry.createdAt);
214
213
  const coveredBySearchType = /* @__PURE__ */ new Map();
215
- for (const { entry, days } of spanned) {
214
+ for (const { entry, startMs, endMs } of spanned) {
216
215
  const slice = inferSearchType(entry);
217
216
  let covered = coveredBySearchType.get(slice);
218
217
  if (!covered) {
219
218
  covered = /* @__PURE__ */ new Set();
220
219
  coveredBySearchType.set(slice, covered);
221
220
  }
222
- if (days.every((d) => covered.has(d))) {
221
+ let fullyCovered = true;
222
+ for (let dayMs = startMs; dayMs <= endMs; dayMs += MS_PER_DAY) if (!covered.has(dayMs)) {
223
+ fullyCovered = false;
224
+ break;
225
+ }
226
+ if (fullyCovered) {
223
227
  subsumed.push(entry);
224
228
  continue;
225
229
  }
226
230
  kept.push(entry);
227
- for (const d of days) covered.add(d);
231
+ for (let dayMs = startMs; dayMs <= endMs; dayMs += MS_PER_DAY) covered.add(dayMs);
228
232
  }
229
233
  return {
230
234
  kept,
@@ -14,8 +14,16 @@ interface DuckDBHandle {
14
14
  interface DuckDBFactory {
15
15
  getDuckDB: () => Promise<DuckDBHandle>;
16
16
  }
17
- declare function createDuckDBCodec(factory: DuckDBFactory): ParquetCodec;
18
- declare function createDuckDBExecutor(factory: DuckDBFactory): QueryExecutor;
17
+ interface DuckDBReadOptions {
18
+ /**
19
+ * Maximum non-URI parquet reads held in flight while registering DuckDB
20
+ * buffers. Defaults to 16; callers handling unusually large files can lower
21
+ * it to trade latency for peak JS memory.
22
+ */
23
+ bufferReadConcurrency?: number;
24
+ }
25
+ declare function createDuckDBCodec(factory: DuckDBFactory, options?: DuckDBReadOptions): ParquetCodec;
26
+ declare function createDuckDBExecutor(factory: DuckDBFactory, options?: DuckDBReadOptions): QueryExecutor;
19
27
  /**
20
28
  * Canonical "empty-file" SELECT clause for a table. Codecs that need to
21
29
  * emit a schema-correct empty Parquet can wrap this in:
@@ -8,11 +8,16 @@ import { sqlEscape } from "../sql-bind.mjs";
8
8
  import { encodeJsonBigintSafe } from "@gscdump/lakehouse";
9
9
  import { buildLogicalPlan } from "gscdump/query/plan";
10
10
  import { normalizeUrl } from "gscdump/normalize";
11
- const BUFFER_READ_BATCH_SIZE = 8;
12
- async function registerBufferedFiles(db, files, dataSource, registered, signal) {
13
- for (let i = 0; i < files.length; i += BUFFER_READ_BATCH_SIZE) {
11
+ const DEFAULT_BUFFER_READ_CONCURRENCY = 16;
12
+ const MAX_BUFFER_READ_CONCURRENCY = 64;
13
+ function bufferReadConcurrency(value, fileCount) {
14
+ return Math.max(1, Math.min(fileCount || 1, typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : DEFAULT_BUFFER_READ_CONCURRENCY, MAX_BUFFER_READ_CONCURRENCY));
15
+ }
16
+ async function registerBufferedFiles(db, files, dataSource, registered, signal, maxConcurrency) {
17
+ const batchSize = bufferReadConcurrency(maxConcurrency, files.length);
18
+ for (let i = 0; i < files.length; i += batchSize) {
14
19
  signal?.throwIfAborted();
15
- const batch = files.slice(i, i + BUFFER_READ_BATCH_SIZE);
20
+ const batch = files.slice(i, i + batchSize);
16
21
  const buffers = await Promise.all(batch.map((file) => dataSource.read(file.key, void 0, signal)));
17
22
  for (let j = 0; j < batch.length; j++) {
18
23
  signal?.throwIfAborted();
@@ -47,7 +52,7 @@ async function decodeBytes(db, bytes, table) {
47
52
  await db.dropFiles([name]);
48
53
  }
49
54
  }
50
- function createDuckDBCodec(factory) {
55
+ function createDuckDBCodec(factory, options = {}) {
51
56
  return {
52
57
  async writeRows(ctx, rows, key, dataSource) {
53
58
  const bytes = await encodeBytes(await factory.getDuckDB(), ctx.table, rows);
@@ -100,7 +105,7 @@ function createDuckDBCodec(factory) {
100
105
  };
101
106
  });
102
107
  try {
103
- await registerBufferedFiles(db, bufferedInputs, dataSource, registered);
108
+ await registerBufferedFiles(db, bufferedInputs, dataSource, registered, void 0, options.bufferReadConcurrency);
104
109
  const fileList = inNames.map((n) => `'${sqlEscape(n)}'`).join(", ");
105
110
  await db.query(`COPY (${dedupedMergeSql(ctx.table, fileList)}) TO '${sqlEscape(outName)}' (FORMAT PARQUET, COMPRESSION ZSTD)`);
106
111
  registered.push(outName);
@@ -137,7 +142,7 @@ function rewriteEmptyFileSets(sql, placeholders, defaultTable, placeholderTables
137
142
  }
138
143
  return out;
139
144
  }
140
- function createDuckDBExecutor(factory) {
145
+ function createDuckDBExecutor(factory, options = {}) {
141
146
  return { async execute({ sql, params, fileKeys, placeholderTables, dataSource, table, signal, profiler }) {
142
147
  signal?.throwIfAborted();
143
148
  const db = await factory.getDuckDB();
@@ -146,24 +151,24 @@ function createDuckDBExecutor(factory) {
146
151
  const totalFiles = Object.values(fileKeys).reduce((n, keys) => n + keys.length, 0);
147
152
  const endRegister = profiler?.start("files.register", { files: totalFiles });
148
153
  try {
149
- await Promise.all(Object.entries(fileKeys).map(async ([name, keys]) => {
154
+ const bufferedByName = /* @__PURE__ */ new Map();
155
+ for (const [name, keys] of Object.entries(fileKeys)) {
150
156
  const resolved = [];
151
- const buffered = [];
152
157
  for (let i = 0; i < keys.length; i++) {
153
158
  const key = keys[i];
154
159
  const uri = dataSource.uri?.(key);
155
160
  if (uri !== void 0) resolved.push(uri);
156
161
  else {
157
- buffered.push({
162
+ if (!bufferedByName.has(key)) bufferedByName.set(key, {
158
163
  key,
159
164
  name: key
160
165
  });
161
166
  resolved.push(key);
162
167
  }
163
168
  }
164
- await registerBufferedFiles(db, buffered, dataSource, registered, signal);
165
169
  placeholders[name] = resolved;
166
- }));
170
+ }
171
+ await registerBufferedFiles(db, [...bufferedByName.values()], dataSource, registered, signal, options.bufferReadConcurrency);
167
172
  endRegister?.({ buffered: registered.length });
168
173
  signal?.throwIfAborted();
169
174
  const finalSql = substituteNamedFiles(rewriteEmptyFileSets(sql, placeholders, table, placeholderTables), placeholders);
@@ -9,9 +9,6 @@ function isMissingKeyError(e) {
9
9
  return /\bnot found\b|\bENOENT\b|\bmissing key\b/i.test(message);
10
10
  }
11
11
  async function readOptional(ds, key, signal) {
12
- if (ds.head) {
13
- if (await ds.head(key) === void 0) return void 0;
14
- }
15
12
  return await ds.read(key, void 0, signal).catch((e) => {
16
13
  if (isMissingKeyError(e)) return void 0;
17
14
  throw e;
@@ -72,9 +69,6 @@ function buildQueryDimRecords(queries, deps) {
72
69
  return out;
73
70
  }
74
71
  function createQueryDimStore({ dataSource }) {
75
- async function exists(key, prefix) {
76
- return (await dataSource.list(prefix)).includes(key);
77
- }
78
72
  return {
79
73
  parquetKey: queryDimParquetKey,
80
74
  async write(ctx, records, builtAt) {
@@ -98,15 +92,14 @@ function createQueryDimStore({ dataSource }) {
98
92
  };
99
93
  },
100
94
  async loadMeta(ctx) {
101
- const key = queryDimMetaKey(ctx);
102
- if (!await exists(key, `${queryDimPrefix(ctx)}/`)) return null;
103
- const bytes = await dataSource.read(key);
95
+ const bytes = await readOptional(dataSource, queryDimMetaKey(ctx));
96
+ if (!bytes) return null;
104
97
  return JSON.parse(new TextDecoder().decode(bytes));
105
98
  },
106
99
  async loadRecords(ctx) {
107
- const key = queryDimParquetKey(ctx);
108
- if (!await exists(key, `${queryDimPrefix(ctx)}/`)) return [];
109
- return (await decodeParquetToRows(await dataSource.read(key))).map((r) => ({
100
+ const bytes = await readOptional(dataSource, queryDimParquetKey(ctx));
101
+ if (!bytes) return [];
102
+ return (await decodeParquetToRows(bytes)).map((r) => ({
110
103
  query: String(r.query),
111
104
  query_canonical: String(r.query_canonical),
112
105
  intent_code: Number(r.intent_code),
@@ -117,6 +110,21 @@ function createQueryDimStore({ dataSource }) {
117
110
  };
118
111
  }
119
112
  const YEAR_MONTH_RE = /^(\d{4})-(\d{2})-/;
113
+ const ENTITY_IO_CONCURRENCY = 8;
114
+ async function mapEntityIo(items, fn) {
115
+ if (items.length === 0) return [];
116
+ const results = Array.from({ length: items.length }, () => void 0);
117
+ let next = 0;
118
+ async function worker() {
119
+ while (true) {
120
+ const index = next++;
121
+ if (index >= items.length) return;
122
+ results[index] = await fn(items[index], index);
123
+ }
124
+ }
125
+ await Promise.all(Array.from({ length: Math.min(ENTITY_IO_CONCURRENCY, items.length) }, worker));
126
+ return results;
127
+ }
120
128
  function inspectionIndexKey(ctx) {
121
129
  return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/inspections/index.json` : `u_${ctx.userId}/entities/inspections/index.json`;
122
130
  }
@@ -311,33 +319,33 @@ function createInspectionStore(opts) {
311
319
  if (!byMonth.has(month)) byMonth.set(month, []);
312
320
  byMonth.get(month).push(r);
313
321
  }
314
- for (const [yearMonth, batch] of byMonth) {
322
+ await mapEntityIo([...byMonth].map(([yearMonth, batch]) => {
315
323
  const bytes = encodeJsonBigintSafe({
316
324
  version: 1,
317
325
  records: batch
318
326
  });
319
327
  if (bytes.byteLength > 5242880) throw new Error(`inspection history shard exceeds ${INSPECTION_HISTORY_MAX_BYTES} bytes (got ${bytes.byteLength}); split the batch`);
320
- await ds.write(inspectionHistoryShardKey(ctx, yearMonth, batchId), bytes);
321
- }
328
+ return {
329
+ key: inspectionHistoryShardKey(ctx, yearMonth, batchId),
330
+ bytes
331
+ };
332
+ }), (shard) => ds.write(shard.key, shard.bytes));
322
333
  },
323
334
  async loadHistory(ctx, yearMonth) {
324
335
  const keys = await ds.list(inspectionHistoryPrefix(ctx, yearMonth));
325
336
  if (keys.length === 0) return void 0;
326
- const out = [];
327
- for (const key of keys) {
328
- const bytes = await readOptional(ds, key);
329
- if (!bytes) continue;
330
- const shard = await Promise.resolve().then(() => JSON.parse(new TextDecoder().decode(bytes))).catch((err) => {
331
- console.warn("[inspection.loadHistory] failed to decode shard", {
332
- key,
333
- error: err.message
334
- });
335
- });
336
- if (shard?.records) out.push(...shard.records);
337
- }
338
337
  return {
339
338
  version: 1,
340
- records: out
339
+ records: (await mapEntityIo(keys, async (key) => {
340
+ const bytes = await readOptional(ds, key);
341
+ if (!bytes) return [];
342
+ return (await Promise.resolve().then(() => JSON.parse(new TextDecoder().decode(bytes))).catch((err) => {
343
+ console.warn("[inspection.loadHistory] failed to decode shard", {
344
+ key,
345
+ error: err.message
346
+ });
347
+ }))?.records ?? [];
348
+ })).flat()
341
349
  };
342
350
  },
343
351
  async materialize(ctx, rowIter) {
@@ -369,18 +377,19 @@ function createInspectionStore(opts) {
369
377
  bucket.push(r);
370
378
  byMonth.set(month, bucket);
371
379
  }
372
- const keys = [];
373
- for (const [month, batch] of byMonth) {
380
+ const files = [...byMonth].map(([month, batch]) => {
374
381
  const bytes = encodeRowsToParquetFlex(batch, {
375
382
  columns: INSPECTION_EVENT_COLUMNS,
376
383
  sortKey: ["urlHash"]
377
384
  });
378
- const key = inspectionEventKey(ctx, month, batchId);
379
- await ds.write(key, bytes);
380
- keys.push(key);
381
- }
385
+ return {
386
+ key: inspectionEventKey(ctx, month, batchId),
387
+ bytes
388
+ };
389
+ });
390
+ await mapEntityIo(files, (file) => ds.write(file.key, file.bytes));
382
391
  return {
383
- keys,
392
+ keys: files.map((file) => file.key),
384
393
  rowCount: rows.length
385
394
  };
386
395
  },
@@ -410,11 +419,18 @@ function createInspectionStore(opts) {
410
419
  for (const row of baseRows) consider(row);
411
420
  let eventsFolded = 0;
412
421
  const consumed = [];
413
- for (const key of eventKeys.sort()) {
422
+ const eventFiles = await mapEntityIo(eventKeys.sort(), async (key) => {
414
423
  const bytes = await readOptional(ds, key);
415
- if (!bytes) continue;
424
+ if (!bytes) return void 0;
425
+ return {
426
+ key,
427
+ rows: await decodeParquetToRows(bytes)
428
+ };
429
+ });
430
+ for (const file of eventFiles) {
431
+ if (!file) continue;
432
+ const { key, rows } = file;
416
433
  consumed.push(key);
417
- const rows = await decodeParquetToRows(bytes);
418
434
  for (const row of rows) {
419
435
  consider(row);
420
436
  eventsFolded++;
@@ -610,16 +626,18 @@ function createSitemapStore(opts) {
610
626
  records: {}
611
627
  };
612
628
  const stamp = now();
629
+ const historyDocs = /* @__PURE__ */ new Map();
613
630
  for (const r of records) {
614
631
  const h = hash(r.path);
615
632
  index.records[h] = r;
616
- await writeJson(sitemapHistoryKey(ctx, h, stamp), {
633
+ historyDocs.set(sitemapHistoryKey(ctx, h, stamp), {
617
634
  version: 1,
618
635
  path: r.path,
619
636
  capturedAt: r.capturedAt,
620
637
  record: r
621
638
  });
622
639
  }
640
+ await mapEntityIo([...historyDocs], ([key, doc]) => writeJson(key, doc));
623
641
  await writeJson(indexKey, index);
624
642
  },
625
643
  async loadIndex(ctx) {
@@ -702,9 +720,10 @@ function createSitemapStore(opts) {
702
720
  async *loadUrls(ctx, feedpath, opts) {
703
721
  const fpHash = hash(feedpath);
704
722
  const includeRemoved = opts?.includeRemoved ?? false;
705
- const indexBytes = await readOptional(ds, sitemapUrlsIndexKey(ctx, fpHash));
706
- const indexRows = indexBytes ? await decodeParquetToRows(indexBytes) : [];
707
- const deltaKeys = (await ds.list(`${sitemapUrlsPrefix(ctx)}/deltas/`)).sort();
723
+ const [indexRows, listedDeltaKeys] = await Promise.all([readOptional(ds, sitemapUrlsIndexKey(ctx, fpHash)).then((bytes) => bytes ? decodeParquetToRows(bytes) : []), ds.list(`${sitemapUrlsPrefix(ctx)}/deltas/`)]);
724
+ const deltaKeys = listedDeltaKeys.filter((key) => {
725
+ return SITEMAP_URLS_DELTA_PREFIX_RE.exec(key)?.[2] === fpHash;
726
+ }).sort();
708
727
  const live = /* @__PURE__ */ new Map();
709
728
  const removedMap = /* @__PURE__ */ new Map();
710
729
  for (const row of indexRows) {
@@ -712,36 +731,34 @@ function createSitemapStore(opts) {
712
731
  if (rec.removedAt != null) removedMap.set(rec.urlHash, rec);
713
732
  else live.set(rec.urlHash, rec);
714
733
  }
715
- for (const key of deltaKeys) {
716
- const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
717
- if (!m || m[2] !== fpHash) continue;
734
+ const deltas = await mapEntityIo(deltaKeys, async (key) => {
718
735
  const dBytes = await readOptional(ds, key);
719
- if (!dBytes) continue;
720
- const dRows = await decodeParquetToRows(dBytes);
721
- for (const r of dRows) {
722
- const op = String(r.op);
723
- const urlHash = String(r.url_hash);
724
- const at = Number(r.at);
725
- if (op === "added") {
726
- const prev = live.get(urlHash) ?? removedMap.get(urlHash);
727
- removedMap.delete(urlHash);
728
- live.set(urlHash, {
729
- feedpath,
730
- feedpathHash: fpHash,
731
- urlHash,
732
- loc: String(r.loc),
733
- lastmod: r.lastmod == null ? void 0 : String(r.lastmod),
734
- firstSeenAt: prev?.firstSeenAt ?? at,
735
- lastSeenAt: at
736
- });
737
- } else if (op === "removed") {
738
- const prev = live.get(urlHash);
739
- live.delete(urlHash);
740
- if (prev) removedMap.set(urlHash, {
741
- ...prev,
742
- removedAt: at
743
- });
744
- }
736
+ if (!dBytes) return [];
737
+ return decodeParquetToRows(dBytes);
738
+ });
739
+ for (const dRows of deltas) for (const r of dRows) {
740
+ const op = String(r.op);
741
+ const urlHash = String(r.url_hash);
742
+ const at = Number(r.at);
743
+ if (op === "added") {
744
+ const prev = live.get(urlHash) ?? removedMap.get(urlHash);
745
+ removedMap.delete(urlHash);
746
+ live.set(urlHash, {
747
+ feedpath,
748
+ feedpathHash: fpHash,
749
+ urlHash,
750
+ loc: String(r.loc),
751
+ lastmod: r.lastmod == null ? void 0 : String(r.lastmod),
752
+ firstSeenAt: prev?.firstSeenAt ?? at,
753
+ lastSeenAt: at
754
+ });
755
+ } else if (op === "removed") {
756
+ const prev = live.get(urlHash);
757
+ live.delete(urlHash);
758
+ if (prev) removedMap.set(urlHash, {
759
+ ...prev,
760
+ removedAt: at
761
+ });
745
762
  }
746
763
  }
747
764
  for (const rec of live.values()) yield rec;
@@ -796,8 +813,14 @@ function createSitemapStore(opts) {
796
813
  for (const [fpHash, feedDeltaKeys] of deltasByFeed) {
797
814
  if (compactedFeedpaths > 0 && (compactedFeedpaths >= maxFeedpaths || now() - startedAt >= deadlineMs)) break;
798
815
  const indexKey = sitemapUrlsIndexKey(ctx, fpHash);
799
- const indexBytes = await readOptional(ds, indexKey);
800
- const indexRows = indexBytes ? await decodeParquetToRows(indexBytes) : [];
816
+ const [indexRows, deltaFiles] = await Promise.all([readOptional(ds, indexKey).then((bytes) => bytes ? decodeParquetToRows(bytes) : []), mapEntityIo(feedDeltaKeys.sort(), async (key) => {
817
+ const bytes = await readOptional(ds, key);
818
+ if (!bytes) return void 0;
819
+ return {
820
+ key,
821
+ rows: await decodeParquetToRows(bytes)
822
+ };
823
+ })]);
801
824
  const live = /* @__PURE__ */ new Map();
802
825
  const removed = /* @__PURE__ */ new Map();
803
826
  for (const row of indexRows) {
@@ -806,11 +829,10 @@ function createSitemapStore(opts) {
806
829
  else live.set(rec.urlHash, rec);
807
830
  }
808
831
  const consumed = [];
809
- for (const key of feedDeltaKeys.sort()) {
810
- const bytes = await readOptional(ds, key);
811
- if (!bytes) continue;
832
+ for (const file of deltaFiles) {
833
+ if (!file) continue;
834
+ const { key, rows } = file;
812
835
  consumed.push(key);
813
- const rows = await decodeParquetToRows(bytes);
814
836
  for (const r of rows) {
815
837
  const urlHash = String(r.url_hash);
816
838
  const at = Number(r.at);
@@ -856,12 +878,13 @@ function createSitemapStore(opts) {
856
878
  const at = atOpt ?? now();
857
879
  const liveHashes = new Set(liveFeedpaths.map((fp) => hash(fp)));
858
880
  const present = /* @__PURE__ */ new Set();
859
- for (const key of await ds.list(`${sitemapUrlsIndexPrefix(ctx)}/`)) {
881
+ const [indexKeys, deltaKeys] = await Promise.all([ds.list(`${sitemapUrlsIndexPrefix(ctx)}/`), ds.list(`${sitemapUrlsPrefix(ctx)}/deltas/`)]);
882
+ for (const key of indexKeys) {
860
883
  const m = /\/by-feed\/([0-9a-f]+)\/index\.parquet$/.exec(key);
861
884
  if (m) present.add(m[1]);
862
885
  }
863
886
  const deltasByFeed = /* @__PURE__ */ new Map();
864
- for (const key of await ds.list(`${sitemapUrlsPrefix(ctx)}/deltas/`)) {
887
+ for (const key of deltaKeys) {
865
888
  const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
866
889
  if (!m) continue;
867
890
  present.add(m[2]);
@@ -874,8 +897,14 @@ function createSitemapStore(opts) {
874
897
  for (const fpHash of present) {
875
898
  if (liveHashes.has(fpHash)) continue;
876
899
  const indexKey = sitemapUrlsIndexKey(ctx, fpHash);
877
- const indexBytes = await readOptional(ds, indexKey);
878
- const indexRows = indexBytes ? await decodeParquetToRows(indexBytes) : [];
900
+ const [indexRows, deltaFiles] = await Promise.all([readOptional(ds, indexKey).then((bytes) => bytes ? decodeParquetToRows(bytes) : []), mapEntityIo((deltasByFeed.get(fpHash) ?? []).sort(), async (key) => {
901
+ const bytes = await readOptional(ds, key);
902
+ if (!bytes) return void 0;
903
+ return {
904
+ key,
905
+ rows: await decodeParquetToRows(bytes)
906
+ };
907
+ })]);
879
908
  const live = /* @__PURE__ */ new Map();
880
909
  const removed = /* @__PURE__ */ new Map();
881
910
  for (const row of indexRows) {
@@ -884,11 +913,10 @@ function createSitemapStore(opts) {
884
913
  else live.set(r.urlHash, r);
885
914
  }
886
915
  const consumed = [];
887
- for (const key of (deltasByFeed.get(fpHash) ?? []).sort()) {
888
- const bytes = await readOptional(ds, key);
889
- if (!bytes) continue;
916
+ for (const file of deltaFiles) {
917
+ if (!file) continue;
918
+ const { key, rows } = file;
890
919
  consumed.push(key);
891
- const rows = await decodeParquetToRows(bytes);
892
920
  for (const r of rows) {
893
921
  const urlHash = String(r.url_hash);
894
922
  const dat = Number(r.at);
@@ -103,8 +103,6 @@ declare const engineErrors: {
103
103
  readonly lockAcquireTimeout: (scope: string, timeoutMs: number) => EngineError;
104
104
  };
105
105
  declare function isEngineError(value: unknown): value is EngineError;
106
- /** The human-readable rendering of an `EngineError`, for logs and string sinks. */
107
- declare function formatEngineError(error: EngineError): string;
108
106
  /**
109
107
  * Re-raises an `EngineError` value as a generic `Error`, stashing the union under
110
108
  * `.engineError` for stack-walking. Used by the throwing wrappers over the
@@ -114,4 +112,4 @@ declare function formatEngineError(error: EngineError): string;
114
112
  * mapper so the class identity callers match on is preserved.
115
113
  */
116
114
  declare function engineErrorToException(error: EngineError): Error;
117
- export { EngineError, EngineErrorKind, engineErrorToException, engineErrors, formatEngineError, isEngineError };
115
+ export { EngineError, EngineErrorKind, engineErrorToException, engineErrors, isEngineError };
@@ -105,14 +105,5 @@ declare function createEngineQuerySource(options: EngineQuerySourceOptions): Ana
105
105
  declare function runAnalyzerWithEngine(deps: {
106
106
  engine: StorageEngine;
107
107
  }, ctx: TenantCtx, params: AnalysisParams, registry: AnalyzerRegistry): Promise<AnalysisResult>;
108
- interface TypedQuery<TRow> {
109
- state: BuilderState;
110
- readonly __row?: TRow;
111
- }
112
- declare function typedQuery<TRow>(state: BuilderState): TypedQuery<TRow>;
113
- declare function queryRows<TRow = QueryRow>(source: AnalysisQuerySource, query: BuilderState | TypedQuery<TRow>): Promise<TRow[]>;
114
- declare function queryComparisonRows<TRow = QueryRow>(source: AnalysisQuerySource, current: BuilderState | TypedQuery<TRow>, previous: BuilderState | TypedQuery<TRow>): Promise<{
115
- current: TRow[];
116
- previous: TRow[];
117
- }>;
118
- export { AttachedTableMissingError, AttachedTableRunner, AttachedTableSourceOptions, CreateSqlQuerySourceOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, TypedQuery, createAttachedTableSource, createEngineQuerySource, createSqlQuerySource, queryComparisonRows, queryRows, rewriteForTableSource, runAnalyzerWithEngine, typedQuery };
108
+ declare function queryRows<TRow = QueryRow>(source: AnalysisQuerySource, state: BuilderState): Promise<TRow[]>;
109
+ export { AttachedTableMissingError, AttachedTableRunner, AttachedTableSourceOptions, CreateSqlQuerySourceOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, createAttachedTableSource, createEngineQuerySource, createSqlQuerySource, queryRows, rewriteForTableSource, runAnalyzerWithEngine };
@@ -30,9 +30,6 @@ declare function defaultStartDate(): string;
30
30
  declare function periodOf(params: AnalysisParams): AnalysisPeriod;
31
31
  declare function comparisonOf(params: AnalysisParams): ComparisonPeriod;
32
32
  declare function resolveWindow(opts: ResolveWindowOptions): ResolvedWindow;
33
- /** Convert a ResolvedWindow into the AnalysisPeriod / ComparisonPeriod shape. */
34
- declare function windowToPeriod(w: ResolvedWindow): AnalysisPeriod;
35
- declare function windowToComparisonPeriod(w: ResolvedWindow): ComparisonPeriod | undefined;
36
33
  interface PadTimeseriesOptions<T> {
37
34
  /** ISO date (YYYY-MM-DD), inclusive lower bound. */
38
35
  startDate: string;
@@ -54,4 +51,4 @@ type DateRowShape = Record<string, unknown> & {
54
51
  * once. Existing dates keep all their rows (grouped timeseries safe).
55
52
  */
56
53
  declare function padTimeseries<T extends DateRowShape = DateRowShape>(rows: readonly T[], options: PadTimeseriesOptions<T>): T[];
57
- export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, defaultStartDate, padTimeseries, periodOf, resolveWindow, windowToComparisonPeriod, windowToPeriod };
54
+ export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, defaultStartDate, padTimeseries, periodOf, resolveWindow };
@@ -14,12 +14,6 @@ declare const pgResolverAdapter: ResolverAdapter<PgTableKey>;
14
14
  * accidental adapter caching that would lock in a stale `{{FILES}}` set.
15
15
  */
16
16
  interface ResolverAdapterOptions {
17
- /**
18
- * Deprecated compatibility flag. Fact tables no longer carry
19
- * `query_canonical`; canonical reads derive from `query_dim`, or from a
20
- * canonical rollup relation when `queryCanonicalSource: 'column'` is set.
21
- */
22
- canonicalFallback?: boolean;
23
17
  /**
24
18
  * `queryDim` reads canonical from a joined query dimension. `column` is only
25
19
  * for derived canonical rollup relations whose primary relation already