@gscdump/engine 0.33.3 → 0.33.5

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.
@@ -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",
@@ -783,54 +785,81 @@ async function rebuildCanonicalDailyResumable(opts) {
783
785
  const sqlFor = dailyWindowSqlFor(useDim, canonExpr);
784
786
  const cols = queryCanonicalDailyRollup.parquetColumns;
785
787
  const sortKey = queryCanonicalDailyRollup.parquetSortKey;
786
- const batchRows = [];
787
788
  let i = windowOffset;
788
- for (; i < windowsTotal; i++) {
789
+ let page = startPageOffset;
790
+ let nextWindowOffset = windowsTotal;
791
+ let nextPageOffset = 0;
792
+ let rowsWritten = 0;
793
+ const flushPage = async (windowIdx, pageOffset, rows) => {
794
+ if (rows.length === 0) return;
795
+ const key = rollupParquetKey(ctx, `${CANONICAL_DAILY_PART_STEM}__w${windowIdx}_p${pageOffset}`, builtAt, searchType);
796
+ await dataSource.write(key, encodeRowsToParquetFlex(rows, {
797
+ columns: cols,
798
+ sortKey
799
+ }));
800
+ rowsWritten += rows.length;
801
+ };
802
+ windowLoop: for (; i < windowsTotal; i++) {
789
803
  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
804
+ const coreSql = sqlFor(w);
805
+ for (;;) {
806
+ const result = await engine.runSQL({
807
+ ctx,
808
+ table: "queries",
809
+ ...sType,
810
+ fileSets: {
811
+ FILES: {
812
+ table: "queries",
813
+ partitions: w.partitions
814
+ },
815
+ ...extraFileSets
799
816
  },
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));
817
+ sql: `${coreSql}\nORDER BY date, query_canonical\nLIMIT ${pageRows} OFFSET ${page}`
818
+ });
819
+ await flushPage(i, page, result.rows.map(mapDailyRow));
820
+ const windowDone = result.rows.length < pageRows;
821
+ page += pageRows;
822
+ if (windowDone) {
823
+ page = 0;
824
+ break;
825
+ }
826
+ if (Date.now() > deadlineMs) {
827
+ nextWindowOffset = i;
828
+ nextPageOffset = page;
829
+ break windowLoop;
830
+ }
831
+ }
807
832
  if (Date.now() > deadlineMs) {
808
- i++;
833
+ nextWindowOffset = i + 1;
834
+ nextPageOffset = 0;
809
835
  break;
810
836
  }
811
837
  }
812
- const nextWindowOffset = i;
813
- const partKey = rollupParquetKey(ctx, `${CANONICAL_DAILY_PART_STEM}__w${windowOffset}`, builtAt, searchType);
814
- await dataSource.write(partKey, encodeRowsToParquetFlex(batchRows, {
815
- columns: cols,
816
- sortKey
817
- }));
818
- if (nextWindowOffset < windowsTotal) return {
838
+ if (nextWindowOffset < windowsTotal || nextPageOffset > 0) return {
819
839
  done: false,
820
840
  nextWindowOffset,
841
+ nextPageOffset,
821
842
  windowsTotal,
822
843
  windowsBuilt: nextWindowOffset - windowOffset,
823
- rowsWritten: batchRows.length
844
+ rowsWritten
824
845
  };
825
846
  const partPrefixDir = rollupParquetKey(ctx, CANONICAL_DAILY_PART_STEM, builtAt, searchType).replace(/[^/]*$/, "");
826
847
  const partKeys = (await dataSource.list(partPrefixDir)).filter((k) => k.includes(`${CANONICAL_DAILY_PART_STEM}__w`) && k.endsWith(`__v${builtAt}.parquet`)).sort();
848
+ if (partKeys.length === 0) {
849
+ const emptyKey = rollupParquetKey(ctx, `${CANONICAL_DAILY_PART_STEM}__w0_p0`, builtAt, searchType);
850
+ await dataSource.write(emptyKey, encodeRowsToParquetFlex([], {
851
+ columns: cols,
852
+ sortKey
853
+ }));
854
+ partKeys.push(emptyKey);
855
+ }
827
856
  const envelope = {
828
857
  version: 1,
829
858
  id: CANONICAL_DAILY_ROLLUP_FINAL_ID,
830
859
  builtAt,
831
860
  windowDays: queryCanonicalDailyRollup.windowDays,
832
861
  payload: {
833
- parquetKey: partKeys[0] ?? partKey,
862
+ parquetKey: partKeys[0],
834
863
  parquetKeys: partKeys,
835
864
  rowCount: 0
836
865
  }
@@ -839,9 +868,10 @@ async function rebuildCanonicalDailyResumable(opts) {
839
868
  return {
840
869
  done: true,
841
870
  nextWindowOffset,
871
+ nextPageOffset,
842
872
  windowsTotal,
843
873
  windowsBuilt: nextWindowOffset - windowOffset,
844
- rowsWritten: batchRows.length
874
+ rowsWritten
845
875
  };
846
876
  }
847
877
  const indexingMetadataRollup = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "0.33.3",
4
+ "version": "0.33.5",
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.3",
185
- "gscdump": "0.33.3"
184
+ "gscdump": "0.33.5",
185
+ "@gscdump/contracts": "0.33.5"
186
186
  },
187
187
  "devDependencies": {
188
188
  "@duckdb/duckdb-wasm": "^1.32.0",