@gscdump/engine 0.32.12 → 0.33.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.
package/dist/index.d.mts CHANGED
@@ -6,7 +6,7 @@ import { InspectionVerdict, SchedulePolicy, ScheduleState, fixedPolicy, inspecti
6
6
  import { IcebergTableName, Sink, SinkCapabilities, SinkCloseResult, SinkOptions, SinkSlice, SinkWriteResult } from "./_chunks/sink.mjs";
7
7
  import { GscApiRow, IngestOptions, RowAccumulator, RowAccumulatorOptions, assembleDatesRow, createRowAccumulator, toPath, toSumPosition, transformGscRow } from "./ingest.mjs";
8
8
  import { FILES_PLACEHOLDER, ResolvedQuery, resolveParquetSQL, substituteNamedFiles } from "./_chunks/planner.mjs";
9
- import { createIcebergResolverAdapter, createParquetResolverAdapter, pgResolverAdapter } from "./_chunks/pg-adapter.mjs";
9
+ import { createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter } from "./_chunks/pg-adapter.mjs";
10
10
  import { rebuildDailyFromHourly } from "./rollups.mjs";
11
11
  import { ENGINE_QUERY_CAPABILITIES, createSqlQuerySource } from "./_chunks/index.mjs";
12
12
  import { bindLiterals, formatLiteral } from "./sql-bind.mjs";
@@ -209,4 +209,4 @@ declare const MIN_SYNC_IMPRESSIONS = 1;
209
209
  declare const MIN_COUNTRY_IMPRESSIONS = 10;
210
210
  declare const MAX_SITEMAP_URLS_PER_SITE = 50000;
211
211
  declare const MAX_TRACKED_URLS_PER_SITE = 200000;
212
- export { type CodecCtx, type ColumnDef, type ColumnType, type CompactionThresholds, type CompactionTier, type CreateIngestAccumulatorOptions, DEFAULT_SEARCH_TYPE, type DataSource, type DateWeight, type DrizzleSchema, type DuckDBFactory, type DuckDBHandle, ENGINE_QUERY_CAPABILITIES, EngineError, EngineErrorKind, type EngineOptions, FILES_PLACEHOLDER, type FileSetRef, type FinalizeOptions, type FinalizeResult, type GcCtx, type Grain, type GscApiRow, type InMemorySink, type IngestAccumulator, type IngestAccumulatorCtx, type IngestAccumulatorEngine, type IngestAccumulatorHooks, type IngestOptions, type InspectionVerdict, type ListLiveFilter, type LockScope, MAX_DAY_BYTES, MAX_GSC_PAGES_R2, MAX_SITEMAP_URLS_PER_SITE, MAX_TRACKED_URLS_PER_SITE, MIN_COUNTRY_IMPRESSIONS, MIN_SYNC_IMPRESSIONS, type ManifestEntry, type ManifestPurgeResult, type ManifestStore, type ParquetCodec, type PurgeFilter, type PurgeResult, type PurgeUrlsResult, type QueryCtx, type QueryExecuteOptions, type QueryExecuteResult, type QueryExecutor, type QueryProfiler, type QueryResult, type QuerySpan, ROW_LIMIT_R2, type ResolvedQuery, type Row, type RowAccumulator, type RowAccumulatorOptions, type RunSQLOptions, SCHEMAS, type SchedulePolicy, type ScheduleState, type SearchType, type Sink, type SinkCapabilities, type SinkCloseResult, type SinkOptions, type SinkSlice, type SinkWriteResult, type StorageEngine, type StoredRow, type SyncState, type SyncStateDetail, type SyncStateFilter, type SyncStateKind, type SyncStateScope, type SyncTableName, TABLES_BY_SEARCH_TYPE, TABLE_METADATA, TABLE_TIERS, TIER_PRIORITY, type TableName, type TableSchema, type TableTier, type TenantCtx, type TieredTableName, WEIGHT_PRIORITY, type Watermark, type WatermarkFilter, type WatermarkScope, type WriteCtx, type WriteResult, allTables, assembleDatesRow, bindLiterals, canonicalEmptyParquetSchema, coerceRow, coerceRows, collectSpans, countries, createDuckDBCodec, createDuckDBExecutor, createIcebergResolverAdapter, createInMemorySink, createIngestAccumulator, createNoopIngestAccumulator, createParquetResolverAdapter, createQueryProfiler, createRowAccumulator, createSqlQuerySource, createStorageEngine, currentSchemaVersion, dates, dayPartition, dimensionToColumn, drizzleSchema, engineErrorToException, engineErrors, enumeratePartitions, fixedPolicy, formatEngineError, formatLiteral, getDateWeight, getTableTier, getTablesForTier, hourPartition, hourly_pages, inferLegacyTier, inferSearchType, inferTable, inspectionPolicy, isEngineError, objectKey, page_queries, pages, parseEnabledSearchTypes, pgResolverAdapter, queries, rebuildDailyFromHourly, resolveParquetSQL, sitemapPolicy, substituteNamedFiles, toPath, toSumPosition, transformGscRow, validateEnabledSearchTypes, validateEnabledSearchTypesResult };
212
+ export { type CodecCtx, type ColumnDef, type ColumnType, type CompactionThresholds, type CompactionTier, type CreateIngestAccumulatorOptions, DEFAULT_SEARCH_TYPE, type DataSource, type DateWeight, type DrizzleSchema, type DuckDBFactory, type DuckDBHandle, ENGINE_QUERY_CAPABILITIES, EngineError, EngineErrorKind, type EngineOptions, FILES_PLACEHOLDER, type FileSetRef, type FinalizeOptions, type FinalizeResult, type GcCtx, type Grain, type GscApiRow, type InMemorySink, type IngestAccumulator, type IngestAccumulatorCtx, type IngestAccumulatorEngine, type IngestAccumulatorHooks, type IngestOptions, type InspectionVerdict, type ListLiveFilter, type LockScope, MAX_DAY_BYTES, MAX_GSC_PAGES_R2, MAX_SITEMAP_URLS_PER_SITE, MAX_TRACKED_URLS_PER_SITE, MIN_COUNTRY_IMPRESSIONS, MIN_SYNC_IMPRESSIONS, type ManifestEntry, type ManifestPurgeResult, type ManifestStore, type ParquetCodec, type PurgeFilter, type PurgeResult, type PurgeUrlsResult, type QueryCtx, type QueryExecuteOptions, type QueryExecuteResult, type QueryExecutor, type QueryProfiler, type QueryResult, type QuerySpan, ROW_LIMIT_R2, type ResolvedQuery, type Row, type RowAccumulator, type RowAccumulatorOptions, type RunSQLOptions, SCHEMAS, type SchedulePolicy, type ScheduleState, type SearchType, type Sink, type SinkCapabilities, type SinkCloseResult, type SinkOptions, type SinkSlice, type SinkWriteResult, type StorageEngine, type StoredRow, type SyncState, type SyncStateDetail, type SyncStateFilter, type SyncStateKind, type SyncStateScope, type SyncTableName, TABLES_BY_SEARCH_TYPE, TABLE_METADATA, TABLE_TIERS, TIER_PRIORITY, type TableName, type TableSchema, type TableTier, type TenantCtx, type TieredTableName, WEIGHT_PRIORITY, type Watermark, type WatermarkFilter, type WatermarkScope, type WriteCtx, type WriteResult, allTables, assembleDatesRow, bindLiterals, canonicalEmptyParquetSchema, coerceRow, coerceRows, collectSpans, countries, createDuckDBCodec, createDuckDBExecutor, createIcebergResolverAdapter, createInMemorySink, createIngestAccumulator, createNoopIngestAccumulator, createParquetResolverAdapter, createQueryProfiler, createR2SqlResolverAdapter, createRowAccumulator, createSqlQuerySource, createStorageEngine, currentSchemaVersion, dates, dayPartition, dimensionToColumn, drizzleSchema, engineErrorToException, engineErrors, enumeratePartitions, fixedPolicy, formatEngineError, formatLiteral, getDateWeight, getTableTier, getTablesForTier, hourPartition, hourly_pages, inferLegacyTier, inferSearchType, inferTable, inspectionPolicy, isEngineError, objectKey, page_queries, pages, parseEnabledSearchTypes, pgResolverAdapter, queries, rebuildDailyFromHourly, resolveParquetSQL, sitemapPolicy, substituteNamedFiles, toPath, toSumPosition, transformGscRow, validateEnabledSearchTypes, validateEnabledSearchTypesResult };
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import { bindLiterals, formatLiteral } from "./sql-bind.mjs";
8
8
  import { MAX_DAY_BYTES, canonicalEmptyParquetSchema, createDuckDBCodec, createDuckDBExecutor, createStorageEngine } from "./_chunks/engine.mjs";
9
9
  import { assembleDatesRow, createRowAccumulator, toPath, toSumPosition, transformGscRow } from "./ingest.mjs";
10
10
  import "./planner.mjs";
11
- import { createIcebergResolverAdapter, createParquetResolverAdapter, pgResolverAdapter } from "./_chunks/resolver.mjs";
11
+ import { createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter } from "./_chunks/resolver.mjs";
12
12
  import { rebuildDailyFromHourly } from "./rollups.mjs";
13
13
  import { fixedPolicy, inspectionPolicy, sitemapPolicy } from "./schedule.mjs";
14
14
  import { err, ok, unwrapResult } from "gscdump/result";
@@ -308,4 +308,4 @@ const MIN_SYNC_IMPRESSIONS = 1;
308
308
  const MIN_COUNTRY_IMPRESSIONS = 10;
309
309
  const MAX_SITEMAP_URLS_PER_SITE = 5e4;
310
310
  const MAX_TRACKED_URLS_PER_SITE = 2e5;
311
- export { DEFAULT_SEARCH_TYPE, ENGINE_QUERY_CAPABILITIES, FILES_PLACEHOLDER, MAX_DAY_BYTES, MAX_GSC_PAGES_R2, MAX_SITEMAP_URLS_PER_SITE, MAX_TRACKED_URLS_PER_SITE, MIN_COUNTRY_IMPRESSIONS, MIN_SYNC_IMPRESSIONS, ROW_LIMIT_R2, SCHEMAS, TABLES_BY_SEARCH_TYPE, TABLE_METADATA, TABLE_TIERS, TIER_PRIORITY, WEIGHT_PRIORITY, allTables, assembleDatesRow, bindLiterals, canonicalEmptyParquetSchema, coerceRow, coerceRows, collectSpans, countries, createDuckDBCodec, createDuckDBExecutor, createIcebergResolverAdapter, createInMemorySink, createIngestAccumulator, createNoopIngestAccumulator, createParquetResolverAdapter, createQueryProfiler, createRowAccumulator, createSqlQuerySource, createStorageEngine, currentSchemaVersion, dates, dayPartition, dimensionToColumn, drizzleSchema, engineErrorToException, engineErrors, enumeratePartitions, fixedPolicy, formatEngineError, formatLiteral, getDateWeight, getTableTier, getTablesForTier, hourPartition, hourly_pages, inferLegacyTier, inferSearchType, inferTable, inspectionPolicy, isEngineError, objectKey, page_queries, pages, parseEnabledSearchTypes, pgResolverAdapter, queries, rebuildDailyFromHourly, resolveParquetSQL, sitemapPolicy, substituteNamedFiles, toPath, toSumPosition, transformGscRow, validateEnabledSearchTypes, validateEnabledSearchTypesResult };
311
+ export { DEFAULT_SEARCH_TYPE, ENGINE_QUERY_CAPABILITIES, FILES_PLACEHOLDER, MAX_DAY_BYTES, MAX_GSC_PAGES_R2, MAX_SITEMAP_URLS_PER_SITE, MAX_TRACKED_URLS_PER_SITE, MIN_COUNTRY_IMPRESSIONS, MIN_SYNC_IMPRESSIONS, ROW_LIMIT_R2, SCHEMAS, TABLES_BY_SEARCH_TYPE, TABLE_METADATA, TABLE_TIERS, TIER_PRIORITY, WEIGHT_PRIORITY, allTables, assembleDatesRow, bindLiterals, canonicalEmptyParquetSchema, coerceRow, coerceRows, collectSpans, countries, createDuckDBCodec, createDuckDBExecutor, createIcebergResolverAdapter, createInMemorySink, createIngestAccumulator, createNoopIngestAccumulator, createParquetResolverAdapter, createQueryProfiler, createR2SqlResolverAdapter, createRowAccumulator, createSqlQuerySource, createStorageEngine, currentSchemaVersion, dates, dayPartition, dimensionToColumn, drizzleSchema, engineErrorToException, engineErrors, enumeratePartitions, fixedPolicy, formatEngineError, formatLiteral, getDateWeight, getTableTier, getTablesForTier, hourPartition, hourly_pages, inferLegacyTier, inferSearchType, inferTable, inspectionPolicy, isEngineError, objectKey, page_queries, pages, parseEnabledSearchTypes, pgResolverAdapter, queries, rebuildDailyFromHourly, resolveParquetSQL, sitemapPolicy, substituteNamedFiles, toPath, toSumPosition, transformGscRow, validateEnabledSearchTypes, validateEnabledSearchTypesResult };
package/dist/ingest.d.mts CHANGED
@@ -16,12 +16,7 @@ interface GscApiRow {
16
16
  position: number;
17
17
  }
18
18
  interface IngestOptions {
19
- /**
20
- * Canonical form of a query string, stored alongside `query` as
21
- * `query_canonical`. Site-specific (e.g. synonym groups, stemming); if
22
- * omitted, `query_canonical` is null. Applied to `queries` +
23
- * `page_queries` tables only.
24
- */
19
+ /** @deprecated Canonical query data is built through query_dim, not fact rows. */
25
20
  normalizeQuery?: (query: string) => string | null | undefined;
26
21
  /** Date for one-day `searchAppearance` total queries, whose keys omit date. */
27
22
  date?: string;
package/dist/ingest.mjs CHANGED
@@ -54,7 +54,6 @@ function transformGscRow(table, apiRow, options = {}) {
54
54
  date,
55
55
  row: {
56
56
  query,
57
- query_canonical: options.normalizeQuery?.(query) ?? null,
58
57
  date,
59
58
  clicks,
60
59
  impressions,
@@ -122,13 +121,11 @@ function transformGscRow(table, apiRow, options = {}) {
122
121
  if (table === "search_appearance_queries") {
123
122
  const query = String(keys[0] ?? "");
124
123
  const date = String(keys[1] ?? "");
125
- const query_canonical = options.normalizeQuery?.(query) ?? null;
126
124
  return {
127
125
  date,
128
126
  row: {
129
127
  searchAppearance: String(options.searchAppearance ?? ""),
130
128
  query,
131
- query_canonical,
132
129
  date,
133
130
  clicks,
134
131
  impressions,
@@ -139,14 +136,12 @@ function transformGscRow(table, apiRow, options = {}) {
139
136
  if (table === "search_appearance_page_queries") {
140
137
  const query = String(keys[1] ?? "");
141
138
  const date = String(keys[2] ?? "");
142
- const query_canonical = options.normalizeQuery?.(query) ?? null;
143
139
  return {
144
140
  date,
145
141
  row: {
146
142
  searchAppearance: String(options.searchAppearance ?? ""),
147
143
  url: toPath(String(keys[0] ?? "")),
148
144
  query,
149
- query_canonical,
150
145
  date,
151
146
  clicks,
152
147
  impressions,
@@ -157,13 +152,11 @@ function transformGscRow(table, apiRow, options = {}) {
157
152
  if (table === "dates") throw new Error("`dates` rows must be built via assembleDatesRow, not transformGscRow");
158
153
  const query = String(keys[1] ?? "");
159
154
  const date = String(keys[2] ?? "");
160
- const query_canonical = options.normalizeQuery?.(query) ?? null;
161
155
  return {
162
156
  date,
163
157
  row: {
164
158
  url: toPath(String(keys[0] ?? "")),
165
159
  query,
166
- query_canonical,
167
160
  date,
168
161
  clicks,
169
162
  impressions,
@@ -1,6 +1,6 @@
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, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, pgResolverAdapter } from "../_chunks/pg-adapter.mjs";
3
+ import { PgTableKey, R2SqlResolverAdapterOptions, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter } from "../_chunks/pg-adapter.mjs";
4
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";
@@ -36,17 +36,19 @@ interface SqlFragmentsConfig<TableKey extends string> {
36
36
  */
37
37
  tableRef?: (tableKey: TableKey) => SQL;
38
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.
39
+ * Where `queryCanonical` comes from when compiling fact-table reads.
45
40
  *
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.
41
+ * - `queryDim` (default): LEFT JOIN a query dimension relation and derive
42
+ * `COALESCE(query_dim.query_canonical, fact.query)`.
43
+ * - `column`: read a `query_canonical` column from the primary relation. This
44
+ * is reserved for derived canonical rollup relations, not fact tables.
49
45
  */
46
+ queryCanonicalSource?: 'queryDim' | 'column';
47
+ /** Relation used for the query-dimension join when `queryCanonicalSource=queryDim`. */
48
+ queryDimTableRef?: () => SQL;
49
+ /** When true, also joins query_dim on site_id (SQLite/D1 dimension table). */
50
+ queryDimSiteScoped?: boolean;
51
+ /** @deprecated Canonical reads now derive from query_dim or canonical rollups. */
50
52
  canonicalFallback?: boolean;
51
53
  }
52
54
  interface SqlFragments<TableKey extends string> {
@@ -59,6 +61,9 @@ interface SqlFragments<TableKey extends string> {
59
61
  urlToPathExpr: (col: string) => string;
60
62
  colRef: (tableKey: TableKey, colName: string) => SQL;
61
63
  tableRef: (tableKey: TableKey) => SQL;
64
+ fromSql: (tableKey: TableKey, options?: {
65
+ queryCanonical?: boolean;
66
+ }) => SQL;
62
67
  dateColRef: (tableKey: TableKey) => SQL;
63
68
  siteIdColRef?: (tableKey: TableKey) => SQL;
64
69
  searchTypeColRef?: (tableKey: TableKey) => SQL;
@@ -81,9 +86,8 @@ declare function createResolverAdapter<TableKey extends string>(config: CreateRe
81
86
  /**
82
87
  * True when `plan` can be served from the canonical-grained rollup instead of
83
88
  * 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.
89
+ * dropped column or the raw row grain disqualifies the query. Callers decide
90
+ * whether that miss is a typed raw fallback or a hard coverage error.
87
91
  */
88
92
  declare function planCoveredByCanonicalRollup(plan: LogicalQueryPlan): boolean;
89
93
  /** State-level convenience: build the plan then gate. */
@@ -133,6 +137,21 @@ interface RunSQLFn {
133
137
  rows: Array<Record<string, unknown>>;
134
138
  }>;
135
139
  }
140
+ interface CanonicalQueryDimSource {
141
+ /** Object keys for the versioned query_dim parquet. */
142
+ keys: string[];
143
+ normalizerVersion: number;
144
+ intentVersion?: number;
145
+ builtAt?: number;
146
+ }
147
+ interface PrimaryColumnarSource {
148
+ /** Pre-resolved compacted/Iceberg parquet object keys for the requested fact table. */
149
+ keys: string[];
150
+ /** Oldest covered date (`YYYY-MM-DD`). Omit to assert the source covers all older dates. */
151
+ coversFrom?: string;
152
+ /** Newest covered date (`YYYY-MM-DD`). Omit to assert the source covers the current tail. */
153
+ coversThrough?: string;
154
+ }
136
155
  /**
137
156
  * Optional overlay that serves a resolver extra (e.g. canonical-variant
138
157
  * grouping, keyed `'canonicalExtras'`) from a precomputed source — typically a
@@ -157,36 +176,75 @@ interface RunOptimizedQueryOptions {
157
176
  /** Overlay tried per extra before the live SQL; absent → today's live path. */
158
177
  resolveExtra?: ResolveExtraFn;
159
178
  /**
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.
179
+ * Deprecated compatibility flag. Canonical reads now derive from query_dim,
180
+ * not a nullable fact-table `query_canonical` column.
163
181
  */
164
182
  canonicalFallback?: boolean;
183
+ /**
184
+ * Versioned query dimension backing canonical reads over fact rows. Required
185
+ * whenever a query groups or filters by `queryCanonical`; a canonical rollup
186
+ * source may carry the same metadata/key set via `canonicalSource.queryDim`.
187
+ */
188
+ queryDim?: CanonicalQueryDimSource;
189
+ /**
190
+ * Primary fact-file source for consumer reads. Callers pass compacted or
191
+ * Iceberg data-file keys with explicit coverage metadata; raw daily
192
+ * partitions are only used when `primarySourceFallback: 'raw'` is set.
193
+ */
194
+ primarySource?: PrimaryColumnarSource;
195
+ /**
196
+ * Explicit compatibility escape hatch for raw daily shards. Default is
197
+ * strict: missing/empty/stale primary coverage fails with
198
+ * `QuerySourceCoverageError`.
199
+ */
200
+ primarySourceFallback?: 'raw';
165
201
  /**
166
202
  * Opt-in canonical-primary performance (ADR-0018 Gap 2): object keys of the
167
203
  * `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
204
+ * coverable (`canonicalRollupCovers`) AND the window is within the rollup's
205
+ * coverage, the MAIN query reads these
170
206
  * 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.
207
+ * partitions. Variant extras still need fact-grain rows, so they read
208
+ * `primarySource` when present and raw partitions only via
209
+ * `primarySourceFallback: 'raw'`.
179
210
  *
180
211
  * `coversThrough` (ISO `YYYY-MM-DD`, the rollup's newest covered date) gates
181
212
  * 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).
213
+ * else strict mode fails so the recent tail is never silently undercounted.
214
+ * Omit to assert full coverage (use with care).
184
215
  */
185
216
  canonicalSource?: {
186
217
  keys: string[];
187
218
  coversThrough?: string;
219
+ queryDim?: CanonicalQueryDimSource;
220
+ };
221
+ /**
222
+ * @deprecated Canonical-source misses are recorded on `source.fallbacks`;
223
+ * raw daily fallback is controlled by `primarySourceFallback: 'raw'`.
224
+ */
225
+ canonicalSourceFallback?: 'raw';
226
+ /** Optional version gates for the query dimension backing `canonicalSource`. */
227
+ canonicalRequirements?: {
228
+ normalizerVersion?: number;
229
+ intentVersion?: number;
188
230
  };
189
231
  }
232
+ type QuerySourceKind = 'canonical-rollup' | 'primary-columnar' | 'raw-partitions';
233
+ type QuerySourceFallbackKind = 'primary-source-missing' | 'primary-source-empty' | 'primary-source-missing-coverage' | 'primary-source-stale-coverage' | 'canonical-source-missing' | 'canonical-source-empty' | 'canonical-source-not-coverable' | 'canonical-source-stale-coverage' | 'query-dim-missing' | 'query-dim-empty' | 'query-dim-stale-version';
234
+ interface QuerySourceFallback {
235
+ kind: QuerySourceFallbackKind;
236
+ message: string;
237
+ }
238
+ interface QuerySourceDecision {
239
+ kind: QuerySourceKind;
240
+ fallback?: QuerySourceFallback;
241
+ fallbacks?: QuerySourceFallback[];
242
+ }
243
+ declare class QuerySourceCoverageError extends Error {
244
+ readonly fallback: QuerySourceFallback;
245
+ name: string;
246
+ constructor(fallback: QuerySourceFallback);
247
+ }
190
248
  interface OptimizedQueryResult {
191
249
  rows: Array<Record<string, unknown>>;
192
250
  totalCount: number;
@@ -200,11 +258,14 @@ interface OptimizedQueryResult {
200
258
  key: string;
201
259
  rows: Array<Record<string, unknown>>;
202
260
  }>;
261
+ source: QuerySourceDecision;
262
+ extraSource?: QuerySourceDecision;
203
263
  }
204
264
  interface ComparisonQueryResult {
205
265
  rows: Array<Record<string, unknown>>;
206
266
  totalCount: number;
207
267
  totals: Record<string, unknown>;
268
+ source: QuerySourceDecision;
208
269
  }
209
270
  declare function runOptimizedQuery(runSQL: RunSQLFn, ctx: RunQueryCtx, state: BuilderState, dateRange: {
210
271
  startDate: string;
@@ -219,13 +280,7 @@ declare function runComparisonQuery(runSQL: RunSQLFn, ctx: RunQueryCtx, current:
219
280
  startDate: string;
220
281
  endDate: string;
221
282
  };
222
- }, filter?: ComparisonFilter, options?: {
223
- canonicalFallback?: boolean;
224
- canonicalSource?: {
225
- keys: string[];
226
- coversThrough?: string;
227
- };
228
- }): Promise<ComparisonQueryResult>;
283
+ }, filter?: ComparisonFilter, options?: RunOptimizedQueryOptions): Promise<ComparisonQueryResult>;
229
284
  /**
230
285
  * Host-supplied reader: return the materialised rollup's rows for an
231
286
  * `(id, tenant, slice)`, in the exact shape the live extra produces, or `null`
@@ -281,4 +336,4 @@ interface AssertSchemaInSyncOptions {
281
336
  mode: 'exact' | 'superset';
282
337
  }
283
338
  declare function assertSchemaInSync(options: AssertSchemaInSyncOptions): void;
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 };
339
+ export { type AssertSchemaInSyncOptions, type CanonicalQueryDimSource, 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 PrimaryColumnarSource, QuerySourceCoverageError, type QuerySourceDecision, type QuerySourceFallback, type QuerySourceFallbackKind, type QuerySourceKind, type R2SqlResolverAdapterOptions, 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, createR2SqlResolverAdapter, 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, 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 };
1
+ import { DIMENSION_SURFACES, LOGICAL_DATASETS, QuerySourceCoverageError, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, 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, QuerySourceCoverageError, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, createResolverAdapter, createRollupExtrasOverlay, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, planCoveredByCanonicalRollup, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
@@ -361,10 +361,10 @@ declare const queryCanonicalVariantsRollup: RollupDef;
361
361
  * these per-date sums over a window is exact — identical to aggregating the raw
362
362
  * rows.
363
363
  *
364
- * Null-free by construction: groups by `COALESCE(NULLIF(query_canonical, ''),
365
- * query)`, the same total-key expression the opt-in read path uses (ADR-0018
366
- * Gap 1), so the rollup never carries a NULL/'' canonical bucket and the read
367
- * path needs no fallback when pointed at it.
364
+ * Null-free by construction: groups by the versioned query dimension when it
365
+ * exists, with raw query as the fallback, so the rollup never carries a NULL/''
366
+ * canonical bucket and the read path can treat the rollup's `query_canonical`
367
+ * column as already-derived.
368
368
  *
369
369
  * Date-grained full history (`windowDays: null`): one rollup serves every date
370
370
  * range (reads filter by `date`) and both windows of a comparison. Opt-in (not
package/dist/rollups.mjs CHANGED
@@ -598,7 +598,7 @@ const queryCanonicalVariantsRollup = {
598
598
  }
599
599
  ],
600
600
  parquetSortKey: ["joinKey"],
601
- async build({ engine, ctx, searchType }) {
601
+ async build({ engine, ctx, dataSource, searchType }) {
602
602
  const parts = await engine.listPartitions({
603
603
  ctx,
604
604
  table: "queries",
@@ -607,28 +607,37 @@ const queryCanonicalVariantsRollup = {
607
607
  if (parts.length === 0) return [];
608
608
  const partitions = parts.map((p) => p.partition);
609
609
  const byCanonical = /* @__PURE__ */ new Map();
610
+ const dimStore = createQueryDimStore({ dataSource });
611
+ const useDim = await dimStore.loadMeta(ctx) !== null;
612
+ const canonExpr = useDim ? "COALESCE(qd.query_canonical, q.query)" : "q.query";
610
613
  let cursor = null;
611
614
  for (;;) {
612
- const after = cursor === null ? "" : `AND query > '${cursor.replace(/'/g, "''")}'`;
615
+ const after = cursor === null ? "" : `AND q.query > '${cursor.replace(/'/g, "''")}'`;
616
+ const fileSets = { FILES: {
617
+ table: "queries",
618
+ partitions
619
+ } };
620
+ if (useDim) fileSets.QUERY_DIM = {
621
+ table: "queries",
622
+ keys: [dimStore.parquetKey(ctx)]
623
+ };
613
624
  const { rows } = await engine.runSQL({
614
625
  ctx,
615
626
  table: "queries",
616
627
  ...searchType !== void 0 ? { searchType } : {},
617
- fileSets: { FILES: {
618
- table: "queries",
619
- partitions
620
- } },
628
+ fileSets,
621
629
  sql: `
622
630
  SELECT
623
- COALESCE(NULLIF(query_canonical, ''), query) AS joinKey,
624
- query AS query,
625
- SUM(clicks) AS clicks,
626
- SUM(impressions) AS impressions,
627
- SUM(sum_position) AS sum_pos
628
- FROM read_parquet({{FILES}}, union_by_name = true)
629
- WHERE query IS NOT NULL ${after}
630
- GROUP BY COALESCE(NULLIF(query_canonical, ''), query), query
631
- ORDER BY query
631
+ ${canonExpr} AS joinKey,
632
+ q.query AS query,
633
+ SUM(q.clicks) AS clicks,
634
+ SUM(q.impressions) AS impressions,
635
+ SUM(q.sum_position) AS sum_pos
636
+ FROM read_parquet({{FILES}}, union_by_name = true) q
637
+ ${useDim ? "LEFT JOIN read_parquet({{QUERY_DIM}}, union_by_name = true) qd ON q.query = qd.query" : ""}
638
+ WHERE q.query IS NOT NULL ${after}
639
+ GROUP BY ${canonExpr}, q.query
640
+ ORDER BY q.query
632
641
  LIMIT ${ROLLUP_PAGE_ROWS_WIDE}
633
642
  `
634
643
  });
@@ -663,6 +672,8 @@ const queryCanonicalVariantsRollup = {
663
672
  return out;
664
673
  }
665
674
  };
675
+ const CANONICAL_DAILY_ROLLUP_FINAL_ID = "query_canonical_daily";
676
+ const CANONICAL_DAILY_PART_STEM = "query_canonical_daily__part";
666
677
  const queryCanonicalDailyRollup = {
667
678
  id: "query_canonical_daily",
668
679
  windowDays: null,
@@ -698,7 +709,7 @@ const queryCanonicalDailyRollup = {
698
709
  async build({ engine, ctx, dataSource, searchType }) {
699
710
  const dimStore = createQueryDimStore({ dataSource });
700
711
  const useDim = await dimStore.loadMeta(ctx) !== null;
701
- const canonExpr = useDim ? `COALESCE(qd.query_canonical, NULLIF(q.query_canonical, ''), q.query)` : `COALESCE(NULLIF(query_canonical, ''), query)`;
712
+ const canonExpr = useDim ? `COALESCE(qd.query_canonical, q.query)` : `query`;
702
713
  return (await runWindowed({
703
714
  engine,
704
715
  ctx,
@@ -764,7 +775,7 @@ async function rebuildCanonicalDailyResumable(opts) {
764
775
  const windowsTotal = windows.length;
765
776
  const dimStore = createQueryDimStore({ dataSource });
766
777
  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)`;
778
+ const canonExpr = useDim ? `COALESCE(qd.query_canonical, q.query)` : `query`;
768
779
  const extraFileSets = useDim ? { QUERY_DIM: {
769
780
  table: "queries",
770
781
  keys: [dimStore.parquetKey(ctx)]
@@ -833,8 +844,6 @@ async function rebuildCanonicalDailyResumable(opts) {
833
844
  rowsWritten: batchRows.length
834
845
  };
835
846
  }
836
- const CANONICAL_DAILY_ROLLUP_FINAL_ID = "query_canonical_daily";
837
- const CANONICAL_DAILY_PART_STEM = "query_canonical_daily__part";
838
847
  const indexingMetadataRollup = {
839
848
  id: "indexing_metadata",
840
849
  windowDays: null,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "0.32.12",
4
+ "version": "0.33.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",
@@ -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": "0.32.12",
185
- "@gscdump/contracts": "0.32.12"
184
+ "@gscdump/contracts": "0.33.0",
185
+ "gscdump": "0.33.0"
186
186
  },
187
187
  "devDependencies": {
188
188
  "@duckdb/duckdb-wasm": "^1.32.0",