@gscdump/engine 0.33.0 → 0.33.4

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.
@@ -304,6 +304,12 @@ function txLeaf(leaf, columns) {
304
304
  if (!column || !columns.has(column)) return null;
305
305
  return { [column]: { $eq: leaf.expression } };
306
306
  }
307
+ function combineFilters(parts, groupType) {
308
+ const first = parts[0];
309
+ if (!first) return null;
310
+ if (parts.length === 1) return first;
311
+ return groupType === "or" ? { $or: [...parts] } : { $and: [...parts] };
312
+ }
307
313
  function txExact(node, columns) {
308
314
  const groupType = node._groupType ?? "and";
309
315
  const leafParts = [];
@@ -314,7 +320,7 @@ function txExact(node, columns) {
314
320
  }
315
321
  if (groupType === "or") {
316
322
  if (node._nestedGroups?.length || leafParts.length === 0) return null;
317
- return leafParts.length === 1 ? leafParts[0] : { $or: leafParts };
323
+ return combineFilters(leafParts, "or");
318
324
  }
319
325
  const parts = leafParts;
320
326
  for (const group of node._nestedGroups ?? []) {
@@ -323,7 +329,7 @@ function txExact(node, columns) {
323
329
  parts.push(t);
324
330
  }
325
331
  if (parts.length === 0) return null;
326
- return parts.length === 1 ? parts[0] : { $and: parts };
332
+ return combineFilters(parts, "and");
327
333
  }
328
334
  function extractParquetPushdown(state, table) {
329
335
  const filter = state?.filter;
@@ -341,7 +347,7 @@ function extractParquetPushdown(state, table) {
341
347
  if (t) parts.push(t);
342
348
  }
343
349
  if (parts.length === 0) return void 0;
344
- return parts.length === 1 ? parts[0] : { $and: parts };
350
+ return combineFilters(parts, "and") ?? void 0;
345
351
  }
346
352
  const URL_PURGE_TABLES = ["pages", "page_queries"];
347
353
  const MAX_DAY_BYTES = 100 * 1024 * 1024;
@@ -737,6 +737,7 @@ function createSitemapStore(opts) {
737
737
  const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
738
738
  if (!m) continue;
739
739
  const date = m[1];
740
+ if (!date) continue;
740
741
  if (from && date < from) continue;
741
742
  if (to && date > to) continue;
742
743
  const bytes = await readOptional(ds, key);
@@ -763,9 +764,11 @@ function createSitemapStore(opts) {
763
764
  for (const key of deltaKeys) {
764
765
  const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
765
766
  if (!m) continue;
766
- const list = deltasByFeed.get(m[2]) ?? [];
767
+ const feedpathHash = m[2];
768
+ if (!feedpathHash) continue;
769
+ const list = deltasByFeed.get(feedpathHash) ?? [];
767
770
  list.push(key);
768
- deltasByFeed.set(m[2], list);
771
+ deltasByFeed.set(feedpathHash, list);
769
772
  }
770
773
  for (const [fpHash, feedDeltaKeys] of deltasByFeed) {
771
774
  const indexKey = sitemapUrlsIndexKey(ctx, fpHash);
@@ -0,0 +1,56 @@
1
+ import { inferLegacyTier, inferSearchType } from "./layout.mjs";
2
+ function manifestEntryKey(entry) {
3
+ return entry.objectKey;
4
+ }
5
+ function watermarkKey(watermark) {
6
+ return `${watermark.userId}|${watermark.siteId ?? ""}|${watermark.table}`;
7
+ }
8
+ function syncStateKey(state) {
9
+ return `${state.userId}|${state.siteId ?? ""}|${state.table}|${state.date}|${inferSearchType(state)}`;
10
+ }
11
+ function matchesManifestEntryFilter(entry, filter, options = {}) {
12
+ if (!options.ignoreUserId && entry.userId !== filter.userId) return false;
13
+ if (filter.siteId !== void 0 && entry.siteId !== filter.siteId) return false;
14
+ if (filter.table !== void 0 && entry.table !== filter.table) return false;
15
+ if (filter.partitions && !filter.partitions.includes(entry.partition)) return false;
16
+ if (filter.tier !== void 0 && inferLegacyTier(entry) !== filter.tier) return false;
17
+ if (filter.searchType !== void 0 && inferSearchType(entry) !== filter.searchType) return false;
18
+ return true;
19
+ }
20
+ function matchesWatermarkFilter(watermark, filter, options = {}) {
21
+ if (!options.ignoreUserId && watermark.userId !== filter.userId) return false;
22
+ if (filter.siteId !== void 0 && watermark.siteId !== filter.siteId) return false;
23
+ if (filter.table !== void 0 && watermark.table !== filter.table) return false;
24
+ return true;
25
+ }
26
+ function matchesSyncStateFilter(state, filter, options = {}) {
27
+ if (!options.ignoreUserId && state.userId !== filter.userId) return false;
28
+ if (filter.siteId !== void 0 && state.siteId !== filter.siteId) return false;
29
+ if (filter.table !== void 0 && state.table !== filter.table) return false;
30
+ if (filter.state !== void 0 && state.state !== filter.state) return false;
31
+ if (filter.searchType !== void 0 && inferSearchType(state) !== filter.searchType) return false;
32
+ return true;
33
+ }
34
+ function mergeSyncState(existing, scope, state, detail) {
35
+ const at = detail?.at ?? Date.now();
36
+ const attemptsBump = state === "inflight" ? 1 : 0;
37
+ if (!existing) return {
38
+ userId: scope.userId,
39
+ siteId: scope.siteId,
40
+ table: scope.table,
41
+ date: scope.date,
42
+ state,
43
+ updatedAt: at,
44
+ attempts: attemptsBump,
45
+ error: detail?.error,
46
+ ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {}
47
+ };
48
+ return {
49
+ ...existing,
50
+ state,
51
+ updatedAt: at,
52
+ attempts: existing.attempts + attemptsBump,
53
+ error: state === "done" ? void 0 : detail?.error ?? existing.error
54
+ };
55
+ }
56
+ export { manifestEntryKey, matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter, mergeSyncState, syncStateKey, watermarkKey };
@@ -1,4 +1,4 @@
1
- import { inferLegacyTier, inferSearchType } from "../_chunks/layout.mjs";
1
+ import { manifestEntryKey, matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter, mergeSyncState, syncStateKey, watermarkKey } from "../_chunks/manifest-store-utils.mjs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { randomBytes } from "node:crypto";
@@ -29,13 +29,13 @@ function createFilesystemDataSource(opts) {
29
29
  }));
30
30
  },
31
31
  async list(prefix) {
32
- const full = resolve(root, prefix);
32
+ const full = pathFor(prefix);
33
33
  const out = [];
34
34
  await walk(full, out);
35
35
  return out.map((p) => p.slice(root.length + 1));
36
36
  },
37
37
  async *streamList(prefix) {
38
- const full = resolve(root, prefix);
38
+ const full = pathFor(prefix);
39
39
  for await (const p of walkStream(full)) yield p.slice(root.length + 1);
40
40
  },
41
41
  async head(key) {
@@ -71,57 +71,6 @@ async function walk(dir, out) {
71
71
  else out.push(p);
72
72
  }
73
73
  }
74
- function watermarkKey(w) {
75
- return `${w.userId}|${w.siteId ?? ""}|${w.table}`;
76
- }
77
- function matchesWatermarkFilter(w, filter) {
78
- if (w.userId !== filter.userId) return false;
79
- if (filter.siteId !== void 0 && w.siteId !== filter.siteId) return false;
80
- if (filter.table !== void 0 && w.table !== filter.table) return false;
81
- return true;
82
- }
83
- function syncStateKey(s) {
84
- return `${s.userId}|${s.siteId ?? ""}|${s.table}|${s.date}|${inferSearchType(s)}`;
85
- }
86
- function matchesSyncStateFilter(s, filter) {
87
- if (s.userId !== filter.userId) return false;
88
- if (filter.siteId !== void 0 && s.siteId !== filter.siteId) return false;
89
- if (filter.table !== void 0 && s.table !== filter.table) return false;
90
- if (filter.state !== void 0 && s.state !== filter.state) return false;
91
- if (filter.searchType !== void 0 && inferSearchType(s) !== filter.searchType) return false;
92
- return true;
93
- }
94
- function mergeSyncState(existing, scope, state, detail) {
95
- const at = detail?.at ?? Date.now();
96
- const attemptsBump = state === "inflight" ? 1 : 0;
97
- if (!existing) return {
98
- userId: scope.userId,
99
- siteId: scope.siteId,
100
- table: scope.table,
101
- date: scope.date,
102
- state,
103
- updatedAt: at,
104
- attempts: attemptsBump,
105
- error: detail?.error,
106
- ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {}
107
- };
108
- return {
109
- ...existing,
110
- state,
111
- updatedAt: at,
112
- attempts: existing.attempts + attemptsBump,
113
- error: state === "done" ? void 0 : detail?.error ?? existing.error
114
- };
115
- }
116
- function matchesFilter(entry, filter) {
117
- if (entry.userId !== filter.userId) return false;
118
- if (filter.siteId !== void 0 && entry.siteId !== filter.siteId) return false;
119
- if (filter.table !== void 0 && entry.table !== filter.table) return false;
120
- if (filter.partitions && !filter.partitions.includes(entry.partition)) return false;
121
- if (filter.tier !== void 0 && inferLegacyTier(entry) !== filter.tier) return false;
122
- if (filter.searchType !== void 0 && inferSearchType(entry) !== filter.searchType) return false;
123
- return true;
124
- }
125
74
  function lockFileFor(locksDir, scope) {
126
75
  return join(locksDir, `${`${scope.userId}|${scope.siteId ?? ""}|${scope.table}|${scope.partition}`.replace(/[^\w.-]/g, "_")}.lock`);
127
76
  }
@@ -166,30 +115,27 @@ function createFilesystemManifestStore(opts) {
166
115
  while (queue.length > 0) await queue.shift()().catch(() => {});
167
116
  running = false;
168
117
  }
169
- function entryKey(e) {
170
- return e.objectKey;
171
- }
172
118
  async function registerVersionsImpl(newEntries, superseding) {
173
119
  const data = await load();
174
120
  const supersededAt = newEntries[0]?.createdAt ?? Date.now();
175
- const byKey = new Map(data.entries.map((e) => [entryKey(e), e]));
121
+ const byKey = new Map(data.entries.map((e) => [manifestEntryKey(e), e]));
176
122
  if (superseding) for (const s of superseding) {
177
- const existing = byKey.get(entryKey(s));
178
- if (existing && existing.retiredAt === void 0) byKey.set(entryKey(s), {
123
+ const existing = byKey.get(manifestEntryKey(s));
124
+ if (existing && existing.retiredAt === void 0) byKey.set(manifestEntryKey(s), {
179
125
  ...existing,
180
126
  retiredAt: supersededAt
181
127
  });
182
128
  }
183
- for (const e of newEntries) byKey.set(entryKey(e), e);
129
+ for (const e of newEntries) byKey.set(manifestEntryKey(e), e);
184
130
  data.entries = Array.from(byKey.values());
185
131
  await save(data);
186
132
  }
187
133
  return {
188
134
  async listLive(filter) {
189
- return (await load()).entries.filter((e) => e.retiredAt === void 0 && matchesFilter(e, filter));
135
+ return (await load()).entries.filter((e) => e.retiredAt === void 0 && matchesManifestEntryFilter(e, filter));
190
136
  },
191
137
  async listAll(filter) {
192
- return (await load()).entries.filter((e) => matchesFilter(e, filter));
138
+ return (await load()).entries.filter((e) => matchesManifestEntryFilter(e, filter));
193
139
  },
194
140
  async registerVersion(entry, superseding) {
195
141
  return enqueue(() => registerVersionsImpl([entry], superseding));
@@ -203,8 +149,8 @@ function createFilesystemManifestStore(opts) {
203
149
  async delete(toDelete) {
204
150
  return enqueue(async () => {
205
151
  const data = await load();
206
- const toDeleteKeys = new Set(toDelete.map(entryKey));
207
- data.entries = data.entries.filter((e) => !toDeleteKeys.has(entryKey(e)));
152
+ const toDeleteKeys = new Set(toDelete.map(manifestEntryKey));
153
+ data.entries = data.entries.filter((e) => !toDeleteKeys.has(manifestEntryKey(e)));
208
154
  await save(data);
209
155
  });
210
156
  },
@@ -124,8 +124,11 @@ function naturalKeyFor(table, row) {
124
124
  }
125
125
  function encodeOrderedRows(rows, columns, rowGroupSize) {
126
126
  const schema = buildWriteSchema(columns);
127
- const isDate = columns.map((col) => col.type === "DATE");
128
- const types = columns.map((col) => basicTypeFor(col.type));
127
+ const codecs = columns.map((col) => ({
128
+ name: col.name,
129
+ isDate: col.type === "DATE",
130
+ type: basicTypeFor(col.type)
131
+ }));
129
132
  const columnSpecs = columns.map((col) => ({
130
133
  name: col.name,
131
134
  nullable: col.nullable,
@@ -134,10 +137,7 @@ function encodeOrderedRows(rows, columns, rowGroupSize) {
134
137
  function* coercedRows() {
135
138
  for (const r of rows) {
136
139
  const out = {};
137
- for (let c = 0; c < columns.length; c++) {
138
- const name = columns[c].name;
139
- out[name] = isDate[c] ? toEpochDays(r[name]) : coerceValue(r[name], types[c]);
140
- }
140
+ for (const codec of codecs) out[codec.name] = codec.isDate ? toEpochDays(r[codec.name]) : coerceValue(r[codec.name], codec.type);
141
141
  yield out;
142
142
  }
143
143
  }
@@ -1,5 +1,6 @@
1
- import { inferLegacyTier, inferSearchType } from "../_chunks/layout.mjs";
1
+ import { inferSearchType } from "../_chunks/layout.mjs";
2
2
  import { engineErrorToException, engineErrors } from "../errors.mjs";
3
+ import { matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter } from "../_chunks/manifest-store-utils.mjs";
3
4
  import { err, ok, unwrapResult } from "gscdump/result";
4
5
  const SHARD_RE = /^u_[^/]+\/manifest\/(?<siteId>[^/]+)\/(?<table>[^/]+)\/HEAD$/;
5
6
  const CAS_BACKOFF_BASE_MS = 5;
@@ -52,26 +53,6 @@ function shardScopesFromEntries(entries) {
52
53
  }
53
54
  return out;
54
55
  }
55
- function matchesEntryFilter(entry, filter) {
56
- if (filter.siteId !== void 0 && entry.siteId !== filter.siteId) return false;
57
- if (filter.table !== void 0 && entry.table !== filter.table) return false;
58
- if (filter.partitions && !filter.partitions.includes(entry.partition)) return false;
59
- if (filter.tier !== void 0 && inferLegacyTier(entry) !== filter.tier) return false;
60
- if (filter.searchType !== void 0 && inferSearchType(entry) !== filter.searchType) return false;
61
- return true;
62
- }
63
- function matchesWatermarkFilter(w, filter) {
64
- if (filter.siteId !== void 0 && w.siteId !== filter.siteId) return false;
65
- if (filter.table !== void 0 && w.table !== filter.table) return false;
66
- return true;
67
- }
68
- function matchesSyncStateFilter(s, filter) {
69
- if (filter.siteId !== void 0 && s.siteId !== filter.siteId) return false;
70
- if (filter.table !== void 0 && s.table !== filter.table) return false;
71
- if (filter.state !== void 0 && s.state !== filter.state) return false;
72
- if (filter.searchType !== void 0 && inferSearchType(s) !== filter.searchType) return false;
73
- return true;
74
- }
75
56
  function createR2ManifestStore(opts) {
76
57
  const { bucket, userId } = opts;
77
58
  const newSnapshotId = opts.newSnapshotId ?? defaultSnapshotId;
@@ -170,13 +151,17 @@ function createR2ManifestStore(opts) {
170
151
  }];
171
152
  return (await listShards()).filter((s) => (filter.siteId === void 0 || s.siteId === filter.siteId) && (filter.table === void 0 || s.table === filter.table));
172
153
  }
154
+ function assertScopedUser(got, op) {
155
+ if (got !== userId) throw new Error(`${op}: R2 manifest store is scoped to userId=${userId}, got ${got}`);
156
+ }
173
157
  async function readEntriesAcrossShards(filter, includeRetired) {
158
+ assertScopedUser(filter.userId, includeRetired ? "listAll" : "listLive");
174
159
  return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
175
160
  const { snapshot } = await readShard(siteId, table);
176
161
  const entries = [];
177
162
  for (const entry of snapshot.entries) {
178
163
  if (!includeRetired && entry.retiredAt !== void 0) continue;
179
- if (matchesEntryFilter(entry, filter)) entries.push(entry);
164
+ if (matchesManifestEntryFilter(entry, filter, { ignoreUserId: true })) entries.push(entry);
180
165
  }
181
166
  return entries;
182
167
  })).flat();
@@ -195,6 +180,7 @@ function createR2ManifestStore(opts) {
195
180
  const supersededAt = newEntries[0]?.createdAt ?? now();
196
181
  const byShard = /* @__PURE__ */ new Map();
197
182
  function bucket(entry, kind) {
183
+ assertScopedUser(entry.userId, "registerVersions");
198
184
  if (entry.siteId === void 0) throw new Error("R2 manifest store requires entries to carry siteId");
199
185
  const key = `${entry.siteId}\0${entry.table}`;
200
186
  let bag = byShard.get(key);
@@ -258,14 +244,16 @@ function createR2ManifestStore(opts) {
258
244
  });
259
245
  },
260
246
  async getWatermarks(filter) {
247
+ assertScopedUser(filter.userId, "getWatermarks");
261
248
  return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
262
249
  const { snapshot } = await readShard(siteId, table);
263
250
  const watermarks = [];
264
- for (const w of snapshot.watermarks) if (matchesWatermarkFilter(w, filter)) watermarks.push(w);
251
+ for (const w of snapshot.watermarks) if (matchesWatermarkFilter(w, filter, { ignoreUserId: true })) watermarks.push(w);
265
252
  return watermarks;
266
253
  })).flat();
267
254
  },
268
255
  async bumpWatermark(scope, date, at) {
256
+ assertScopedUser(scope.userId, "bumpWatermark");
269
257
  if (scope.siteId === void 0) throw new Error("R2 manifest store requires watermarks to carry siteId");
270
258
  const ts = at ?? now();
271
259
  await mutateShard(scope.siteId, scope.table, (snap) => {
@@ -294,14 +282,16 @@ function createR2ManifestStore(opts) {
294
282
  });
295
283
  },
296
284
  async getSyncStates(filter) {
285
+ assertScopedUser(filter.userId, "getSyncStates");
297
286
  return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
298
287
  const { snapshot } = await readShard(siteId, table);
299
288
  const states = [];
300
- for (const s of snapshot.syncStates) if (matchesSyncStateFilter(s, filter)) states.push(s);
289
+ for (const s of snapshot.syncStates) if (matchesSyncStateFilter(s, filter, { ignoreUserId: true })) states.push(s);
301
290
  return states;
302
291
  })).flat();
303
292
  },
304
293
  async setSyncState(scope, state, detail) {
294
+ assertScopedUser(scope.userId, "setSyncState");
305
295
  if (scope.siteId === void 0) throw new Error("R2 manifest store requires sync states to carry siteId");
306
296
  const at = detail?.at ?? now();
307
297
  const scopeSearchType = inferSearchType(scope);
@@ -377,14 +377,22 @@ declare const queryCanonicalDailyRollup: RollupDef;
377
377
  * Resumable, cross-invocation build of `query_canonical_daily` for a high-
378
378
  * cardinality site whose full windowed build exceeds one job reservation (300s).
379
379
  *
380
- * Each call builds the day-capped windows from `windowOffset` until `deadlineMs`,
381
- * writes that batch's rows to a STAGING partial parquet (windows are disjoint by
382
- * date, so partials never overlap), and returns `{ done:false, nextWindowOffset }`
383
- * for the caller to re-enqueue. When the last window is built it MERGES every
384
- * staging partial into the canonical `query_canonical_daily` parquet + envelope
385
- * (the read path is unchanged — still one rollup file) and deletes the staging,
380
+ * Each call builds from `(windowOffset, pageOffset)` until `deadlineMs`, writes
381
+ * that batch's rows to a PART parquet, and returns `{ done:false, nextWindowOffset,
382
+ * nextPageOffset }` for the caller to re-enqueue. When the last window is fully
383
+ * paged it publishes a multi-file envelope listing every part (parts are disjoint
384
+ * by `(query_canonical, date)`, so the read path just unions them — no merge),
386
385
  * returning `{ done:true }`. `builtAt` MUST be stable across the continuation chain
387
- * (it versions both the staging keys and the final rollup key).
386
+ * (it versions both the part keys and the final rollup key).
387
+ *
388
+ * INTRA-WINDOW resumability: the deadline is checked between OUTPUT PAGES, not just
389
+ * between windows. A single high-cardinality window's paged aggregation can exceed
390
+ * one 300s reservation on its own; checking only between windows let that window
391
+ * run unbounded and stale-reservation-loop forever (huuto.net/comparaja.pt never
392
+ * completed window 0). `pageOffset` lets a continuation resume the SAME window at
393
+ * the next page, so no single invocation runs past the deadline by more than one
394
+ * page. Parts are keyed by `(windowOffset, pageOffset)` so a mid-window pause and
395
+ * its continuation write disjoint, non-colliding files.
388
396
  */
389
397
  declare function rebuildCanonicalDailyResumable(opts: {
390
398
  engine: RollupEngine;
@@ -392,11 +400,14 @@ declare function rebuildCanonicalDailyResumable(opts: {
392
400
  dataSource: DataSource;
393
401
  searchType?: SearchType;
394
402
  builtAt: number;
395
- windowOffset: number;
403
+ windowOffset: number; /** Resume the `windowOffset` window at this output-page offset (0 = window start). */
404
+ pageOffset?: number; /** Output rows per page (default `ROLLUP_PAGE_ROWS_DAILY`). Injectable for tests. */
405
+ pageRows?: number;
396
406
  deadlineMs: number;
397
407
  }): Promise<{
398
408
  done: boolean;
399
409
  nextWindowOffset: number;
410
+ nextPageOffset: number;
400
411
  windowsTotal: number;
401
412
  windowsBuilt: number;
402
413
  rowsWritten: number;
package/dist/rollups.mjs CHANGED
@@ -764,6 +764,8 @@ function mapDailyRow(r) {
764
764
  async function rebuildCanonicalDailyResumable(opts) {
765
765
  const { engine, ctx, dataSource, searchType, builtAt, windowOffset, deadlineMs } = opts;
766
766
  const sType = searchType !== void 0 ? { searchType } : {};
767
+ const startPageOffset = opts.pageOffset ?? 0;
768
+ const pageRows = opts.pageRows ?? 7e4;
767
769
  const windows = planRollupWindows((await engine.listPartitions({
768
770
  ctx,
769
771
  table: "queries",
@@ -785,39 +787,54 @@ async function rebuildCanonicalDailyResumable(opts) {
785
787
  const sortKey = queryCanonicalDailyRollup.parquetSortKey;
786
788
  const batchRows = [];
787
789
  let i = windowOffset;
788
- for (; i < windowsTotal; i++) {
790
+ let page = startPageOffset;
791
+ let nextWindowOffset = windowsTotal;
792
+ let nextPageOffset = 0;
793
+ windowLoop: for (; i < windowsTotal; i++) {
789
794
  const w = windows[i];
790
- const winRows = await runPagedQuery({
791
- engine,
792
- ctx,
793
- table: "queries",
794
- ...sType,
795
- fileSets: {
796
- FILES: {
797
- table: "queries",
798
- partitions: w.partitions
795
+ const coreSql = sqlFor(w);
796
+ for (;;) {
797
+ const result = await engine.runSQL({
798
+ ctx,
799
+ table: "queries",
800
+ ...sType,
801
+ fileSets: {
802
+ FILES: {
803
+ table: "queries",
804
+ partitions: w.partitions
805
+ },
806
+ ...extraFileSets
799
807
  },
800
- ...extraFileSets
801
- },
802
- coreSql: sqlFor(w),
803
- orderBy: "date, query_canonical",
804
- pageRows: ROLLUP_PAGE_ROWS_DAILY
805
- });
806
- for (const r of winRows) batchRows.push(mapDailyRow(r));
808
+ sql: `${coreSql}\nORDER BY date, query_canonical\nLIMIT ${pageRows} OFFSET ${page}`
809
+ });
810
+ for (const r of result.rows) batchRows.push(mapDailyRow(r));
811
+ const windowDone = result.rows.length < pageRows;
812
+ page += pageRows;
813
+ if (windowDone) {
814
+ page = 0;
815
+ break;
816
+ }
817
+ if (Date.now() > deadlineMs) {
818
+ nextWindowOffset = i;
819
+ nextPageOffset = page;
820
+ break windowLoop;
821
+ }
822
+ }
807
823
  if (Date.now() > deadlineMs) {
808
- i++;
824
+ nextWindowOffset = i + 1;
825
+ nextPageOffset = 0;
809
826
  break;
810
827
  }
811
828
  }
812
- const nextWindowOffset = i;
813
- const partKey = rollupParquetKey(ctx, `${CANONICAL_DAILY_PART_STEM}__w${windowOffset}`, builtAt, searchType);
829
+ const partKey = rollupParquetKey(ctx, `${CANONICAL_DAILY_PART_STEM}__w${windowOffset}_p${startPageOffset}`, builtAt, searchType);
814
830
  await dataSource.write(partKey, encodeRowsToParquetFlex(batchRows, {
815
831
  columns: cols,
816
832
  sortKey
817
833
  }));
818
- if (nextWindowOffset < windowsTotal) return {
834
+ if (nextWindowOffset < windowsTotal || nextPageOffset > 0) return {
819
835
  done: false,
820
836
  nextWindowOffset,
837
+ nextPageOffset,
821
838
  windowsTotal,
822
839
  windowsBuilt: nextWindowOffset - windowOffset,
823
840
  rowsWritten: batchRows.length
@@ -839,6 +856,7 @@ async function rebuildCanonicalDailyResumable(opts) {
839
856
  return {
840
857
  done: true,
841
858
  nextWindowOffset,
859
+ nextPageOffset,
842
860
  windowsTotal,
843
861
  windowsBuilt: nextWindowOffset - windowOffset,
844
862
  rowsWritten: batchRows.length
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "0.33.0",
4
+ "version": "0.33.4",
5
5
  "description": "Append-only Parquet/DuckDB storage engine + planner + adapters for the gscdump pipeline. Node + edge runtimes; opt-in heavy peers.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -181,8 +181,8 @@
181
181
  "dependencies": {
182
182
  "drizzle-orm": "1.0.0-rc.3",
183
183
  "proper-lockfile": "^4.1.2",
184
- "@gscdump/contracts": "0.33.0",
185
- "gscdump": "0.33.0"
184
+ "@gscdump/contracts": "0.33.4",
185
+ "gscdump": "0.33.4"
186
186
  },
187
187
  "devDependencies": {
188
188
  "@duckdb/duckdb-wasm": "^1.32.0",