@gscdump/engine 1.0.2 → 1.0.3

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.mjs CHANGED
@@ -1,17 +1,8 @@
1
- import { engineErrorToException, engineErrors, isEngineError } from "./errors.mjs";
1
+ import { engineErrorToException, engineErrors } from "./errors.mjs";
2
2
  import { DEFAULT_SEARCH_TYPE, dayPartition, hourPartition, inferLegacyTier, inferSearchType, objectKey } from "./_chunks/layout.mjs";
3
3
  import { matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter } from "./_chunks/manifest-store-utils.mjs";
4
4
  import { coerceRow, coerceRows } from "./_chunks/coerce.mjs";
5
- import { SCHEMAS, TABLE_METADATA, allTables, countries, currentSchemaVersion, dates, dimensionToColumn, drizzleSchema, hourly_pages, inferTable, page_queries, pages, queries } from "./_chunks/schema.mjs";
6
- import { FILES_PLACEHOLDER, enumeratePartitions, resolveParquetSQL, substituteNamedFiles } from "./_chunks/parquet-plan.mjs";
7
- import { bindLiterals, formatLiteral } from "./sql-bind.mjs";
8
5
  import { MAX_DAY_BYTES, createDuckDBCodec, createDuckDBExecutor, createStorageEngine } from "./_chunks/engine.mjs";
9
- import { assembleDatesRow, createRowAccumulator, toPath, toSumPosition, transformGscRow } from "./ingest.mjs";
10
- import "./planner.mjs";
11
- import { createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter } from "./_chunks/resolver.mjs";
12
- import { rebuildDailyFromHourly } from "./rollups.mjs";
13
- import { fixedPolicy, inspectionPolicy, sitemapPolicy } from "./schedule.mjs";
14
- import { ENGINE_QUERY_CAPABILITIES, createSqlQuerySource } from "./_chunks/source.mjs";
15
6
  import { err, ok, unwrapResult } from "gscdump/result";
16
7
  const SHARD_RE = /^u_[^/]+\/manifest\/(?<siteId>[^/]+)\/(?<table>[^/]+)\/HEAD$/;
17
8
  const CAS_BACKOFF_BASE_MS = 5;
@@ -372,127 +363,6 @@ function createR2ManifestStore(opts) {
372
363
  }
373
364
  };
374
365
  }
375
- function scopeOf(ctx, table, date) {
376
- return {
377
- userId: String(ctx.userId),
378
- siteId: ctx.siteId,
379
- table,
380
- date,
381
- ...ctx.searchType !== void 0 ? { searchType: ctx.searchType } : {}
382
- };
383
- }
384
- function createIngestAccumulator(opts) {
385
- const { engine, ctx, hooks, ...accOpts } = opts;
386
- const acc = createRowAccumulator(accOpts);
387
- function reportHookFailure(hook, error) {
388
- console.warn(`[gscdump/engine] ${hook} hook failed`, error);
389
- }
390
- async function notifyWriteError(info) {
391
- try {
392
- await hooks.onWriteError(info);
393
- } catch (hookError) {
394
- reportHookFailure("onWriteError", hookError);
395
- }
396
- }
397
- async function writeOne(table, date, rows) {
398
- const scope = scopeOf(ctx, table, date);
399
- return (ctx.grain === "hour" ? engine.writeHour ?? (() => Promise.reject(/* @__PURE__ */ new Error("ingest accumulator: grain=hour requires engine.writeHour"))) : engine.writeDay)(scope, rows).then(() => engine.setSyncState(scope, "done")).then(async () => {
400
- await hooks.onWritten?.({
401
- table,
402
- date,
403
- rowCount: rows.length
404
- });
405
- return {
406
- ok: true,
407
- rows: rows.length
408
- };
409
- }).catch(async (err) => {
410
- await notifyWriteError({
411
- table,
412
- date,
413
- error: err
414
- });
415
- return { ok: false };
416
- });
417
- }
418
- async function recover(table, date) {
419
- const scope = scopeOf(ctx, table, date);
420
- try {
421
- await engine.setSyncState(scope, "failed", { error: "mid-continuation-skip" });
422
- } catch (stateError) {
423
- await notifyWriteError({
424
- table,
425
- date,
426
- error: stateError
427
- });
428
- }
429
- return hooks.onRecover(table, date).catch(async (err) => {
430
- await notifyWriteError({
431
- table,
432
- date,
433
- error: err
434
- });
435
- return false;
436
- });
437
- }
438
- return {
439
- push(table, rows) {
440
- return acc.push(table, rows);
441
- },
442
- async finalize({ hasMore }) {
443
- const overflowed = acc.overflowed;
444
- const totalRows = acc.totalRows;
445
- const buckets = acc.drain();
446
- if (overflowed || hasMore) {
447
- const tasks = [];
448
- for (const [table, byDate] of buckets) for (const date of byDate.keys()) tasks.push(recover(table, date));
449
- const results = await Promise.all(tasks).catch(async (err) => {
450
- await notifyWriteError({
451
- table: null,
452
- date: null,
453
- error: err
454
- });
455
- return [];
456
- });
457
- if (overflowed) await notifyWriteError({
458
- table: null,
459
- date: null,
460
- error: /* @__PURE__ */ new Error(`ingest accumulator overflow at ${totalRows} rows; recovering via forced re-sync`)
461
- });
462
- return {
463
- flushed: 0,
464
- recovered: results.filter(Boolean).length,
465
- failed: 0,
466
- rowsWritten: 0
467
- };
468
- }
469
- const writes = [];
470
- for (const [table, byDate] of buckets) for (const [date, rows] of byDate) writes.push(writeOne(table, date, rows));
471
- const outcomes = await Promise.all(writes);
472
- let flushed = 0;
473
- let failed = 0;
474
- let rowsWritten = 0;
475
- for (const o of outcomes) if (o.ok) {
476
- flushed++;
477
- rowsWritten += o.rows;
478
- } else failed++;
479
- if (flushed > 0 && hooks.onJobComplete) try {
480
- await hooks.onJobComplete({
481
- flushed,
482
- rowsWritten
483
- });
484
- } catch (hookError) {
485
- reportHookFailure("onJobComplete", hookError);
486
- }
487
- return {
488
- flushed,
489
- recovered: 0,
490
- failed,
491
- rowsWritten
492
- };
493
- }
494
- };
495
- }
496
366
  function createQueryProfiler(sink, now = () => Date.now()) {
497
367
  return { start(name, meta) {
498
368
  const t0 = now();
@@ -519,6 +389,105 @@ function collectSpans(now) {
519
389
  spans
520
390
  };
521
391
  }
392
+ const DAY = 24 * (3600 * 1e3);
393
+ function isDue(state, now) {
394
+ return now >= state.nextAt;
395
+ }
396
+ function sitemapCadenceMs(consecutiveUnchanged) {
397
+ if (consecutiveUnchanged >= 7) return 30 * DAY;
398
+ if (consecutiveUnchanged >= 3) return 7 * DAY;
399
+ return DAY;
400
+ }
401
+ const SITEMAP_VERSION = 1;
402
+ const sitemapPolicy = {
403
+ version: SITEMAP_VERSION,
404
+ initial(now) {
405
+ return {
406
+ nextAt: now + DAY,
407
+ consecutiveUnchanged: 0,
408
+ policyVersion: SITEMAP_VERSION
409
+ };
410
+ },
411
+ observe(prev, evt) {
412
+ if (prev.policyVersion !== SITEMAP_VERSION) return {
413
+ nextAt: evt.at + sitemapCadenceMs(0),
414
+ consecutiveUnchanged: 0,
415
+ policyVersion: SITEMAP_VERSION
416
+ };
417
+ if (evt.changed) return {
418
+ nextAt: evt.at + DAY,
419
+ consecutiveUnchanged: 0,
420
+ policyVersion: SITEMAP_VERSION
421
+ };
422
+ const next = prev.consecutiveUnchanged + 1;
423
+ return {
424
+ nextAt: evt.at + sitemapCadenceMs(next),
425
+ consecutiveUnchanged: next,
426
+ policyVersion: SITEMAP_VERSION
427
+ };
428
+ },
429
+ isDue
430
+ };
431
+ const INSPECTION_VERSION = 1;
432
+ function inspectionCadenceMs(verdict) {
433
+ if (verdict === "PASS") return 30 * DAY;
434
+ if (verdict === "FAIL") return 7 * DAY;
435
+ return 14 * DAY;
436
+ }
437
+ function inspectionPolicy(verdict) {
438
+ const cadence = inspectionCadenceMs(verdict);
439
+ return {
440
+ version: INSPECTION_VERSION,
441
+ initial(now) {
442
+ return {
443
+ nextAt: now + cadence,
444
+ consecutiveUnchanged: 0,
445
+ policyVersion: INSPECTION_VERSION
446
+ };
447
+ },
448
+ observe(prev, evt) {
449
+ if (prev.policyVersion !== INSPECTION_VERSION) return {
450
+ nextAt: evt.at + cadence,
451
+ consecutiveUnchanged: 0,
452
+ policyVersion: INSPECTION_VERSION
453
+ };
454
+ const next = evt.changed ? 0 : prev.consecutiveUnchanged + 1;
455
+ return {
456
+ nextAt: evt.at + cadence,
457
+ consecutiveUnchanged: next,
458
+ policyVersion: INSPECTION_VERSION
459
+ };
460
+ },
461
+ isDue
462
+ };
463
+ }
464
+ const FIXED_VERSION = 1;
465
+ function fixedPolicy(intervalMs) {
466
+ return {
467
+ version: FIXED_VERSION,
468
+ initial(now) {
469
+ return {
470
+ nextAt: now + intervalMs,
471
+ consecutiveUnchanged: 0,
472
+ policyVersion: FIXED_VERSION
473
+ };
474
+ },
475
+ observe(prev, evt) {
476
+ if (prev.policyVersion !== FIXED_VERSION) return {
477
+ nextAt: evt.at + intervalMs,
478
+ consecutiveUnchanged: 0,
479
+ policyVersion: FIXED_VERSION
480
+ };
481
+ const next = evt.changed ? 0 : prev.consecutiveUnchanged + 1;
482
+ return {
483
+ nextAt: evt.at + intervalMs,
484
+ consecutiveUnchanged: next,
485
+ policyVersion: FIXED_VERSION
486
+ };
487
+ },
488
+ isDue
489
+ };
490
+ }
522
491
  const KEY_SEP = "\0";
523
492
  function partitionKey(slice) {
524
493
  return [
@@ -674,4 +643,4 @@ const MIN_SYNC_IMPRESSIONS = 1;
674
643
  const MIN_COUNTRY_IMPRESSIONS = 10;
675
644
  const MAX_SITEMAP_URLS_PER_SITE = 5e4;
676
645
  const MAX_TRACKED_URLS_PER_SITE = 2e5;
677
- 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, coerceRow, coerceRows, collectSpans, countries, createDuckDBCodec, createDuckDBExecutor, createIcebergResolverAdapter, createInMemorySink, createIngestAccumulator, createParquetResolverAdapter, createQueryProfiler, createR2ManifestStore, createR2SqlResolverAdapter, createRowAccumulator, createSqlQuerySource, createStorageEngine, currentSchemaVersion, dates, dayPartition, dimensionToColumn, drizzleSchema, engineErrorToException, engineErrors, enumeratePartitions, fixedPolicy, 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 };
646
+ export { DEFAULT_SEARCH_TYPE, 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, TABLES_BY_SEARCH_TYPE, TABLE_TIERS, TIER_PRIORITY, WEIGHT_PRIORITY, coerceRow, coerceRows, collectSpans, createDuckDBCodec, createDuckDBExecutor, createInMemorySink, createQueryProfiler, createR2ManifestStore, createStorageEngine, dayPartition, fixedPolicy, getDateWeight, getTableTier, getTablesForTier, hourPartition, inferLegacyTier, inferSearchType, inspectionPolicy, objectKey, parseEnabledSearchTypes, sitemapPolicy, validateEnabledSearchTypes, validateEnabledSearchTypesResult };
@@ -0,0 +1,97 @@
1
+ import { SearchType, TenantCtx as TenantCtx$1 } from "./_chunks/storage.mjs";
2
+ import { GscApiRow, RowAccumulatorOptions } from "./ingest.mjs";
3
+ import { Grain, Row, TableName } from "@gscdump/contracts";
4
+ interface IngestAccumulatorEngine {
5
+ writeDay: (scope: TenantCtx$1 & {
6
+ table: TableName;
7
+ date: string;
8
+ searchType?: SearchType;
9
+ }, rows: Row[]) => Promise<void>;
10
+ /**
11
+ * Routed when the accumulator's `ctx.grain === 'hour'`. Same scope shape as
12
+ * `writeDay`; `date` is the PT calendar day, rows carry `hour` + `date`.
13
+ * Optional so hosts that never opt into hourly need not implement it.
14
+ */
15
+ writeHour?: (scope: TenantCtx$1 & {
16
+ table: TableName;
17
+ date: string;
18
+ searchType?: SearchType;
19
+ }, rows: Row[]) => Promise<void>;
20
+ setSyncState: (scope: TenantCtx$1 & {
21
+ table: TableName;
22
+ date: string;
23
+ searchType?: SearchType;
24
+ }, state: 'done' | 'failed', info?: {
25
+ error?: string;
26
+ }) => Promise<void>;
27
+ }
28
+ interface IngestAccumulatorCtx {
29
+ userId: string | number;
30
+ siteId: string;
31
+ searchType?: SearchType;
32
+ /**
33
+ * Temporal granularity for this accumulator. `'day'` (default) routes
34
+ * flushed buckets to `engine.writeDay`. `'hour'` routes to
35
+ * `engine.writeHour` and requires the engine implementation to be set.
36
+ */
37
+ grain?: Grain;
38
+ }
39
+ interface IngestAccumulatorHooks {
40
+ /**
41
+ * Called once per (table, date) when the job must abandon in-memory rows
42
+ * (overflow or `hasMore` continuation). Host queues a forced re-sync from
43
+ * the source. Return true iff a recovery job was actually queued.
44
+ */
45
+ onRecover: (table: TableName, date: string) => Promise<boolean>;
46
+ /**
47
+ * Called when an engine.writeDay fails or recovery itself errors. Host
48
+ * logs to its error sink (e.g. `r2_write_errors` D1 table).
49
+ */
50
+ onWriteError: (info: {
51
+ table: TableName | null;
52
+ date: string | null;
53
+ error: unknown;
54
+ }) => Promise<void>;
55
+ /**
56
+ * Called after a successful writeDay for a (table, date). Host typically
57
+ * busts the manifest cache here so the next read sees the new parquet.
58
+ */
59
+ onWritten?: (info: {
60
+ table: TableName;
61
+ date: string;
62
+ rowCount: number;
63
+ }) => void | Promise<void>;
64
+ /**
65
+ * Called once at end of `finalize`, only when at least one (table, date)
66
+ * actually landed. Host queues rollup rebuild + compaction.
67
+ */
68
+ onJobComplete?: (info: {
69
+ flushed: number;
70
+ rowsWritten: number;
71
+ }) => Promise<void>;
72
+ }
73
+ interface FinalizeOptions {
74
+ /**
75
+ * The GSC `hasMore` flag for the whole job. When true, in-memory buckets
76
+ * only reflect this job's slice; we re-queue forced single-day re-syncs
77
+ * via `onRecover` so R2 stays authoritative.
78
+ */
79
+ hasMore: boolean;
80
+ }
81
+ interface FinalizeResult {
82
+ flushed: number;
83
+ recovered: number;
84
+ failed: number;
85
+ rowsWritten: number;
86
+ }
87
+ interface IngestAccumulator {
88
+ push: (table: TableName, rows: readonly GscApiRow[]) => boolean;
89
+ finalize: (opts: FinalizeOptions) => Promise<FinalizeResult>;
90
+ }
91
+ interface CreateIngestAccumulatorOptions extends RowAccumulatorOptions {
92
+ engine: IngestAccumulatorEngine;
93
+ ctx: IngestAccumulatorCtx;
94
+ hooks: IngestAccumulatorHooks;
95
+ }
96
+ declare function createIngestAccumulator(opts: CreateIngestAccumulatorOptions): IngestAccumulator;
97
+ export { CreateIngestAccumulatorOptions, FinalizeOptions, FinalizeResult, IngestAccumulator, IngestAccumulatorCtx, IngestAccumulatorEngine, IngestAccumulatorHooks, createIngestAccumulator };
@@ -0,0 +1,123 @@
1
+ import { createRowAccumulator } from "./ingest.mjs";
2
+ function scopeOf(ctx, table, date) {
3
+ return {
4
+ userId: String(ctx.userId),
5
+ siteId: ctx.siteId,
6
+ table,
7
+ date,
8
+ ...ctx.searchType !== void 0 ? { searchType: ctx.searchType } : {}
9
+ };
10
+ }
11
+ function createIngestAccumulator(opts) {
12
+ const { engine, ctx, hooks, ...accOpts } = opts;
13
+ const acc = createRowAccumulator(accOpts);
14
+ function reportHookFailure(hook, error) {
15
+ console.warn(`[gscdump/engine] ${hook} hook failed`, error);
16
+ }
17
+ async function notifyWriteError(info) {
18
+ try {
19
+ await hooks.onWriteError(info);
20
+ } catch (hookError) {
21
+ reportHookFailure("onWriteError", hookError);
22
+ }
23
+ }
24
+ async function writeOne(table, date, rows) {
25
+ const scope = scopeOf(ctx, table, date);
26
+ return (ctx.grain === "hour" ? engine.writeHour ?? (() => Promise.reject(/* @__PURE__ */ new Error("ingest accumulator: grain=hour requires engine.writeHour"))) : engine.writeDay)(scope, rows).then(() => engine.setSyncState(scope, "done")).then(async () => {
27
+ await hooks.onWritten?.({
28
+ table,
29
+ date,
30
+ rowCount: rows.length
31
+ });
32
+ return {
33
+ ok: true,
34
+ rows: rows.length
35
+ };
36
+ }).catch(async (err) => {
37
+ await notifyWriteError({
38
+ table,
39
+ date,
40
+ error: err
41
+ });
42
+ return { ok: false };
43
+ });
44
+ }
45
+ async function recover(table, date) {
46
+ const scope = scopeOf(ctx, table, date);
47
+ try {
48
+ await engine.setSyncState(scope, "failed", { error: "mid-continuation-skip" });
49
+ } catch (stateError) {
50
+ await notifyWriteError({
51
+ table,
52
+ date,
53
+ error: stateError
54
+ });
55
+ }
56
+ return hooks.onRecover(table, date).catch(async (err) => {
57
+ await notifyWriteError({
58
+ table,
59
+ date,
60
+ error: err
61
+ });
62
+ return false;
63
+ });
64
+ }
65
+ return {
66
+ push(table, rows) {
67
+ return acc.push(table, rows);
68
+ },
69
+ async finalize({ hasMore }) {
70
+ const overflowed = acc.overflowed;
71
+ const totalRows = acc.totalRows;
72
+ const buckets = acc.drain();
73
+ if (overflowed || hasMore) {
74
+ const tasks = [];
75
+ for (const [table, byDate] of buckets) for (const date of byDate.keys()) tasks.push(recover(table, date));
76
+ const results = await Promise.all(tasks).catch(async (err) => {
77
+ await notifyWriteError({
78
+ table: null,
79
+ date: null,
80
+ error: err
81
+ });
82
+ return [];
83
+ });
84
+ if (overflowed) await notifyWriteError({
85
+ table: null,
86
+ date: null,
87
+ error: /* @__PURE__ */ new Error(`ingest accumulator overflow at ${totalRows} rows; recovering via forced re-sync`)
88
+ });
89
+ return {
90
+ flushed: 0,
91
+ recovered: results.filter(Boolean).length,
92
+ failed: 0,
93
+ rowsWritten: 0
94
+ };
95
+ }
96
+ const writes = [];
97
+ for (const [table, byDate] of buckets) for (const [date, rows] of byDate) writes.push(writeOne(table, date, rows));
98
+ const outcomes = await Promise.all(writes);
99
+ let flushed = 0;
100
+ let failed = 0;
101
+ let rowsWritten = 0;
102
+ for (const o of outcomes) if (o.ok) {
103
+ flushed++;
104
+ rowsWritten += o.rows;
105
+ } else failed++;
106
+ if (flushed > 0 && hooks.onJobComplete) try {
107
+ await hooks.onJobComplete({
108
+ flushed,
109
+ rowsWritten
110
+ });
111
+ } catch (hookError) {
112
+ reportHookFailure("onJobComplete", hookError);
113
+ }
114
+ return {
115
+ flushed,
116
+ recovered: 0,
117
+ failed,
118
+ rowsWritten
119
+ };
120
+ }
121
+ };
122
+ }
123
+ export { createIngestAccumulator };
@@ -1,2 +1,2 @@
1
- import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, padTimeseries, periodOf, resolveWindow } from "../_chunks/index2.mjs";
1
+ import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, padTimeseries, periodOf, resolveWindow } from "../_chunks/index.mjs";
2
2
  export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, padTimeseries, periodOf, resolveWindow };
@@ -1,3 +1,15 @@
1
- import { enumeratePartitions } from "./_chunks/storage.mjs";
2
- import { FILES_PLACEHOLDER, ResolvedQuery, compileLogicalQueryPlan, resolveParquetSQL, substituteNamedFiles } from "./_chunks/planner.mjs";
1
+ import { TableName, enumeratePartitions } from "./_chunks/storage.mjs";
2
+ import { LogicalQueryPlan } from "gscdump/query/plan";
3
+ import { BuilderState } from "gscdump/query";
4
+ interface ResolvedQuery {
5
+ sql: string;
6
+ params: unknown[];
7
+ partitions: string[];
8
+ table: TableName;
9
+ filesPlaceholder: string;
10
+ }
11
+ declare const FILES_PLACEHOLDER = "{{FILES}}";
12
+ declare function compileLogicalQueryPlan(plan: LogicalQueryPlan, table?: TableName): ResolvedQuery;
13
+ declare function resolveParquetSQL(state: BuilderState, table?: TableName): ResolvedQuery;
14
+ declare function substituteNamedFiles(sql: string, sets: Record<string, string[]>): string;
3
15
  export { FILES_PLACEHOLDER, type ResolvedQuery, compileLogicalQueryPlan, enumeratePartitions, resolveParquetSQL, substituteNamedFiles };
@@ -1,5 +1,5 @@
1
1
  import { AnalysisParams, AnalysisResult } from "../_chunks/analysis-types.mjs";
2
- import { ComparisonMode, ResolvedWindow, WindowPreset } from "../_chunks/index2.mjs";
2
+ import { ComparisonMode, ResolvedWindow, WindowPreset } from "../_chunks/index.mjs";
3
3
  /** Status vocabulary mirrors `ActionPrioritySourceStatus`. */
4
4
  type ReportStepStatus = 'pending' | 'running' | 'done' | 'skipped' | 'error';
5
5
  type ReportSeverity = 'info' | 'low' | 'medium' | 'high';
@@ -1,6 +1,5 @@
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, R2SqlResolverAdapterOptions, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter } from "../_chunks/pg-adapter.mjs";
4
3
  import { LogicalDataset, LogicalDataset as LogicalDataset$1, PlannerCapabilities, UnresolvableDatasetError, inferDataset as inferLogicalDataset, isDatasetResolvable } from "gscdump/query/plan";
5
4
  import { SQL } from "drizzle-orm";
6
5
  import { BuilderState, Dimension, FilterInput, InternalFilter, Metric } from "gscdump/query";
@@ -304,6 +303,59 @@ declare function dimensionValue(row: Record<string, unknown>, dimension: string)
304
303
  declare function matchesDimensionFilter(row: Record<string, unknown>, filter: InternalFilter): boolean;
305
304
  declare function matchesMetricFilter(row: Record<string, unknown>, filter: InternalFilter): boolean;
306
305
  declare function matchesTopLevelPage(row: Record<string, unknown>): boolean;
306
+ type PgTableKey = TableName;
307
+ declare const pgResolverAdapter: ResolverAdapter<PgTableKey>;
308
+ /**
309
+ * Parquet-aware variant of {@link pgResolverAdapter}. Identical SQL output
310
+ * except FROM clauses emit `read_parquet({{FILES}}, union_by_name = true) AS
311
+ * "${tk}"`. The runSQL pipeline substitutes `{{FILES}}` with R2 object keys
312
+ * resolved from the manifest. The `AS "${tk}"` alias is mandatory — drizzle
313
+ * compiles `colRef` to table-qualified `"pages"."url"`, which would not
314
+ * resolve against an unaliased FROM.
315
+ *
316
+ * Single-use: build a fresh adapter per query. Cheap (no I/O) and avoids
317
+ * accidental adapter caching that would lock in a stale `{{FILES}}` set.
318
+ */
319
+ interface ResolverAdapterOptions {
320
+ /**
321
+ * `queryDim` reads canonical from a joined query dimension. `column` is only
322
+ * for derived canonical rollup relations whose primary relation already
323
+ * carries a null-free `query_canonical` output column.
324
+ */
325
+ queryCanonicalSource?: 'queryDim' | 'column';
326
+ }
327
+ interface R2SqlResolverAdapterOptions extends ResolverAdapterOptions {
328
+ /**
329
+ * R2 SQL string partition equality can undercount on identity partitions;
330
+ * string-encoded catalogs use CONCAT(col, '') in partition predicates. Int
331
+ * catalogs do not need the workaround and keep bare equality for pruning.
332
+ */
333
+ partitionKeyEncoding?: 'string' | 'int';
334
+ }
335
+ declare function createParquetResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
336
+ /**
337
+ * Multi-tenant pg-flavored adapter for the Iceberg / R2 SQL read path.
338
+ * Identical SQL output to `pgResolverAdapter` except WHERE clauses inject
339
+ * `site_id = ?` AND `search_type = ?` automatically when those scopes are
340
+ * passed to `resolveToSQL`. Required for the Iceberg fact tables which are
341
+ * shared across tenants — querying without these predicates would leak
342
+ * cross-tenant data. Single-use: the adapter has no `tableRef` override,
343
+ * so callers must rewrite bare table names to their qualified form (e.g.
344
+ * `${namespace}.pages`) before sending to R2 SQL.
345
+ */
346
+ declare function createIcebergResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
347
+ /**
348
+ * R2 SQL adapter for the Iceberg fact tables.
349
+ *
350
+ * It shares the multi-tenant Iceberg schema with `createIcebergResolverAdapter`
351
+ * but models R2 SQL's narrower execution surface: no window-total plans and no
352
+ * comparison joins. Int-partition catalogs are the default and emit bare
353
+ * equality predicates for pruning. Legacy string-partition catalogs must pass
354
+ * `partitionKeyEncoding: 'string'` to emit `CONCAT(partition_col, '') = ?`,
355
+ * working around R2 SQL's partition-string equality undercount while preserving
356
+ * bound params.
357
+ */
358
+ declare function createR2SqlResolverAdapter(options?: R2SqlResolverAdapterOptions): ResolverAdapter<PgTableKey>;
307
359
  interface AssertSchemaInSyncOptions {
308
360
  /** Label used in the thrown error (e.g. 'browser', 'sqlite'). */
309
361
  label: string;
package/dist/rollups.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  import { engineErrors } from "./errors.mjs";
2
2
  import "./_chunks/layout.mjs";
3
+ import { inspectionParquetKey, sitemapUrlsIndexPrefix } from "./entity-keys.mjs";
3
4
  import { encodeRowsToParquetFlex } from "./adapters/hyparquet.mjs";
4
- import { createIndexingMetadataStore, createQueryDimStore, createSitemapStore, inspectionParquetKey, sitemapUrlsIndexPrefix } from "./_chunks/entities.mjs";
5
+ import { createIndexingMetadataStore, createQueryDimStore, createSitemapStore } from "./_chunks/entities.mjs";
5
6
  import { MS_PER_DAY } from "gscdump/dates";
6
- import { encodeJsonBigintSafe } from "@gscdump/lakehouse";
7
+ import { encodeJsonBigintSafe } from "@gscdump/lakehouse/bigint";
7
8
  function rollupPrefix(ctx, searchType) {
8
9
  const base = ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/rollups` : `u_${ctx.userId}/rollups`;
9
10
  return searchType !== void 0 && searchType !== "web" ? `${base}/${searchType}` : base;