@gscdump/engine 0.31.8 → 0.31.10

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.
@@ -123,6 +123,15 @@ interface RollupEnvelope<T = unknown> {
123
123
  interface ParquetRollupPointer {
124
124
  parquetKey: string;
125
125
  rowCount: number;
126
+ /**
127
+ * MULTI-FILE rollup: when set, the rollup is the UNION of these parquet keys
128
+ * (disjoint by the grain's partition column, e.g. `date` for the resumable
129
+ * `query_canonical_daily` build). Readers MUST union all keys; `parquetKey`
130
+ * stays populated (the first part) for single-file readers. Avoids a JS
131
+ * merge/re-encode of the whole rollup — the scaling bottleneck for a
132
+ * cross-invocation resumable build.
133
+ */
134
+ parquetKeys?: string[];
126
135
  }
127
136
  declare function rollupKey(ctx: TenantCtx, id: string, builtAt: number, searchType?: SearchType): string;
128
137
  declare function rollupParquetKey(ctx: TenantCtx, id: string, builtAt: number, searchType?: SearchType): string;
@@ -364,6 +373,34 @@ declare const queryCanonicalVariantsRollup: RollupDef;
364
373
  * `RunOptimizedQueryOptions.canonicalSource`).
365
374
  */
366
375
  declare const queryCanonicalDailyRollup: RollupDef;
376
+ /**
377
+ * Resumable, cross-invocation build of `query_canonical_daily` for a high-
378
+ * cardinality site whose full windowed build exceeds one job reservation (300s).
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,
386
+ * returning `{ done:true }`. `builtAt` MUST be stable across the continuation chain
387
+ * (it versions both the staging keys and the final rollup key).
388
+ */
389
+ declare function rebuildCanonicalDailyResumable(opts: {
390
+ engine: RollupEngine;
391
+ ctx: TenantCtx;
392
+ dataSource: DataSource;
393
+ searchType?: SearchType;
394
+ builtAt: number;
395
+ windowOffset: number;
396
+ deadlineMs: number;
397
+ }): Promise<{
398
+ done: boolean;
399
+ nextWindowOffset: number;
400
+ windowsTotal: number;
401
+ windowsBuilt: number;
402
+ rowsWritten: number;
403
+ }>;
367
404
  /**
368
405
  * Aggregates the per-URL Indexing API metadata entity store (populated by
369
406
  * `gscdump entities indexing snapshot`) into daily counts of `URL_UPDATED`
@@ -449,4 +486,4 @@ declare const DEFAULT_ROLLUPS: readonly RollupDef[];
449
486
  * (CLI: `gscdump rollups --with-canonical`).
450
487
  */
451
488
  declare const CANONICAL_ROLLUPS: readonly RollupDef[];
452
- export { CANONICAL_ROLLUPS, DAILY_MAX_WINDOW_DAYS, DEFAULT_ROLLUPS, ParquetRollupPointer, ROLLUP_PAGE_ROWS, ROLLUP_PAGE_ROWS_DAILY, ROLLUP_PAGE_ROWS_WIDE, RebuildDailyFromHourlyOptions, RebuildRollupResult, RebuildRollupsOptions, RollupBucket, RollupCtx, RollupDef, RollupEngine, RollupEnvelope, WINDOW_BYTE_BUDGET, dailyTotalsRollup, indexPercentRollup, indexingHealthRollup, indexingMetadataRollup, partitionDaySpan, partitionsInRange, planRollupWindows, queryCanonicalDailyRollup, queryCanonicalVariantsRollup, readLatestRollup, rebuildDailyFromHourly, rebuildRollups, rollupKey, rollupParquetKey, runWindowed, sitemapChanges28dRollup, sitemapHealthRollup, topCountries28dRollup, topKeywords28dParquetRollup, topKeywords28dRollup, topPages28dRollup, weeklyTotalsRollup };
489
+ export { CANONICAL_ROLLUPS, DAILY_MAX_WINDOW_DAYS, DEFAULT_ROLLUPS, ParquetRollupPointer, ROLLUP_PAGE_ROWS, ROLLUP_PAGE_ROWS_DAILY, ROLLUP_PAGE_ROWS_WIDE, RebuildDailyFromHourlyOptions, RebuildRollupResult, RebuildRollupsOptions, RollupBucket, RollupCtx, RollupDef, RollupEngine, RollupEnvelope, WINDOW_BYTE_BUDGET, dailyTotalsRollup, indexPercentRollup, indexingHealthRollup, indexingMetadataRollup, partitionDaySpan, partitionsInRange, planRollupWindows, queryCanonicalDailyRollup, queryCanonicalVariantsRollup, readLatestRollup, rebuildCanonicalDailyResumable, rebuildDailyFromHourly, rebuildRollups, rollupKey, rollupParquetKey, runWindowed, sitemapChanges28dRollup, sitemapHealthRollup, topCountries28dRollup, topKeywords28dParquetRollup, topKeywords28dRollup, topPages28dRollup, weeklyTotalsRollup };
package/dist/rollups.mjs CHANGED
@@ -713,37 +713,128 @@ const queryCanonicalDailyRollup = {
713
713
  pageRows: ROLLUP_PAGE_ROWS_DAILY
714
714
  },
715
715
  maxWindowDays: 7,
716
- sqlFor: useDim ? (w) => `
717
- SELECT
718
- ${canonExpr} AS query_canonical,
719
- CAST(q.date AS VARCHAR) AS date,
720
- SUM(q.clicks)::BIGINT AS clicks,
721
- SUM(q.impressions)::BIGINT AS impressions,
722
- SUM(q.sum_position)::DOUBLE AS sum_position
723
- FROM read_parquet({{FILES}}, union_by_name = true) q
724
- LEFT JOIN read_parquet({{QUERY_DIM}}, union_by_name = true) qd ON q.query = qd.query
725
- WHERE q.date >= '${w.start}' AND q.date <= '${w.end}'
726
- GROUP BY ${canonExpr}, q.date
727
- ` : (w) => `
728
- SELECT
729
- ${canonExpr} AS query_canonical,
730
- CAST(date AS VARCHAR) AS date,
731
- SUM(clicks)::BIGINT AS clicks,
732
- SUM(impressions)::BIGINT AS impressions,
733
- SUM(sum_position)::DOUBLE AS sum_position
734
- FROM read_parquet({{FILES}}, union_by_name = true)
735
- WHERE date >= '${w.start}' AND date <= '${w.end}'
736
- GROUP BY ${canonExpr}, date
737
- `
738
- })).map((r) => ({
739
- query_canonical: String(r.query_canonical),
740
- date: String(r.date),
741
- clicks: BigInt(r.clicks),
742
- impressions: BigInt(r.impressions),
743
- sum_position: Number(r.sum_position)
744
- }));
716
+ sqlFor: dailyWindowSqlFor(useDim, canonExpr)
717
+ })).map(mapDailyRow);
745
718
  }
746
719
  };
720
+ function dailyWindowSqlFor(useDim, canonExpr) {
721
+ return useDim ? (w) => `
722
+ SELECT
723
+ ${canonExpr} AS query_canonical,
724
+ CAST(q.date AS VARCHAR) AS date,
725
+ SUM(q.clicks)::BIGINT AS clicks,
726
+ SUM(q.impressions)::BIGINT AS impressions,
727
+ SUM(q.sum_position)::DOUBLE AS sum_position
728
+ FROM read_parquet({{FILES}}, union_by_name = true) q
729
+ LEFT JOIN read_parquet({{QUERY_DIM}}, union_by_name = true) qd ON q.query = qd.query
730
+ WHERE q.date >= '${w.start}' AND q.date <= '${w.end}'
731
+ GROUP BY ${canonExpr}, q.date
732
+ ` : (w) => `
733
+ SELECT
734
+ ${canonExpr} AS query_canonical,
735
+ CAST(date AS VARCHAR) AS date,
736
+ SUM(clicks)::BIGINT AS clicks,
737
+ SUM(impressions)::BIGINT AS impressions,
738
+ SUM(sum_position)::DOUBLE AS sum_position
739
+ FROM read_parquet({{FILES}}, union_by_name = true)
740
+ WHERE date >= '${w.start}' AND date <= '${w.end}'
741
+ GROUP BY ${canonExpr}, date
742
+ `;
743
+ }
744
+ function mapDailyRow(r) {
745
+ return {
746
+ query_canonical: String(r.query_canonical),
747
+ date: String(r.date),
748
+ clicks: BigInt(r.clicks),
749
+ impressions: BigInt(r.impressions),
750
+ sum_position: Number(r.sum_position)
751
+ };
752
+ }
753
+ async function rebuildCanonicalDailyResumable(opts) {
754
+ const { engine, ctx, dataSource, searchType, builtAt, windowOffset, deadlineMs } = opts;
755
+ const sType = searchType !== void 0 ? { searchType } : {};
756
+ const windows = planRollupWindows((await engine.listPartitions({
757
+ ctx,
758
+ table: "queries",
759
+ ...sType
760
+ })).map((p) => ({
761
+ partition: p.partition,
762
+ bytes: p.bytes
763
+ })), void 0, 7);
764
+ const windowsTotal = windows.length;
765
+ const dimStore = createQueryDimStore({ dataSource });
766
+ const useDim = await dimStore.loadMeta(ctx) !== null;
767
+ const canonExpr = useDim ? `COALESCE(qd.query_canonical, NULLIF(q.query_canonical, ''), q.query)` : `COALESCE(NULLIF(query_canonical, ''), query)`;
768
+ const extraFileSets = useDim ? { QUERY_DIM: {
769
+ table: "queries",
770
+ keys: [dimStore.parquetKey(ctx)]
771
+ } } : void 0;
772
+ const sqlFor = dailyWindowSqlFor(useDim, canonExpr);
773
+ const cols = queryCanonicalDailyRollup.parquetColumns;
774
+ const sortKey = queryCanonicalDailyRollup.parquetSortKey;
775
+ const batchRows = [];
776
+ let i = windowOffset;
777
+ for (; i < windowsTotal; i++) {
778
+ const w = windows[i];
779
+ const winRows = await runPagedQuery({
780
+ engine,
781
+ ctx,
782
+ table: "queries",
783
+ ...sType,
784
+ fileSets: {
785
+ FILES: {
786
+ table: "queries",
787
+ partitions: w.partitions
788
+ },
789
+ ...extraFileSets
790
+ },
791
+ coreSql: sqlFor(w),
792
+ orderBy: "date, query_canonical",
793
+ pageRows: ROLLUP_PAGE_ROWS_DAILY
794
+ });
795
+ for (const r of winRows) batchRows.push(mapDailyRow(r));
796
+ if (Date.now() > deadlineMs) {
797
+ i++;
798
+ break;
799
+ }
800
+ }
801
+ const nextWindowOffset = i;
802
+ const partKey = rollupParquetKey(ctx, `${CANONICAL_DAILY_PART_STEM}__w${windowOffset}`, builtAt, searchType);
803
+ await dataSource.write(partKey, encodeRowsToParquetFlex(batchRows, {
804
+ columns: cols,
805
+ sortKey
806
+ }));
807
+ if (nextWindowOffset < windowsTotal) return {
808
+ done: false,
809
+ nextWindowOffset,
810
+ windowsTotal,
811
+ windowsBuilt: nextWindowOffset - windowOffset,
812
+ rowsWritten: batchRows.length
813
+ };
814
+ const partPrefixDir = rollupParquetKey(ctx, CANONICAL_DAILY_PART_STEM, builtAt, searchType).replace(/[^/]*$/, "");
815
+ const partKeys = (await dataSource.list(partPrefixDir)).filter((k) => k.includes(`${CANONICAL_DAILY_PART_STEM}__w`) && k.endsWith(`__v${builtAt}.parquet`)).sort();
816
+ const envelope = {
817
+ version: 1,
818
+ id: CANONICAL_DAILY_ROLLUP_FINAL_ID,
819
+ builtAt,
820
+ windowDays: queryCanonicalDailyRollup.windowDays,
821
+ payload: {
822
+ parquetKey: partKeys[0] ?? partKey,
823
+ parquetKeys: partKeys,
824
+ rowCount: 0
825
+ }
826
+ };
827
+ await dataSource.write(rollupKey(ctx, CANONICAL_DAILY_ROLLUP_FINAL_ID, builtAt, searchType), new TextEncoder().encode(JSON.stringify(envelope)));
828
+ return {
829
+ done: true,
830
+ nextWindowOffset,
831
+ windowsTotal,
832
+ windowsBuilt: nextWindowOffset - windowOffset,
833
+ rowsWritten: batchRows.length
834
+ };
835
+ }
836
+ const CANONICAL_DAILY_ROLLUP_FINAL_ID = "query_canonical_daily";
837
+ const CANONICAL_DAILY_PART_STEM = "query_canonical_daily__part";
747
838
  const indexingMetadataRollup = {
748
839
  id: "indexing_metadata",
749
840
  windowDays: null,
@@ -1056,4 +1147,4 @@ const DEFAULT_ROLLUPS = [
1056
1147
  sitemapChanges28dRollup
1057
1148
  ];
1058
1149
  const CANONICAL_ROLLUPS = [queryCanonicalVariantsRollup, queryCanonicalDailyRollup];
1059
- export { CANONICAL_ROLLUPS, DAILY_MAX_WINDOW_DAYS, DEFAULT_ROLLUPS, ROLLUP_PAGE_ROWS, ROLLUP_PAGE_ROWS_DAILY, ROLLUP_PAGE_ROWS_WIDE, WINDOW_BYTE_BUDGET, dailyTotalsRollup, indexPercentRollup, indexingHealthRollup, indexingMetadataRollup, partitionDaySpan, partitionsInRange, planRollupWindows, queryCanonicalDailyRollup, queryCanonicalVariantsRollup, readLatestRollup, rebuildDailyFromHourly, rebuildRollups, rollupKey, rollupParquetKey, runWindowed, sitemapChanges28dRollup, sitemapHealthRollup, topCountries28dRollup, topKeywords28dParquetRollup, topKeywords28dRollup, topPages28dRollup, weeklyTotalsRollup };
1150
+ export { CANONICAL_ROLLUPS, DAILY_MAX_WINDOW_DAYS, DEFAULT_ROLLUPS, ROLLUP_PAGE_ROWS, ROLLUP_PAGE_ROWS_DAILY, ROLLUP_PAGE_ROWS_WIDE, WINDOW_BYTE_BUDGET, dailyTotalsRollup, indexPercentRollup, indexingHealthRollup, indexingMetadataRollup, partitionDaySpan, partitionsInRange, planRollupWindows, queryCanonicalDailyRollup, queryCanonicalVariantsRollup, readLatestRollup, rebuildCanonicalDailyResumable, rebuildDailyFromHourly, rebuildRollups, rollupKey, rollupParquetKey, runWindowed, sitemapChanges28dRollup, sitemapHealthRollup, topCountries28dRollup, topKeywords28dParquetRollup, topKeywords28dRollup, topPages28dRollup, weeklyTotalsRollup };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "0.31.8",
4
+ "version": "0.31.10",
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.31.8",
185
- "gscdump": "0.31.8"
184
+ "gscdump": "0.31.10",
185
+ "@gscdump/contracts": "0.31.10"
186
186
  },
187
187
  "devDependencies": {
188
188
  "@duckdb/duckdb-wasm": "^1.32.0",