@gscdump/engine 0.29.0 → 0.30.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.
@@ -3319,24 +3319,38 @@ async function icebergManifests({ metadata, resolver, snapshotId, partitionFilte
3319
3319
  });
3320
3320
  return await fetchManifests(manifests, resolver);
3321
3321
  }
3322
+ const MANIFEST_FETCH_CONCURRENCY = 8;
3323
+ async function fetchOneManifest(manifest, resolver) {
3324
+ const url = manifest.manifest_path;
3325
+ const entries = await fetchAvroRecords(url, resolver, Number(manifest.manifest_length));
3326
+ for (const entry of entries) {
3327
+ entry.partition_spec_id = manifest.partition_spec_id ?? 0;
3328
+ if (entry.sequence_number === void 0) entry.sequence_number = manifest.sequence_number ?? 0n;
3329
+ if (entry.status === 1) {
3330
+ if (entry.sequence_number === void 0) entry.sequence_number = manifest.sequence_number;
3331
+ if (entry.file_sequence_number === void 0) entry.file_sequence_number = manifest.sequence_number;
3332
+ } else if (entry.sequence_number === void 0 || entry.file_sequence_number === void 0) throw new Error("iceberg manifest entry missing sequence number");
3333
+ }
3334
+ assignFirstRowIds(manifest, entries);
3335
+ return {
3336
+ url,
3337
+ entries
3338
+ };
3339
+ }
3322
3340
  async function fetchManifests(manifests, resolver) {
3323
- return await Promise.all(manifests.map(async (manifest) => {
3324
- const url = manifest.manifest_path;
3325
- const entries = await fetchAvroRecords(url, resolver, Number(manifest.manifest_length));
3326
- for (const entry of entries) {
3327
- entry.partition_spec_id = manifest.partition_spec_id ?? 0;
3328
- if (entry.sequence_number === void 0) entry.sequence_number = manifest.sequence_number ?? 0n;
3329
- if (entry.status === 1) {
3330
- if (entry.sequence_number === void 0) entry.sequence_number = manifest.sequence_number;
3331
- if (entry.file_sequence_number === void 0) entry.file_sequence_number = manifest.sequence_number;
3332
- } else if (entry.sequence_number === void 0 || entry.file_sequence_number === void 0) throw new Error("iceberg manifest entry missing sequence number");
3333
- }
3334
- assignFirstRowIds(manifest, entries);
3335
- return {
3336
- url,
3337
- entries
3338
- };
3339
- }));
3341
+ const results = new Array(manifests.length);
3342
+ let next = 0;
3343
+ async function worker() {
3344
+ while (next < manifests.length) {
3345
+ const i = next++;
3346
+ results[i] = await fetchOneManifest(manifests[i], resolver);
3347
+ }
3348
+ }
3349
+ const poolSize = Math.min(MANIFEST_FETCH_CONCURRENCY, manifests.length);
3350
+ const workers = [];
3351
+ for (let w = 0; w < poolSize; w++) workers.push(worker());
3352
+ await Promise.all(workers);
3353
+ return results;
3340
3354
  }
3341
3355
  function assignFirstRowIds(manifest, entries) {
3342
3356
  if (manifest.content !== 0 || manifest.first_row_id == null) return;
@@ -13,7 +13,15 @@ declare const pgResolverAdapter: ResolverAdapter<PgTableKey>;
13
13
  * Single-use: build a fresh adapter per query. Cheap (no I/O) and avoids
14
14
  * accidental adapter caching that would lock in a stale `{{FILES}}` set.
15
15
  */
16
- declare function createParquetResolverAdapter(): ResolverAdapter<PgTableKey>;
16
+ interface ResolverAdapterOptions {
17
+ /**
18
+ * Opt-in canonical-primary correctness: fold NULL/'' `query_canonical` back
19
+ * to the raw `query` so canonical is a total GROUP BY / join key. Default
20
+ * false preserves the legacy raw-column behaviour. See ADR-0018.
21
+ */
22
+ canonicalFallback?: boolean;
23
+ }
24
+ declare function createParquetResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
17
25
  /**
18
26
  * Multi-tenant pg-flavored adapter for the Iceberg / R2 SQL read path.
19
27
  * Identical SQL output to `pgResolverAdapter` except WHERE clauses inject
@@ -24,5 +32,5 @@ declare function createParquetResolverAdapter(): ResolverAdapter<PgTableKey>;
24
32
  * so callers must rewrite bare table names to their qualified form (e.g.
25
33
  * `${namespace}.pages`) before sending to R2 SQL.
26
34
  */
27
- declare function createIcebergResolverAdapter(): ResolverAdapter<PgTableKey>;
28
- export { PgTableKey, createIcebergResolverAdapter, createParquetResolverAdapter, pgResolverAdapter };
35
+ declare function createIcebergResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
36
+ export { PgTableKey, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, pgResolverAdapter };
@@ -180,7 +180,7 @@ function buildDimensionColumnMap(datasetToTableKey) {
180
180
  return Object.fromEntries(entries);
181
181
  }
182
182
  function createSqlFragments(config) {
183
- const { schema, datasetToTableKey, metricCast, regexPredicate, tableLabel, includeSiteId, includeSearchType, urlToPathExpr: urlToPathExprOverride, tableRef: tableRefOverride } = config;
183
+ const { schema, datasetToTableKey, metricCast, regexPredicate, tableLabel, includeSiteId, includeSearchType, urlToPathExpr: urlToPathExprOverride, tableRef: tableRefOverride, canonicalFallback = false } = config;
184
184
  const DIM_COLUMN_MAP = buildDimensionColumnMap(datasetToTableKey);
185
185
  function isMetricDimension(dim) {
186
186
  return METRIC_NAMES.includes(dim);
@@ -217,6 +217,7 @@ function createSqlFragments(config) {
217
217
  function dimExprSql(dim, tableKey) {
218
218
  const colName = dimColumn(dim, tableKey);
219
219
  if (dim === "page") return sql.raw(urlToPathExpr(colName));
220
+ if (canonicalFallback && dim === "queryCanonical") return sql`COALESCE(NULLIF(${colRef(tableKey, colName)}, ''), ${colRef(tableKey, "query")})`;
220
221
  return colRef(tableKey, colName);
221
222
  }
222
223
  function metricSql(metric, tableKey) {
@@ -431,23 +432,37 @@ const pgResolverAdapter = createResolverAdapter({
431
432
  ...PG_BASE_CONFIG,
432
433
  tableLabel: "pg-resolver-adapter"
433
434
  });
434
- function createParquetResolverAdapter() {
435
+ function createParquetResolverAdapter(options = {}) {
435
436
  return createResolverAdapter({
436
437
  ...PG_BASE_CONFIG,
437
438
  tableLabel: "parquet-resolver-adapter",
439
+ canonicalFallback: options.canonicalFallback ?? false,
438
440
  tableRef: (tk) => sql.raw(`read_parquet({{FILES}}, union_by_name = true) AS "${tk}"`)
439
441
  });
440
442
  }
441
- function createIcebergResolverAdapter() {
443
+ function createIcebergResolverAdapter(options = {}) {
442
444
  return createResolverAdapter({
443
445
  ...PG_BASE_CONFIG,
444
446
  schema: icebergSchema,
445
447
  includeSiteId: true,
446
448
  includeSearchType: true,
447
449
  tableLabel: "iceberg-resolver-adapter",
450
+ canonicalFallback: options.canonicalFallback ?? false,
448
451
  tableRef: (tk) => sql.raw(`"${tk}"`)
449
452
  });
450
453
  }
454
+ const ALLOWED_FILTER_DIMS = /* @__PURE__ */ new Set(["date", "queryCanonical"]);
455
+ function planCoveredByCanonicalRollup(plan) {
456
+ if (plan.dataset !== "queries") return false;
457
+ if (plan.groupByDimensions.length !== 1 || plan.groupByDimensions[0] !== "queryCanonical") return false;
458
+ if (!plan.dimensionFilters.every((f) => ALLOWED_FILTER_DIMS.has(f.dimension))) return false;
459
+ if (plan.prefilters.length > 0) return false;
460
+ if (plan.specialFilters.topLevel) return false;
461
+ return true;
462
+ }
463
+ function canonicalRollupCovers(state, capabilities) {
464
+ return planCoveredByCanonicalRollup(buildLogicalPlan(state, capabilities));
465
+ }
451
466
  const COMPARISON_FILTER_SQL = {
452
467
  new: sql`AND COALESCE(p.impressions, 0) = 0 AND COALESCE(c.impressions, 0) > 0`,
453
468
  lost: sql`AND COALESCE(p.impressions, 0) > 0 AND COALESCE(c.impressions, 0) = 0`,
@@ -726,7 +741,8 @@ function buildExtrasQueries(state, options) {
726
741
  whereParts.push(sql`${adapter.dateColRef(queriesKey)} <= ${plan.dateRange.endDate}`);
727
742
  const whereExpr = whereParts.length > 0 ? sql`WHERE ${joinAnd(whereParts)}` : sql``;
728
743
  const outerQueryCol = sql.raw("query");
729
- const compiled = compileCollapsed(adapter, sql`WITH per_variant AS (SELECT ${t.query_canonical} as joinKey, ${t.query} as query, SUM(${t.clicks}) as clicks, SUM(${t.impressions}) as impressions, SUM(${t.sum_position}) as sum_pos, ROW_NUMBER() OVER (PARTITION BY ${t.query_canonical} ORDER BY SUM(${t.clicks}) DESC) as rn, COUNT(*) OVER (PARTITION BY ${t.query_canonical}) as variantCount FROM ${table} ${whereExpr} GROUP BY ${t.query_canonical}, ${t.query}) SELECT joinKey, MAX(variantCount) as variantCount, MAX(CASE WHEN rn = 1 THEN ${outerQueryCol} END) as canonicalName, GROUP_CONCAT(CASE WHEN rn <= 10 THEN ${outerQueryCol} || ':::' || clicks || ':::' || impressions || ':::' || CAST(ROUND(CAST(sum_pos AS REAL) / NULLIF(impressions, 0) + 1, 1) AS TEXT) END, '||') as variants FROM per_variant GROUP BY joinKey`);
744
+ const canonKey = sql`COALESCE(NULLIF(${t.query_canonical}, ''), ${t.query})`;
745
+ const compiled = compileCollapsed(adapter, sql`WITH per_variant AS (SELECT ${canonKey} as joinKey, ${t.query} as query, SUM(${t.clicks}) as clicks, SUM(${t.impressions}) as impressions, SUM(${t.sum_position}) as sum_pos, ROW_NUMBER() OVER (PARTITION BY ${canonKey} ORDER BY SUM(${t.clicks}) DESC) as rn, COUNT(*) OVER (PARTITION BY ${canonKey}) as variantCount FROM ${table} ${whereExpr} GROUP BY ${canonKey}, ${t.query}) SELECT joinKey, MAX(variantCount) as variantCount, MAX(CASE WHEN rn = 1 THEN ${outerQueryCol} END) as canonicalName, GROUP_CONCAT(CASE WHEN rn <= 10 THEN ${outerQueryCol} || ':::' || clicks || ':::' || impressions || ':::' || CAST(ROUND(CAST(sum_pos AS REAL) / NULLIF(impressions, 0) + 1, 1) AS TEXT) END, '||') as variants FROM per_variant GROUP BY joinKey`);
730
746
  extras.push({
731
747
  key: "canonicalExtras",
732
748
  sql: compiled.sql,
@@ -802,6 +818,22 @@ function mergeExtras(rows, extrasResults) {
802
818
  return enriched;
803
819
  });
804
820
  }
821
+ const EXTRA_ROLLUP_IDS = { canonicalExtras: "query_canonical_variants" };
822
+ function createRollupExtrasOverlay(readRollupRows) {
823
+ return async ({ key, ctx, dateRange }) => {
824
+ const id = EXTRA_ROLLUP_IDS[key];
825
+ if (id === void 0) return null;
826
+ return readRollupRows({
827
+ id,
828
+ ctx: {
829
+ userId: ctx.userId,
830
+ siteId: ctx.siteId
831
+ },
832
+ dateRange,
833
+ ...ctx.searchType !== void 0 ? { searchType: ctx.searchType } : {}
834
+ });
835
+ };
836
+ }
805
837
  function collectInternalFilters(filter) {
806
838
  if (!filter || !("_filters" in filter)) return [];
807
839
  const flat = filter._filters;
@@ -856,6 +888,9 @@ function matchesMetricFilter(row, filter) {
856
888
  function matchesTopLevelPage(row) {
857
889
  return (normalizeUrl(dimensionValue(row, "page")).match(/\//g)?.length ?? 0) <= 1;
858
890
  }
891
+ function canonicalSourceWithinCoverage(source, windowEnd) {
892
+ return source.coversThrough === void 0 || windowEnd <= source.coversThrough;
893
+ }
859
894
  function runArgs(ctx, partitions) {
860
895
  return {
861
896
  ctx: {
@@ -870,9 +905,11 @@ function runArgs(ctx, partitions) {
870
905
  ...ctx.searchType !== void 0 ? { searchType: ctx.searchType } : {}
871
906
  };
872
907
  }
873
- async function runOptimizedQuery(runSQL, ctx, state, dateRange) {
874
- const adapter = createParquetResolverAdapter();
908
+ async function runOptimizedQuery(runSQL, ctx, state, dateRange, options = {}) {
875
909
  const base = runArgs(ctx, enumeratePartitions(dateRange.startDate, dateRange.endDate));
910
+ const probe = createParquetResolverAdapter({ canonicalFallback: options.canonicalFallback ?? false });
911
+ const useCanonicalSource = options.canonicalSource !== void 0 && (options.canonicalFallback ?? false) && canonicalSourceWithinCoverage(options.canonicalSource, dateRange.endDate) && canonicalRollupCovers(state, probe.capabilities);
912
+ const adapter = useCanonicalSource ? createParquetResolverAdapter({ canonicalFallback: false }) : probe;
876
913
  const optimized = resolveToSQLOptimized(state, {
877
914
  adapter,
878
915
  siteId: void 0
@@ -881,15 +918,31 @@ async function runOptimizedQuery(runSQL, ctx, state, dateRange) {
881
918
  adapter,
882
919
  siteId: void 0
883
920
  });
884
- const [optRes, ...extrasRows] = await Promise.all([runSQL({
921
+ const mainArgs = useCanonicalSource ? {
885
922
  ...base,
923
+ fileSets: { FILES: {
924
+ table: ctx.table,
925
+ keys: options.canonicalSource.keys
926
+ } }
927
+ } : base;
928
+ const resolveExtra = options.resolveExtra;
929
+ const [optRes, ...extrasRows] = await Promise.all([runSQL({
930
+ ...mainArgs,
886
931
  sql: optimized.sql,
887
932
  params: optimized.params
888
- }), ...extras.map((e) => runSQL({
889
- ...base,
890
- sql: e.sql,
891
- params: e.params
892
- }))]);
933
+ }), ...extras.map(async (e) => {
934
+ const overlaid = resolveExtra ? await resolveExtra({
935
+ key: e.key,
936
+ state,
937
+ ctx,
938
+ dateRange
939
+ }) : null;
940
+ return overlaid !== null ? { rows: overlaid } : runSQL({
941
+ ...base,
942
+ sql: e.sql,
943
+ params: e.params
944
+ });
945
+ })]);
893
946
  const firstRow = optRes.rows[0];
894
947
  const totalCount = Number(firstRow?.totalCount ?? 0);
895
948
  const totals = {
@@ -911,8 +964,10 @@ async function runOptimizedQuery(runSQL, ctx, state, dateRange) {
911
964
  }))
912
965
  };
913
966
  }
914
- async function runComparisonQuery(runSQL, ctx, current, previous, windows, filter) {
915
- const adapter = createParquetResolverAdapter();
967
+ async function runComparisonQuery(runSQL, ctx, current, previous, windows, filter, options = {}) {
968
+ const probe = createParquetResolverAdapter({ canonicalFallback: options.canonicalFallback ?? false });
969
+ const useCanonicalSource = options.canonicalSource !== void 0 && (options.canonicalFallback ?? false) && canonicalSourceWithinCoverage(options.canonicalSource, windows.current.endDate > windows.previous.endDate ? windows.current.endDate : windows.previous.endDate) && canonicalRollupCovers(current, probe.capabilities) && canonicalRollupCovers(previous, probe.capabilities);
970
+ const adapter = useCanonicalSource ? createParquetResolverAdapter({ canonicalFallback: false }) : probe;
916
971
  const comparison = resolveComparisonSQL(current, previous, {
917
972
  adapter,
918
973
  siteId: void 0
@@ -921,7 +976,14 @@ async function runComparisonQuery(runSQL, ctx, current, previous, windows, filte
921
976
  adapter,
922
977
  siteId: void 0
923
978
  });
924
- const base = runArgs(ctx, enumeratePartitions(windows.current.startDate < windows.previous.startDate ? windows.current.startDate : windows.previous.startDate, windows.current.endDate > windows.previous.endDate ? windows.current.endDate : windows.previous.endDate));
979
+ const partitions = enumeratePartitions(windows.current.startDate < windows.previous.startDate ? windows.current.startDate : windows.previous.startDate, windows.current.endDate > windows.previous.endDate ? windows.current.endDate : windows.previous.endDate);
980
+ const base = useCanonicalSource ? {
981
+ ...runArgs(ctx, partitions),
982
+ fileSets: { FILES: {
983
+ table: ctx.table,
984
+ keys: options.canonicalSource.keys
985
+ } }
986
+ } : runArgs(ctx, partitions);
925
987
  const main = await runSQL({
926
988
  ...base,
927
989
  sql: comparison.sql,
@@ -953,4 +1015,4 @@ function assertSchemaInSync(options) {
953
1015
  if (missing.length > 0 || extra.length > 0) throw new Error(`${label} drizzle schema for '${key}' drifted from SCHEMAS. Missing: [${missing.join(", ")}]. Extra: [${extra.join(", ")}].`);
954
1016
  }
955
1017
  }
956
- export { DIMENSION_SURFACES, LOGICAL_DATASETS, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, createIcebergResolverAdapter, createParquetResolverAdapter, createResolverAdapter, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
1018
+ export { DIMENSION_SURFACES, LOGICAL_DATASETS, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createResolverAdapter, createRollupExtrasOverlay, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, planCoveredByCanonicalRollup, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
@@ -1,7 +1,7 @@
1
1
  import { SearchType as SearchType$1, TableName as TableName$1 } from "../_chunks/storage.mjs";
2
2
  import { ComparisonFilter, ExtraQuery, ResolvedComparisonSQL, ResolvedSQL, ResolvedSQLOptimized, ResolverAdapter, ResolverOptions } from "../_chunks/types.mjs";
3
- import { PgTableKey, createIcebergResolverAdapter, createParquetResolverAdapter, pgResolverAdapter } from "../_chunks/pg-adapter.mjs";
4
- import { LogicalDataset, LogicalDataset as LogicalDataset$1, PlannerCapabilities, UnresolvableDatasetError, inferDataset as inferLogicalDataset, isDatasetResolvable } from "gscdump/query/plan";
3
+ import { PgTableKey, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, pgResolverAdapter } from "../_chunks/pg-adapter.mjs";
4
+ import { LogicalDataset, LogicalDataset as LogicalDataset$1, LogicalQueryPlan, PlannerCapabilities, UnresolvableDatasetError, inferDataset as inferLogicalDataset, isDatasetResolvable } from "gscdump/query/plan";
5
5
  import { SQL } from "drizzle-orm";
6
6
  import { BuilderState, Dimension, FilterInput, InternalFilter, Metric } from "gscdump/query";
7
7
  import { Grain, TableName } from "@gscdump/contracts";
@@ -35,6 +35,19 @@ interface SqlFragmentsConfig<TableKey extends string> {
35
35
  * against the alias.
36
36
  */
37
37
  tableRef?: (tableKey: TableKey) => SQL;
38
+ /**
39
+ * Opt-in correctness for canonical-primary lookups. When true, the
40
+ * `queryCanonical` dimension expression falls back to the raw `query` when
41
+ * the stored `query_canonical` is NULL (no normalizer ran at ingest) or `''`
42
+ * (a fully-stripped query like "free online"), i.e.
43
+ * `COALESCE(NULLIF(query_canonical, ''), query)`. This makes canonical a
44
+ * TOTAL key, valid for GROUP BY / comparison joins.
45
+ *
46
+ * Default (false) preserves legacy behaviour: the raw nullable column, so a
47
+ * NULL/'' bucket pollutes top results and — because `NULL = NULL` is UNKNOWN
48
+ * — double-counts in the gaining/losing FULL OUTER JOIN. See ADR-0018.
49
+ */
50
+ canonicalFallback?: boolean;
38
51
  }
39
52
  interface SqlFragments<TableKey extends string> {
40
53
  METRIC_NAMES: Metric[];
@@ -65,6 +78,16 @@ interface CreateResolverAdapterConfig<TableKey extends string> extends SqlFragme
65
78
  capabilities: PlannerCapabilities;
66
79
  }
67
80
  declare function createResolverAdapter<TableKey extends string>(config: CreateResolverAdapterConfig<TableKey>): ResolverAdapter<TableKey>;
81
+ /**
82
+ * True when `plan` can be served from the canonical-grained rollup instead of
83
+ * the raw `queries` fact partitions. Conservative: anything that would read a
84
+ * dropped column or the raw row grain disqualifies the query, so a false
85
+ * negative just falls back to live aggregation (correct, slower) — never wrong
86
+ * data.
87
+ */
88
+ declare function planCoveredByCanonicalRollup(plan: LogicalQueryPlan): boolean;
89
+ /** State-level convenience: build the plan then gate. */
90
+ declare function canonicalRollupCovers(state: BuilderState, capabilities: PlannerCapabilities): boolean;
68
91
  declare function resolveToSQLOptimized<TK extends string>(state: BuilderState, options: ResolverOptions<TK>): ResolvedSQLOptimized;
69
92
  declare function resolveToSQL<TK extends string>(state: BuilderState, options: ResolverOptions<TK>): ResolvedSQL;
70
93
  declare function buildTotalsSql<TK extends string>(state: BuilderState, options: ResolverOptions<TK>): {
@@ -77,14 +100,6 @@ declare function mergeExtras(rows: Record<string, unknown>[], extrasResults: {
77
100
  key: string;
78
101
  results: Record<string, unknown>[];
79
102
  }[]): Record<string, unknown>[];
80
- declare function getInternalFilters(filter: FilterInput | undefined): InternalFilter[];
81
- declare function getDimensionFilters(filter: FilterInput | undefined, isMetricDimension: (dim: string) => dim is Metric): InternalFilter[];
82
- declare function getFilterDimensions(filter: FilterInput | undefined, isMetricDimension: (dim: string) => dim is Metric): Dimension[];
83
- declare function metricValue(row: Record<string, unknown>, metric: string): number;
84
- declare function dimensionValue(row: Record<string, unknown>, dimension: string): string;
85
- declare function matchesDimensionFilter(row: Record<string, unknown>, filter: InternalFilter): boolean;
86
- declare function matchesMetricFilter(row: Record<string, unknown>, filter: InternalFilter): boolean;
87
- declare function matchesTopLevelPage(row: Record<string, unknown>): boolean;
88
103
  interface RunQueryCtx {
89
104
  userId: string;
90
105
  siteId: string;
@@ -118,6 +133,60 @@ interface RunSQLFn {
118
133
  rows: Array<Record<string, unknown>>;
119
134
  }>;
120
135
  }
136
+ /**
137
+ * Optional overlay that serves a resolver extra (e.g. canonical-variant
138
+ * grouping, keyed `'canonicalExtras'`) from a precomputed source — typically a
139
+ * materialised rollup — instead of the live window-function SQL. Return the
140
+ * rows in the exact shape the live extra produces (`mergeExtras` consumes
141
+ * either source unchanged), or `null` to decline so the caller falls back to
142
+ * the live query. Pure seam: storage/tenant routing lives in the host's
143
+ * implementation, not here. See ADR-0017.
144
+ */
145
+ interface ResolveExtraFn {
146
+ (opts: {
147
+ key: string;
148
+ state: BuilderState;
149
+ ctx: RunQueryCtx;
150
+ dateRange: {
151
+ startDate: string;
152
+ endDate: string;
153
+ };
154
+ }): Promise<Array<Record<string, unknown>> | null>;
155
+ }
156
+ interface RunOptimizedQueryOptions {
157
+ /** Overlay tried per extra before the live SQL; absent → today's live path. */
158
+ resolveExtra?: ResolveExtraFn;
159
+ /**
160
+ * Opt-in canonical-primary correctness: group/compare `queryCanonical` as a
161
+ * total key (NULL/'' folds to the raw `query`). Default false = legacy raw
162
+ * nullable column. See ADR-0018.
163
+ */
164
+ canonicalFallback?: boolean;
165
+ /**
166
+ * Opt-in canonical-primary performance (ADR-0018 Gap 2): object keys of the
167
+ * `query_canonical_daily` rollup parquet(s). When supplied AND the query is
168
+ * coverable (`canonicalRollupCovers`) AND `canonicalFallback` is on AND the
169
+ * window is within the rollup's coverage, the MAIN query reads these
170
+ * pre-summed `(query_canonical × date)` rows instead of re-aggregating raw
171
+ * partitions; variant extras still read raw. Ignored (live path) on any miss,
172
+ * so a mis-wired host degrades to correct-but-slow, never wrong.
173
+ *
174
+ * `canonicalFallback` is REQUIRED: the rollup is built with
175
+ * `COALESCE(NULLIF(query_canonical, ''), query)` (fallback semantics), so
176
+ * serving it to a legacy (`canonicalFallback: false`) caller would change
177
+ * NULL/'' rows from legacy buckets to raw-query keys. The rollup is already
178
+ * null-free, so the rollup READ itself runs without fallback.
179
+ *
180
+ * `coversThrough` (ISO `YYYY-MM-DD`, the rollup's newest covered date) gates
181
+ * staleness: the source is used only when `dateRange.endDate <= coversThrough`,
182
+ * else the live path serves the window so the recent tail is never silently
183
+ * undercounted. Omit to assert full coverage (use with care).
184
+ */
185
+ canonicalSource?: {
186
+ keys: string[];
187
+ coversThrough?: string;
188
+ };
189
+ }
121
190
  interface OptimizedQueryResult {
122
191
  rows: Array<Record<string, unknown>>;
123
192
  totalCount: number;
@@ -140,7 +209,7 @@ interface ComparisonQueryResult {
140
209
  declare function runOptimizedQuery(runSQL: RunSQLFn, ctx: RunQueryCtx, state: BuilderState, dateRange: {
141
210
  startDate: string;
142
211
  endDate: string;
143
- }): Promise<OptimizedQueryResult>;
212
+ }, options?: RunOptimizedQueryOptions): Promise<OptimizedQueryResult>;
144
213
  declare function runComparisonQuery(runSQL: RunSQLFn, ctx: RunQueryCtx, current: BuilderState, previous: BuilderState, windows: {
145
214
  current: {
146
215
  startDate: string;
@@ -150,7 +219,55 @@ declare function runComparisonQuery(runSQL: RunSQLFn, ctx: RunQueryCtx, current:
150
219
  startDate: string;
151
220
  endDate: string;
152
221
  };
153
- }, filter?: ComparisonFilter): Promise<ComparisonQueryResult>;
222
+ }, filter?: ComparisonFilter, options?: {
223
+ canonicalFallback?: boolean;
224
+ canonicalSource?: {
225
+ keys: string[];
226
+ coversThrough?: string;
227
+ };
228
+ }): Promise<ComparisonQueryResult>;
229
+ /**
230
+ * Host-supplied reader: return the materialised rollup's rows for an
231
+ * `(id, tenant, slice)`, in the exact shape the live extra produces, or `null`
232
+ * when no rollup exists (first sync, never built, stale) so the overlay
233
+ * declines and the resolver falls back to the live query. Typically wired with
234
+ * `readLatestRollup` + a `read_parquet` of the pointer.
235
+ *
236
+ * `dateRange` is the request window. `query_canonical_variants` is full-history
237
+ * (its grouping/variant metrics span all dates), but `buildExtrasQueries`
238
+ * windows the live `canonicalExtras` to the requested range — so for a narrow
239
+ * window the reader MUST decline (return `null`) rather than attach
240
+ * out-of-window variantCount/canonicalName/variants. A common rule: serve only
241
+ * when the request window covers full history.
242
+ */
243
+ interface RollupRowsReader {
244
+ (opts: {
245
+ id: string;
246
+ ctx: {
247
+ userId: string;
248
+ siteId: string;
249
+ };
250
+ searchType?: SearchType$1;
251
+ dateRange: {
252
+ startDate: string;
253
+ endDate: string;
254
+ };
255
+ }): Promise<Array<Record<string, unknown>> | null>;
256
+ }
257
+ /**
258
+ * Build a {@link ResolveExtraFn} that serves resolver extras from materialised
259
+ * rollups when one is mapped for the extra's key, else returns `null` to fall
260
+ * back to the live SQL. Pure wiring around the host's `readRollupRows`.
261
+ */
262
+ declare function createRollupExtrasOverlay(readRollupRows: RollupRowsReader): ResolveExtraFn;
263
+ declare function getInternalFilters(filter: FilterInput | undefined): InternalFilter[];
264
+ declare function getDimensionFilters(filter: FilterInput | undefined, isMetricDimension: (dim: string) => dim is Metric): InternalFilter[];
265
+ declare function getFilterDimensions(filter: FilterInput | undefined, isMetricDimension: (dim: string) => dim is Metric): Dimension[];
266
+ declare function metricValue(row: Record<string, unknown>, metric: string): number;
267
+ declare function dimensionValue(row: Record<string, unknown>, dimension: string): string;
268
+ declare function matchesDimensionFilter(row: Record<string, unknown>, filter: InternalFilter): boolean;
269
+ declare function matchesMetricFilter(row: Record<string, unknown>, filter: InternalFilter): boolean;
270
+ declare function matchesTopLevelPage(row: Record<string, unknown>): boolean;
154
271
  interface AssertSchemaInSyncOptions {
155
272
  /** Label used in the thrown error (e.g. 'browser', 'sqlite'). */
156
273
  label: string;
@@ -164,4 +281,4 @@ interface AssertSchemaInSyncOptions {
164
281
  mode: 'exact' | 'superset';
165
282
  }
166
283
  declare function assertSchemaInSync(options: AssertSchemaInSyncOptions): void;
167
- export { type AssertSchemaInSyncOptions, type ComparisonFilter, type ComparisonQueryResult, type CreateResolverAdapterConfig, DIMENSION_SURFACES, type DimensionBinding, type DimensionSurface, type ExtraQuery, LOGICAL_DATASETS, type LogicalDataset, type LogicalDatasetDefinition, type OptimizedQueryResult, type PgTableKey, type ResolvedComparisonSQL, type ResolvedSQL, type ResolvedSQLOptimized, type ResolverAdapter, type ResolverOptions, type RunQueryCtx, type RunSQLFn, type SqlFragments, type SqlFragmentsConfig, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, createIcebergResolverAdapter, createParquetResolverAdapter, createResolverAdapter, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
284
+ export { type AssertSchemaInSyncOptions, type ComparisonFilter, type ComparisonQueryResult, type CreateResolverAdapterConfig, DIMENSION_SURFACES, type DimensionBinding, type DimensionSurface, type ExtraQuery, LOGICAL_DATASETS, type LogicalDataset, type LogicalDatasetDefinition, type OptimizedQueryResult, type PgTableKey, type ResolveExtraFn, type ResolvedComparisonSQL, type ResolvedSQL, type ResolvedSQLOptimized, type ResolverAdapter, type ResolverAdapterOptions, type ResolverOptions, type RollupRowsReader, type RunOptimizedQueryOptions, type RunQueryCtx, type RunSQLFn, type SqlFragments, type SqlFragmentsConfig, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createResolverAdapter, createRollupExtrasOverlay, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, planCoveredByCanonicalRollup, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
@@ -1,2 +1,2 @@
1
- import { DIMENSION_SURFACES, LOGICAL_DATASETS, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, createIcebergResolverAdapter, createParquetResolverAdapter, createResolverAdapter, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface } from "../_chunks/resolver.mjs";
2
- export { DIMENSION_SURFACES, LOGICAL_DATASETS, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, createIcebergResolverAdapter, createParquetResolverAdapter, createResolverAdapter, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
1
+ import { DIMENSION_SURFACES, LOGICAL_DATASETS, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createResolverAdapter, createRollupExtrasOverlay, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, planCoveredByCanonicalRollup, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface } from "../_chunks/resolver.mjs";
2
+ export { DIMENSION_SURFACES, LOGICAL_DATASETS, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createResolverAdapter, createRollupExtrasOverlay, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, planCoveredByCanonicalRollup, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
@@ -283,6 +283,47 @@ declare const topKeywords28dRollup: RollupDef;
283
283
  * coexist during a migration.
284
284
  */
285
285
  declare const topKeywords28dParquetRollup: RollupDef;
286
+ /**
287
+ * Materialises canonical-query variant grouping so the read path
288
+ * (`buildExtrasQueries` in `resolver/compile.ts`) becomes a passthrough scan
289
+ * instead of two window passes (`ROW_NUMBER`/`COUNT` over `PARTITION BY
290
+ * query_canonical`) plus a `GROUP_CONCAT` over the whole `queries` table on
291
+ * every request — work that is single-threaded under DuckDB-WASM/Workers and
292
+ * scales with table size. See ADR-0017.
293
+ *
294
+ * One row per `query_canonical` group, columns named 1:1 with the live query's
295
+ * output (`joinKey`, `variantCount`, `canonicalName`, `variants`) so
296
+ * `mergeExtras` consumes either source unchanged. `variants` packs the top-10
297
+ * variants as `query:::clicks:::impressions:::position` joined by `||`,
298
+ * identical to the live composer.
299
+ *
300
+ * Full history (`windowDays: null`), not a trailing window: grouping metadata
301
+ * is global (which variant is canonical, how many variants exist) and stays
302
+ * stable across requests rather than shifting with each query's date range.
303
+ * Reflects the last sync/compaction, not the live tail — readers that need the
304
+ * tail can layer a recent-overlay later (the envelope carries `builtAt`).
305
+ */
306
+ declare const queryCanonicalVariantsRollup: RollupDef;
307
+ /**
308
+ * Canonical-grained fact aggregate (ADR-0018 Gap 2): pre-sums the raw
309
+ * `(query × date)` query rows to `(query_canonical × date)`, so canonical-
310
+ * primary top/gaining/losing reads a small pre-aggregated table instead of
311
+ * re-collapsing variants on every request. Metrics are additive, so summing
312
+ * these per-date sums over a window is exact — identical to aggregating the raw
313
+ * rows.
314
+ *
315
+ * Null-free by construction: groups by `COALESCE(NULLIF(query_canonical, ''),
316
+ * query)`, the same total-key expression the opt-in read path uses (ADR-0018
317
+ * Gap 1), so the rollup never carries a NULL/'' canonical bucket and the read
318
+ * path needs no fallback when pointed at it.
319
+ *
320
+ * Date-grained full history (`windowDays: null`): one rollup serves every date
321
+ * range (reads filter by `date`) and both windows of a comparison. Opt-in (not
322
+ * in `DEFAULT_ROLLUPS`); the host points the main query's file set at it for
323
+ * queries the rollup covers (see `canonicalRollupCovers` /
324
+ * `RunOptimizedQueryOptions.canonicalSource`).
325
+ */
326
+ declare const queryCanonicalDailyRollup: RollupDef;
286
327
  /**
287
328
  * Aggregates the per-URL Indexing API metadata entity store (populated by
288
329
  * `gscdump entities indexing snapshot`) into daily counts of `URL_UPDATED`
@@ -360,4 +401,12 @@ declare function rebuildDailyFromHourly(opts: RebuildDailyFromHourlyOptions): Pr
360
401
  rowsWritten: number;
361
402
  }>;
362
403
  declare const DEFAULT_ROLLUPS: readonly RollupDef[];
363
- export { DEFAULT_ROLLUPS, ParquetRollupPointer, RebuildDailyFromHourlyOptions, RebuildRollupResult, RebuildRollupsOptions, RollupBucket, RollupCtx, RollupDef, RollupEngine, RollupEnvelope, WINDOW_BYTE_BUDGET, dailyTotalsRollup, indexPercentRollup, indexingHealthRollup, indexingMetadataRollup, partitionDaySpan, partitionsInRange, planRollupWindows, readLatestRollup, rebuildDailyFromHourly, rebuildRollups, rollupKey, rollupParquetKey, runWindowed, sitemapChanges28dRollup, sitemapHealthRollup, topCountries28dRollup, topKeywords28dParquetRollup, topKeywords28dRollup, topPages28dRollup, weeklyTotalsRollup };
404
+ /**
405
+ * Canonical-primary rollups (ADR-0017 / ADR-0018). Opt-in — kept out of
406
+ * `DEFAULT_ROLLUPS` because they only pay off once the consumer queries by
407
+ * `queryCanonical` and wires the read seams (`resolveExtra` /
408
+ * `canonicalSource`). Hosts opt in by concatenating these onto their def list
409
+ * (CLI: `gscdump rollups --with-canonical`).
410
+ */
411
+ declare const CANONICAL_ROLLUPS: readonly RollupDef[];
412
+ export { CANONICAL_ROLLUPS, DEFAULT_ROLLUPS, ParquetRollupPointer, 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 };
package/dist/rollups.mjs CHANGED
@@ -534,6 +534,136 @@ const topKeywords28dParquetRollup = {
534
534
  }));
535
535
  }
536
536
  };
537
+ const queryCanonicalVariantsRollup = {
538
+ id: "query_canonical_variants",
539
+ windowDays: null,
540
+ format: "parquet",
541
+ parquetColumns: [
542
+ {
543
+ name: "joinKey",
544
+ type: "VARCHAR",
545
+ nullable: false
546
+ },
547
+ {
548
+ name: "variantCount",
549
+ type: "BIGINT",
550
+ nullable: false
551
+ },
552
+ {
553
+ name: "canonicalName",
554
+ type: "VARCHAR",
555
+ nullable: true
556
+ },
557
+ {
558
+ name: "variants",
559
+ type: "VARCHAR",
560
+ nullable: true
561
+ }
562
+ ],
563
+ parquetSortKey: ["joinKey"],
564
+ async build({ engine, ctx, searchType }) {
565
+ const parts = await engine.listPartitions({
566
+ ctx,
567
+ table: "queries",
568
+ ...searchType !== void 0 ? { searchType } : {}
569
+ });
570
+ if (parts.length === 0) return [];
571
+ const partitions = parts.map((p) => p.partition);
572
+ return (await engine.runSQL({
573
+ ctx,
574
+ table: "queries",
575
+ fileSets: { FILES: {
576
+ table: "queries",
577
+ partitions
578
+ } },
579
+ ...searchType !== void 0 ? { searchType } : {},
580
+ sql: `
581
+ WITH per_variant AS (
582
+ SELECT
583
+ COALESCE(NULLIF(query_canonical, ''), query) AS joinKey,
584
+ query AS query,
585
+ SUM(clicks) AS clicks,
586
+ SUM(impressions) AS impressions,
587
+ SUM(sum_position) AS sum_pos,
588
+ ROW_NUMBER() OVER (PARTITION BY COALESCE(NULLIF(query_canonical, ''), query) ORDER BY SUM(clicks) DESC) AS rn,
589
+ COUNT(*) OVER (PARTITION BY COALESCE(NULLIF(query_canonical, ''), query)) AS variantCount
590
+ FROM read_parquet({{FILES}}, union_by_name = true)
591
+ GROUP BY COALESCE(NULLIF(query_canonical, ''), query), query
592
+ )
593
+ SELECT
594
+ joinKey,
595
+ MAX(variantCount)::BIGINT AS variantCount,
596
+ MAX(CASE WHEN rn = 1 THEN query END) AS canonicalName,
597
+ GROUP_CONCAT(CASE WHEN rn <= 10 THEN query || ':::' || clicks || ':::' || impressions || ':::' || CAST(ROUND(CAST(sum_pos AS REAL) / NULLIF(impressions, 0) + 1, 1) AS TEXT) END, '||') AS variants
598
+ FROM per_variant
599
+ GROUP BY joinKey
600
+ `
601
+ })).rows.map((r) => ({
602
+ joinKey: String(r.joinKey),
603
+ variantCount: BigInt(r.variantCount),
604
+ canonicalName: r.canonicalName == null ? null : String(r.canonicalName),
605
+ variants: r.variants == null ? null : String(r.variants)
606
+ }));
607
+ }
608
+ };
609
+ const queryCanonicalDailyRollup = {
610
+ id: "query_canonical_daily",
611
+ windowDays: null,
612
+ format: "parquet",
613
+ parquetColumns: [
614
+ {
615
+ name: "query_canonical",
616
+ type: "VARCHAR",
617
+ nullable: false
618
+ },
619
+ {
620
+ name: "date",
621
+ type: "DATE",
622
+ nullable: false
623
+ },
624
+ {
625
+ name: "clicks",
626
+ type: "BIGINT",
627
+ nullable: false
628
+ },
629
+ {
630
+ name: "impressions",
631
+ type: "BIGINT",
632
+ nullable: false
633
+ },
634
+ {
635
+ name: "sum_position",
636
+ type: "DOUBLE",
637
+ nullable: false
638
+ }
639
+ ],
640
+ parquetSortKey: ["date", "query_canonical"],
641
+ async build({ engine, ctx, searchType }) {
642
+ return (await runWindowed({
643
+ engine,
644
+ ctx,
645
+ table: "queries",
646
+ ...searchType !== void 0 ? { searchType } : {},
647
+ sqlFor: (w) => `
648
+ SELECT
649
+ COALESCE(NULLIF(query_canonical, ''), query) AS query_canonical,
650
+ CAST(date AS VARCHAR) AS date,
651
+ SUM(clicks)::BIGINT AS clicks,
652
+ SUM(impressions)::BIGINT AS impressions,
653
+ SUM(sum_position)::DOUBLE AS sum_position
654
+ FROM read_parquet({{FILES}}, union_by_name = true)
655
+ WHERE date >= '${w.start}' AND date <= '${w.end}'
656
+ GROUP BY COALESCE(NULLIF(query_canonical, ''), query), date
657
+ `
658
+ })).map((r) => ({
659
+ query_canonical: String(r.query_canonical),
660
+ date: String(r.date),
661
+ clicks: BigInt(r.clicks),
662
+ impressions: BigInt(r.impressions),
663
+ sum_position: Number(r.sum_position)
664
+ }));
665
+ }
666
+ };
537
667
  const indexingMetadataRollup = {
538
668
  id: "indexing_metadata",
539
669
  windowDays: null,
@@ -845,4 +975,5 @@ const DEFAULT_ROLLUPS = [
845
975
  sitemapHealthRollup,
846
976
  sitemapChanges28dRollup
847
977
  ];
848
- export { DEFAULT_ROLLUPS, WINDOW_BYTE_BUDGET, dailyTotalsRollup, indexPercentRollup, indexingHealthRollup, indexingMetadataRollup, partitionDaySpan, partitionsInRange, planRollupWindows, readLatestRollup, rebuildDailyFromHourly, rebuildRollups, rollupKey, rollupParquetKey, runWindowed, sitemapChanges28dRollup, sitemapHealthRollup, topCountries28dRollup, topKeywords28dParquetRollup, topKeywords28dRollup, topPages28dRollup, weeklyTotalsRollup };
978
+ const CANONICAL_ROLLUPS = [queryCanonicalVariantsRollup, queryCanonicalDailyRollup];
979
+ export { CANONICAL_ROLLUPS, DEFAULT_ROLLUPS, 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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "0.29.0",
4
+ "version": "0.30.0",
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",
@@ -191,8 +191,8 @@
191
191
  "hyparquet": "^1.26.1",
192
192
  "hyparquet-writer": "^0.16.1",
193
193
  "proper-lockfile": "^4.1.2",
194
- "@gscdump/contracts": "0.29.0",
195
- "gscdump": "0.29.0"
194
+ "@gscdump/contracts": "0.30.0",
195
+ "gscdump": "0.30.0"
196
196
  },
197
197
  "devDependencies": {
198
198
  "@duckdb/duckdb-wasm": "^1.32.0",