@gscdump/engine 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -23,15 +23,17 @@ Optional peers (install only what your runtime needs):
23
23
 
24
24
  | Subpath | Purpose |
25
25
  |---|---|
26
- | `@gscdump/engine` | Barrel: `createStorageEngine`, codec/executor factories, all storage contracts. |
26
+ | `@gscdump/engine` | Storage runtime: `createStorageEngine`, codec/executor factories, storage contracts. |
27
27
  | `@gscdump/engine/contracts` | `StorageEngine`, `Row`, `TableName`, `WriteCtx`, `ManifestEntry`, ... |
28
28
  | `@gscdump/engine/schema` | `SCHEMAS`, `allTables`, `inferTable`, column metadata. |
29
29
  | `@gscdump/engine/planner` | `resolveToSQL`, `enumeratePartitions`, partition planning. |
30
30
  | `@gscdump/engine/ingest` | GSC row → storage row helpers (`createRowAccumulator`, `transformGscRow`). |
31
+ | `@gscdump/engine/ingest-accumulator` | Stateful ingest flushing behind injected storage and recovery hooks. |
31
32
  | `@gscdump/engine/sql` | SQL literal binding helpers (`bindLiterals`, `formatLiteral`). |
32
33
  | `@gscdump/engine/sql-fragments` | Reusable SQL fragments shared across analyzers. |
33
34
  | `@gscdump/engine/rollups` | Pre-aggregated rollup contracts + helpers. |
34
- | `@gscdump/engine/entities` | Entity helpers (sites, tenants, scope keys). |
35
+ | `@gscdump/engine/entities` | Parquet-backed entity stores and their record contracts. |
36
+ | `@gscdump/engine/entity-keys` | Pure entity object-key and URL-hash helpers, with no Parquet runtime. |
35
37
  | `@gscdump/engine/resolver` | Dialect-neutral SQL composition: `ResolverAdapter`, `pgResolverAdapter`, `resolveToSQL`. |
36
38
  | `@gscdump/engine/scope` | Multi-tenant scope predicates. |
37
39
  | `@gscdump/engine/arrow` | Apache Arrow utilities for engine result conversion. |
@@ -1,4 +1,4 @@
1
- import { coerceBigIntToNumber } from "@gscdump/lakehouse";
1
+ import { coerceBigIntToNumber } from "@gscdump/lakehouse/bigint";
2
2
  function coerceRow(row) {
3
3
  let mutated = null;
4
4
  for (const k in row) {
@@ -4,7 +4,7 @@ import { SCHEMAS, TABLE_METADATA, currentSchemaVersion, dateColumnsFor, dedupeBy
4
4
  import { compactTieredImpl, compileLogicalQueryPlan, dedupeOverlappingTiers, splitOverlappingTiers, substituteNamedFiles } from "./parquet-plan.mjs";
5
5
  import { dateReplaceClause as dateReplaceClause$1 } from "../sql-fragments.mjs";
6
6
  import { sqlEscape } from "../sql-bind.mjs";
7
- import { encodeJsonBigintSafe } from "@gscdump/lakehouse";
7
+ import { encodeJsonBigintSafe } from "@gscdump/lakehouse/bigint";
8
8
  import { buildLogicalPlan } from "gscdump/query/plan";
9
9
  import { normalizeUrl } from "gscdump/normalize";
10
10
  const DEFAULT_BUFFER_READ_CONCURRENCY = 16;
@@ -1,5 +1,6 @@
1
+ import { emptyTypesKey, hashSortedUrlList, hashUrl, hashUrlList, indexingMetadataIndexKey, inspectionBaseKey, inspectionEventKey, inspectionEventsPrefix, inspectionHistoryPrefix, inspectionHistoryShardKey, inspectionParquetKey, queryDimMetaKey, queryDimParquetKey, sitemapHistoryKey, sitemapIndexKey, sitemapUrlsDeltaKey, sitemapUrlsIndexKey, sitemapUrlsIndexPrefix, sitemapUrlsPrefix } from "../entity-keys.mjs";
1
2
  import { decodeParquetToRows, encodeRowsToParquetFlex } from "../adapters/hyparquet.mjs";
2
- import { encodeJsonBigintSafe } from "@gscdump/lakehouse";
3
+ import { encodeJsonBigintSafe } from "@gscdump/lakehouse/bigint";
3
4
  function isMissingKeyError(e) {
4
5
  if (typeof e !== "object" || e === null) return false;
5
6
  if (e.code === "ENOENT") return true;
@@ -41,15 +42,6 @@ const QUERY_DIM_COLUMNS = [
41
42
  nullable: false
42
43
  }
43
44
  ];
44
- function queryDimPrefix(ctx) {
45
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/query_dim` : `u_${ctx.userId}/entities/query_dim`;
46
- }
47
- function queryDimParquetKey(ctx) {
48
- return `${queryDimPrefix(ctx)}/index.parquet`;
49
- }
50
- function queryDimMetaKey(ctx) {
51
- return `${queryDimPrefix(ctx)}/index.json`;
52
- }
53
45
  function buildQueryDimRecords(queries, deps) {
54
46
  const seen = /* @__PURE__ */ new Set();
55
47
  const out = [];
@@ -125,45 +117,7 @@ async function mapEntityIo(items, fn) {
125
117
  await Promise.all(Array.from({ length: Math.min(ENTITY_IO_CONCURRENCY, items.length) }, worker));
126
118
  return results;
127
119
  }
128
- function inspectionIndexKey(ctx) {
129
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/inspections/index.json` : `u_${ctx.userId}/entities/inspections/index.json`;
130
- }
131
- function emptyTypesKey(ctx) {
132
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/empty-types.json` : `u_${ctx.userId}/entities/empty-types.json`;
133
- }
134
- function inspectionParquetKey(ctx) {
135
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/inspections/index.parquet` : `u_${ctx.userId}/entities/inspections/index.parquet`;
136
- }
137
- function inspectionEventsPrefix(ctx) {
138
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/inspections/events` : `u_${ctx.userId}/entities/inspections/events`;
139
- }
140
- function inspectionEventKey(ctx, yearMonth, batchId) {
141
- return `${inspectionEventsPrefix(ctx)}/${yearMonth}/${batchId}.parquet`;
142
- }
143
- function inspectionBaseKey(ctx) {
144
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/inspections/base.parquet` : `u_${ctx.userId}/entities/inspections/base.parquet`;
145
- }
146
120
  const INSPECTION_EVENT_KEY_RE = /\/inspections\/events\/\d{4}-\d{2}\/[^/]+\.parquet$/;
147
- function inspectionHistoryPrefix(ctx, yearMonth) {
148
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/inspections/history/${yearMonth}` : `u_${ctx.userId}/entities/inspections/history/${yearMonth}`;
149
- }
150
- function inspectionHistoryShardKey(ctx, yearMonth, batchId) {
151
- return `${inspectionHistoryPrefix(ctx, yearMonth)}/${batchId}.json`;
152
- }
153
- function hashUrl(url) {
154
- let hi = 2166136261;
155
- let lo = 3421674724;
156
- for (let i = 0; i < url.length; i++) {
157
- const c = url.charCodeAt(i);
158
- lo ^= c;
159
- const loMul = Math.imul(lo, 435) >>> 0;
160
- const carry = Math.floor(lo * 435 / 4294967296);
161
- const hiMul = Math.imul(hi, 435) + Math.imul(lo, 1) + carry >>> 0;
162
- lo = loMul;
163
- hi = hiMul;
164
- }
165
- return (hi >>> 0).toString(16).padStart(8, "0") + (lo >>> 0).toString(16).padStart(8, "0");
166
- }
167
121
  const INSPECTION_HISTORY_MAX_BYTES = 5 * 1024 * 1024;
168
122
  const INSPECTION_PARQUET_COLUMNS = [
169
123
  {
@@ -459,24 +413,6 @@ function createInspectionStore(opts) {
459
413
  }
460
414
  };
461
415
  }
462
- function sitemapIndexKey(ctx) {
463
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/sitemaps/index.json` : `u_${ctx.userId}/entities/sitemaps/index.json`;
464
- }
465
- function sitemapHistoryKey(ctx, feedpathHash, capturedAtMs) {
466
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/sitemaps/history/${feedpathHash}__${capturedAtMs}.json` : `u_${ctx.userId}/entities/sitemaps/history/${feedpathHash}__${capturedAtMs}.json`;
467
- }
468
- function sitemapUrlsPrefix(ctx) {
469
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/sitemaps/urls` : `u_${ctx.userId}/entities/sitemaps/urls`;
470
- }
471
- function sitemapUrlsIndexPrefix(ctx) {
472
- return `${sitemapUrlsPrefix(ctx)}/by-feed`;
473
- }
474
- function sitemapUrlsIndexKey(ctx, feedpathHash) {
475
- return `${sitemapUrlsIndexPrefix(ctx)}/${feedpathHash}/index.parquet`;
476
- }
477
- function sitemapUrlsDeltaKey(ctx, feedpathHash, date) {
478
- return `${sitemapUrlsPrefix(ctx)}/deltas/${date}__${feedpathHash}.parquet`;
479
- }
480
416
  const SITEMAP_URLS_DELTA_PREFIX_RE = /\/urls\/deltas\/(\d{4}-\d{2}-\d{2})__([0-9a-f]+)\.parquet$/;
481
417
  const URLS_INDEX_COLUMNS = [
482
418
  {
@@ -584,27 +520,6 @@ function urlRecordToRow(r) {
584
520
  function isoDate(ms) {
585
521
  return new Date(ms).toISOString().slice(0, 10);
586
522
  }
587
- function hashUrlList(urls) {
588
- return hashSortedUrlList(urls.map((u) => u.loc).sort());
589
- }
590
- function hashSortedUrlList(locs) {
591
- let hi = 2166136261;
592
- let lo = 3421674724;
593
- for (let locIndex = 0; locIndex < locs.length; locIndex++) {
594
- const loc = locs[locIndex];
595
- const length = loc.length + (locIndex < locs.length - 1 ? 1 : 0);
596
- for (let i = 0; i < length; i++) {
597
- const c = i < loc.length ? loc.charCodeAt(i) : 10;
598
- lo ^= c;
599
- const loMul = Math.imul(lo, 435) >>> 0;
600
- const carry = Math.floor(lo * 435 / 4294967296);
601
- const hiMul = Math.imul(hi, 435) + Math.imul(lo, 1) + carry >>> 0;
602
- lo = loMul;
603
- hi = hiMul;
604
- }
605
- }
606
- return (hi >>> 0).toString(16).padStart(8, "0") + (lo >>> 0).toString(16).padStart(8, "0");
607
- }
608
523
  function createSitemapStore(opts) {
609
524
  const ds = opts.dataSource;
610
525
  const hash = opts.hash ?? hashUrl;
@@ -968,9 +883,6 @@ function createSitemapStore(opts) {
968
883
  }
969
884
  };
970
885
  }
971
- function indexingMetadataIndexKey(ctx) {
972
- return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/indexing/index.json` : `u_${ctx.userId}/entities/indexing/index.json`;
973
- }
974
886
  function createIndexingMetadataStore(opts) {
975
887
  const ds = opts.dataSource;
976
888
  const hash = opts.hash ?? hashUrl;
@@ -1052,4 +964,4 @@ function createEmptyTypesStore(opts) {
1052
964
  }
1053
965
  };
1054
966
  }
1055
- export { INSPECTION_HISTORY_MAX_BYTES, buildQueryDimRecords, createEmptyTypesStore, createIndexingMetadataStore, createInspectionStore, createQueryDimStore, createSitemapStore, emptyTypesKey, hashUrl, hashUrlList, indexingMetadataIndexKey, inspectionBaseKey, inspectionEventKey, inspectionEventsPrefix, inspectionHistoryPrefix, inspectionHistoryShardKey, inspectionIndexKey, inspectionParquetKey, queryDimMetaKey, queryDimParquetKey, sitemapHistoryKey, sitemapIndexKey, sitemapUrlsDeltaKey, sitemapUrlsIndexKey, sitemapUrlsIndexPrefix };
967
+ export { INSPECTION_HISTORY_MAX_BYTES, buildQueryDimRecords, createEmptyTypesStore, createIndexingMetadataStore, createInspectionStore, createQueryDimStore, createSitemapStore };
@@ -1,104 +1,53 @@
1
- import { Row, SearchType as SearchType$1, StorageEngine, TenantCtx } from "./storage.mjs";
2
- import { AnalysisParams, AnalysisResult } from "./analysis-types.mjs";
3
- import { ResolverAdapter } from "./types.mjs";
4
- import { AnalysisQuerySource, AnalysisSourceKind, AnalyzerRegistry, QueryRow, SourceCapabilities } from "./registry.mjs";
5
- import { EngineError } from "./errors.mjs";
6
- import "./contracts.mjs";
7
- import { PlannerCapabilities } from "gscdump/query/plan";
8
- import { BuilderState } from "gscdump/query";
9
- interface AttachedTableRunner {
10
- /**
11
- * Run a query with positional (`?`) bound parameters. Return objects keyed
12
- * by column name. BIGINT → number coercion is applied by the source factory
13
- * (see `coerceRows`); runners only need to handle DATE → ISO string (or
14
- * let the analyzer reducer normalize via `num(v)`/`str(v)`).
15
- */
16
- query: (sql: string, params?: unknown[], signal?: AbortSignal) => Promise<Row[]>;
1
+ import { AnalysisParams } from "./analysis-types.mjs";
2
+ type WindowPreset = 'last-7d' | 'last-28d' | 'last-30d' | 'last-90d' | 'last-180d' | 'last-365d' | 'mtd' | 'ytd' | 'custom';
3
+ type ComparisonMode = 'none' | 'prev-period' | 'yoy';
4
+ interface ResolveWindowOptions {
5
+ preset: WindowPreset;
6
+ comparison?: ComparisonMode;
7
+ anchor?: string;
8
+ start?: string;
9
+ end?: string;
17
10
  }
18
- interface AttachedTableSourceOptions {
19
- /** Schema name the exported DuckDB file was attached under — e.g. `gsc`. */
20
- schema: string;
21
- /**
22
- * Abort in-flight queries when the caller no longer cares about the
23
- * result. Every `runner.query` call receives the same signal.
24
- */
25
- signal?: AbortSignal;
26
- /**
27
- * List of table names actually attached to this connection. When provided,
28
- * `executeSql` short-circuits with a specific "table not attached" error
29
- * if the SQL plan references a table that isn't in this list — letting
30
- * callers (e.g. the analytics layer) route to cloud fallback without
31
- * paying the SQL execution cost. Omit to disable the check.
32
- */
33
- attachedTables?: readonly string[];
34
- /**
35
- * Dialect adapter surfaced on the source for analyzers that compose SQL
36
- * from a `BuilderState` at plan-build time (e.g. `data-query`,
37
- * `data-detail`). Attached-table sources execute pg-flavored DuckDB SQL,
38
- * so callers should pass `pgResolverAdapter` here.
39
- */
40
- adapter?: ResolverAdapter<any>;
11
+ interface ResolvedWindow {
12
+ start: string;
13
+ end: string;
14
+ days: number;
15
+ comparison?: {
16
+ start: string;
17
+ end: string;
18
+ };
41
19
  }
42
- declare class AttachedTableMissingError extends Error {
43
- readonly missing: readonly string[];
44
- readonly engineError: EngineError;
45
- constructor(missing: readonly string[]);
20
+ interface AnalysisPeriod {
21
+ startDate: string;
22
+ endDate: string;
46
23
  }
47
- declare function createAttachedTableSource(runner: AttachedTableRunner, options: AttachedTableSourceOptions): AnalysisQuerySource;
48
- interface CreateSqlQuerySourceOptions<TKey extends string> {
49
- /** Debug-only identifier surfaced on the source for error messages. */
50
- name: string;
51
- /** Telemetry tag stamped onto analyzer result meta. */
52
- kind?: AnalysisSourceKind;
53
- /** Dialect-specific adapter; compiles `BuilderState` → `{ sql, params }`. */
54
- adapter: ResolverAdapter<TKey>;
55
- /** Drives the underlying DB. Called for both typed queries and raw SQL. */
56
- execute: (sql: string, params: unknown[]) => Promise<QueryRow[]>;
57
- /** Tenant id for multi-tenant dialects; forwarded to `resolveToSQL`. */
58
- siteId?: string | number;
59
- /**
60
- * Search-type scope for multi-tenant dialects; forwarded to `resolveToSQL`.
61
- * `number` = int-encoded code (`SEARCH_TYPE_INT`) for INT `search_type`
62
- * catalogs (bound bare so the int partition prunes); `string` otherwise.
63
- */
64
- searchType?: string | number;
65
- /** Additional capability flags merged on top of `adapter.capabilities`. */
66
- extraCapabilities?: Partial<SourceCapabilities>;
24
+ interface ComparisonPeriod {
25
+ current: AnalysisPeriod;
26
+ previous: AnalysisPeriod;
67
27
  }
68
- declare function createSqlQuerySource<TKey extends string>(options: CreateSqlQuerySourceOptions<TKey>): AnalysisQuerySource;
69
- /**
70
- * Capabilities the engine query path honors. Matches what the DuckDB compiler
71
- * passes to `buildLogicalPlan`: regex pushes down; comparison joins and
72
- * multi-dataset queries belong to the analyzer dispatcher, not the engine's
73
- * builder-state query path.
74
- */
75
- declare const ENGINE_QUERY_CAPABILITIES: PlannerCapabilities;
76
- interface EngineQuerySourceOptions {
77
- engine: StorageEngine;
78
- ctx: TenantCtx;
28
+ declare function defaultEndDate(): string;
29
+ declare function periodOf(params: AnalysisParams): AnalysisPeriod;
30
+ declare function comparisonOf(params: AnalysisParams): ComparisonPeriod;
31
+ declare function resolveWindow(opts: ResolveWindowOptions): ResolvedWindow;
32
+ interface PadTimeseriesOptions<T> {
33
+ /** ISO date (YYYY-MM-DD), inclusive lower bound. */
34
+ startDate: string;
35
+ /** ISO date (YYYY-MM-DD), inclusive upper bound. */
36
+ endDate: string;
79
37
  /**
80
- * Restrict every manifest lookup the source performs to a single search-type
81
- * slice. Threads into `engine.query` and `engine.runSQL` so the wrapped
82
- * source returns rows from one cohort instead of unioning web + non-web
83
- * parquet. Undefined preserves legacy cross-type behaviour for web-only
84
- * tenants and admin paths.
38
+ * Row to insert for missing dates. Defaults to `{ clicks: 0, impressions: 0, ctr: 0, position: 0 }`.
39
+ * The `date` field is set automatically.
85
40
  */
86
- searchType?: SearchType$1;
41
+ fill?: Omit<T, 'date'>;
42
+ /** Row-field that carries the ISO date. Defaults to `date`. */
43
+ dateKey?: string;
87
44
  }
45
+ type DateRowShape = Record<string, unknown> & {
46
+ date?: unknown;
47
+ };
88
48
  /**
89
- * Wraps a storage engine as an `AnalysisQuerySource` with SQL execution.
90
- * `queryRows` runs typed builder-state queries; `executeSql` delegates to
91
- * `engine.runSQL` and requires `opts.fileSets` (with a `FILES` entry so the
92
- * target table can be resolved for partition lookup).
93
- */
94
- declare function createEngineQuerySource(options: EngineQuerySourceOptions): AnalysisQuerySource;
95
- /**
96
- * Convenience: wrap a storage engine + tenant ctx in a source and dispatch.
97
- * Equivalent to wrapping `createEngineQuerySource`, with omitted searchType
98
- * defaulted to web at this public helper boundary.
49
+ * Pad rows so every calendar day in `[startDate, endDate]` appears at least
50
+ * once. Existing dates keep all their rows (grouped timeseries safe).
99
51
  */
100
- declare function runAnalyzerWithEngine(deps: {
101
- engine: StorageEngine;
102
- }, ctx: TenantCtx, params: AnalysisParams, registry: AnalyzerRegistry): Promise<AnalysisResult>;
103
- declare function queryRows<TRow = QueryRow>(source: AnalysisQuerySource, state: BuilderState): Promise<TRow[]>;
104
- export { AttachedTableMissingError, AttachedTableRunner, AttachedTableSourceOptions, CreateSqlQuerySourceOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, createAttachedTableSource, createEngineQuerySource, createSqlQuerySource, queryRows, runAnalyzerWithEngine };
52
+ declare function padTimeseries<T extends DateRowShape = DateRowShape>(rows: readonly T[], options: PadTimeseriesOptions<T>): T[];
53
+ export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, padTimeseries, periodOf, resolveWindow };
@@ -2,10 +2,10 @@ import { SCHEMAS, drizzleSchema } from "./schema.mjs";
2
2
  import { enumeratePartitions } from "./parquet-plan.mjs";
3
3
  import { escapeLike } from "../sql-fragments.mjs";
4
4
  import "../planner.mjs";
5
- import { DEFAULT_PARTITION_KEY_ENCODING } from "./schema2.mjs";
6
5
  import { UnresolvableDatasetError, buildLogicalComparisonPlan, buildLogicalPlan, inferDataset as inferLogicalDataset, isDatasetResolvable } from "gscdump/query/plan";
7
6
  import { PgDialect, pgTable, varchar } from "drizzle-orm/pg-core";
8
7
  import { normalizeUrl } from "gscdump/normalize";
8
+ import { DEFAULT_PARTITION_KEY_ENCODING } from "@gscdump/lakehouse/schema";
9
9
  import { sql } from "drizzle-orm";
10
10
  import { and as and$1, gte as gte$1, impressions } from "gscdump/query";
11
11
  const DIMENSION_SURFACES = {
@@ -392,110 +392,6 @@ function createResolverAdapter(config) {
392
392
  compile: config.compile
393
393
  };
394
394
  }
395
- const pgDialect = new PgDialect();
396
- function withTenantCols(tableName, baseTable) {
397
- const t = pgTable(tableName, {
398
- site_id: varchar("site_id").notNull(),
399
- search_type: varchar("search_type").notNull()
400
- });
401
- return {
402
- ...baseTable,
403
- site_id: t.site_id,
404
- search_type: t.search_type
405
- };
406
- }
407
- const icebergSchema = {
408
- pages: withTenantCols("pages", drizzleSchema.pages),
409
- queries: withTenantCols("queries", drizzleSchema.queries),
410
- countries: withTenantCols("countries", drizzleSchema.countries),
411
- page_queries: withTenantCols("page_queries", drizzleSchema.page_queries),
412
- dates: withTenantCols("dates", drizzleSchema.dates),
413
- search_appearance: withTenantCols("search_appearance", drizzleSchema.search_appearance),
414
- search_appearance_pages: withTenantCols("search_appearance_pages", drizzleSchema.search_appearance_pages),
415
- search_appearance_queries: withTenantCols("search_appearance_queries", drizzleSchema.search_appearance_queries),
416
- search_appearance_page_queries: withTenantCols("search_appearance_page_queries", drizzleSchema.search_appearance_page_queries),
417
- hourly_pages: withTenantCols("hourly_pages", drizzleSchema.hourly_pages)
418
- };
419
- function compilePg(query) {
420
- const compiled = pgDialect.sqlToQuery(query);
421
- return {
422
- sql: compiled.sql,
423
- params: compiled.params
424
- };
425
- }
426
- const PG_BASE_CONFIG = {
427
- schema: drizzleSchema,
428
- datasetToTableKey: {
429
- pages: "pages",
430
- queries: "queries",
431
- page_queries: "page_queries",
432
- countries: "countries",
433
- dates: "dates",
434
- search_appearance: "search_appearance",
435
- search_appearance_pages: "search_appearance_pages",
436
- search_appearance_queries: "search_appearance_queries",
437
- search_appearance_page_queries: "search_appearance_page_queries",
438
- hourly_pages: "hourly_pages"
439
- },
440
- metricCast: "DOUBLE",
441
- regexPredicate: (expr, pattern, negate) => negate ? sql`NOT regexp_matches(${expr}, ${pattern})` : sql`regexp_matches(${expr}, ${pattern})`,
442
- urlToPathExpr: (col) => `CASE WHEN ${col} LIKE 'http%' THEN COALESCE(NULLIF(regexp_replace(${col}, '^https?://[^/]+', ''), ''), '/') ELSE ${col} END`,
443
- includeSiteId: false,
444
- compile: compilePg,
445
- capabilities: {
446
- regex: true,
447
- comparisonJoin: true,
448
- windowTotals: true
449
- }
450
- };
451
- const pgResolverAdapter = createResolverAdapter({
452
- ...PG_BASE_CONFIG,
453
- tableLabel: "pg-resolver-adapter"
454
- });
455
- function createParquetResolverAdapter(options = {}) {
456
- return createResolverAdapter({
457
- ...PG_BASE_CONFIG,
458
- tableLabel: "parquet-resolver-adapter",
459
- queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
460
- tableRef: (tk) => sql.raw(`read_parquet({{FILES}}, union_by_name = true) AS "${tk}"`),
461
- queryDimTableRef: () => sql.raw("read_parquet({{QUERY_DIM}}, union_by_name = true) AS \"query_dim\"")
462
- });
463
- }
464
- function createIcebergResolverAdapter(options = {}) {
465
- return createResolverAdapter({
466
- ...PG_BASE_CONFIG,
467
- schema: icebergSchema,
468
- includeSiteId: true,
469
- includeSearchType: true,
470
- tableLabel: "iceberg-resolver-adapter",
471
- queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
472
- tableRef: (tk) => sql.raw(`"${tk}"`),
473
- queryDimTableRef: () => sql.raw("\"query_dim\"")
474
- });
475
- }
476
- function createR2SqlResolverAdapter(options = {}) {
477
- const adapter = createResolverAdapter({
478
- ...PG_BASE_CONFIG,
479
- schema: icebergSchema,
480
- includeSiteId: true,
481
- includeSearchType: true,
482
- tableLabel: "r2-sql-resolver-adapter",
483
- queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
484
- capabilities: {
485
- regex: false,
486
- comparisonJoin: false,
487
- windowTotals: false
488
- },
489
- tableRef: (tk) => sql.raw(`"${tk}"`),
490
- queryDimTableRef: () => sql.raw("\"query_dim\"")
491
- });
492
- if ((options.partitionKeyEncoding ?? DEFAULT_PARTITION_KEY_ENCODING) === "int") return adapter;
493
- return {
494
- ...adapter,
495
- siteIdColRef: (tk) => sql`CONCAT(${adapter.siteIdColRef(tk)}, '')`,
496
- searchTypeColRef: (tk) => sql`CONCAT(${adapter.searchTypeColRef(tk)}, '')`
497
- };
498
- }
499
395
  const ALLOWED_FILTER_DIMS = /* @__PURE__ */ new Set(["date", "queryCanonical"]);
500
396
  function planCoveredByCanonicalRollup(plan) {
501
397
  if (plan.dataset !== "queries") return false;
@@ -973,6 +869,110 @@ function matchesTopLevelPage(row) {
973
869
  for (let index = 0; index < path.length; index++) if (path.charCodeAt(index) === 47 && ++slashes > 1) return false;
974
870
  return true;
975
871
  }
872
+ const pgDialect = new PgDialect();
873
+ function withTenantCols(tableName, baseTable) {
874
+ const t = pgTable(tableName, {
875
+ site_id: varchar("site_id").notNull(),
876
+ search_type: varchar("search_type").notNull()
877
+ });
878
+ return {
879
+ ...baseTable,
880
+ site_id: t.site_id,
881
+ search_type: t.search_type
882
+ };
883
+ }
884
+ const icebergSchema = {
885
+ pages: withTenantCols("pages", drizzleSchema.pages),
886
+ queries: withTenantCols("queries", drizzleSchema.queries),
887
+ countries: withTenantCols("countries", drizzleSchema.countries),
888
+ page_queries: withTenantCols("page_queries", drizzleSchema.page_queries),
889
+ dates: withTenantCols("dates", drizzleSchema.dates),
890
+ search_appearance: withTenantCols("search_appearance", drizzleSchema.search_appearance),
891
+ search_appearance_pages: withTenantCols("search_appearance_pages", drizzleSchema.search_appearance_pages),
892
+ search_appearance_queries: withTenantCols("search_appearance_queries", drizzleSchema.search_appearance_queries),
893
+ search_appearance_page_queries: withTenantCols("search_appearance_page_queries", drizzleSchema.search_appearance_page_queries),
894
+ hourly_pages: withTenantCols("hourly_pages", drizzleSchema.hourly_pages)
895
+ };
896
+ function compilePg(query) {
897
+ const compiled = pgDialect.sqlToQuery(query);
898
+ return {
899
+ sql: compiled.sql,
900
+ params: compiled.params
901
+ };
902
+ }
903
+ const PG_BASE_CONFIG = {
904
+ schema: drizzleSchema,
905
+ datasetToTableKey: {
906
+ pages: "pages",
907
+ queries: "queries",
908
+ page_queries: "page_queries",
909
+ countries: "countries",
910
+ dates: "dates",
911
+ search_appearance: "search_appearance",
912
+ search_appearance_pages: "search_appearance_pages",
913
+ search_appearance_queries: "search_appearance_queries",
914
+ search_appearance_page_queries: "search_appearance_page_queries",
915
+ hourly_pages: "hourly_pages"
916
+ },
917
+ metricCast: "DOUBLE",
918
+ regexPredicate: (expr, pattern, negate) => negate ? sql`NOT regexp_matches(${expr}, ${pattern})` : sql`regexp_matches(${expr}, ${pattern})`,
919
+ urlToPathExpr: (col) => `CASE WHEN ${col} LIKE 'http%' THEN COALESCE(NULLIF(regexp_replace(${col}, '^https?://[^/]+', ''), ''), '/') ELSE ${col} END`,
920
+ includeSiteId: false,
921
+ compile: compilePg,
922
+ capabilities: {
923
+ regex: true,
924
+ comparisonJoin: true,
925
+ windowTotals: true
926
+ }
927
+ };
928
+ const pgResolverAdapter = createResolverAdapter({
929
+ ...PG_BASE_CONFIG,
930
+ tableLabel: "pg-resolver-adapter"
931
+ });
932
+ function createParquetResolverAdapter(options = {}) {
933
+ return createResolverAdapter({
934
+ ...PG_BASE_CONFIG,
935
+ tableLabel: "parquet-resolver-adapter",
936
+ queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
937
+ tableRef: (tk) => sql.raw(`read_parquet({{FILES}}, union_by_name = true) AS "${tk}"`),
938
+ queryDimTableRef: () => sql.raw("read_parquet({{QUERY_DIM}}, union_by_name = true) AS \"query_dim\"")
939
+ });
940
+ }
941
+ function createIcebergResolverAdapter(options = {}) {
942
+ return createResolverAdapter({
943
+ ...PG_BASE_CONFIG,
944
+ schema: icebergSchema,
945
+ includeSiteId: true,
946
+ includeSearchType: true,
947
+ tableLabel: "iceberg-resolver-adapter",
948
+ queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
949
+ tableRef: (tk) => sql.raw(`"${tk}"`),
950
+ queryDimTableRef: () => sql.raw("\"query_dim\"")
951
+ });
952
+ }
953
+ function createR2SqlResolverAdapter(options = {}) {
954
+ const adapter = createResolverAdapter({
955
+ ...PG_BASE_CONFIG,
956
+ schema: icebergSchema,
957
+ includeSiteId: true,
958
+ includeSearchType: true,
959
+ tableLabel: "r2-sql-resolver-adapter",
960
+ queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
961
+ capabilities: {
962
+ regex: false,
963
+ comparisonJoin: false,
964
+ windowTotals: false
965
+ },
966
+ tableRef: (tk) => sql.raw(`"${tk}"`),
967
+ queryDimTableRef: () => sql.raw("\"query_dim\"")
968
+ });
969
+ if ((options.partitionKeyEncoding ?? DEFAULT_PARTITION_KEY_ENCODING) === "int") return adapter;
970
+ return {
971
+ ...adapter,
972
+ siteIdColRef: (tk) => sql`CONCAT(${adapter.siteIdColRef(tk)}, '')`,
973
+ searchTypeColRef: (tk) => sql`CONCAT(${adapter.searchTypeColRef(tk)}, '')`
974
+ };
975
+ }
976
976
  var QuerySourceCoverageError = class extends Error {
977
977
  fallback;
978
978
  name = "QuerySourceCoverageError";
@@ -1,5 +1,6 @@
1
1
  import { SCHEMAS, TABLE_METADATA } from "./schema.mjs";
2
- import { DEFAULT_PARTITION_KEY_ENCODING, DEFAULT_PARTITION_KEY_ENCODING as DEFAULT_PARTITION_KEY_ENCODING$1, defineIcebergDataset } from "@gscdump/lakehouse";
2
+ import { defineIcebergDataset } from "@gscdump/lakehouse";
3
+ import { DEFAULT_PARTITION_KEY_ENCODING, DEFAULT_PARTITION_KEY_ENCODING as DEFAULT_PARTITION_KEY_ENCODING$1 } from "@gscdump/lakehouse/schema";
3
4
  function mapColumnType(t) {
4
5
  switch (t) {
5
6
  case "VARCHAR": return "STRING";
@@ -2,7 +2,8 @@ import { Row as Row$1, SearchType, TenantCtx as TenantCtx$1 } from "./storage.mj
2
2
  import { EngineError } from "./errors.mjs";
3
3
  import { icebergCreateTable } from "./libs/icebird.mjs";
4
4
  import { Result } from "gscdump/result";
5
- import { CommitRetryOptions, ConnectIcebergOptions, IcebergCatalogConfig, IcebergColumn, IcebergConnection, IcebergListedDataFile, IcebergPartitionField, IcebergS3Config, IcebergTableSpec, PartitionKeyEncoding, PartitionKeyEncoding as PartitionKeyEncoding$1, QueryProfiler } from "@gscdump/lakehouse";
5
+ import { CommitRetryOptions, ConnectIcebergOptions, IcebergCatalogConfig, IcebergConnection, IcebergListedDataFile, QueryProfiler } from "@gscdump/lakehouse";
6
+ import { IcebergColumn, IcebergPartitionField, IcebergS3Config, IcebergTableSpec, PartitionKeyEncoding, PartitionKeyEncoding as PartitionKeyEncoding$1 } from "@gscdump/lakehouse/schema";
6
7
  import { TableName } from "@gscdump/contracts";
7
8
  /** The 9 fact tables that exist as global Iceberg tables. */
8
9
  type IcebergTableName = Extract<TableName, 'pages' | 'queries' | 'countries' | 'page_queries' | 'dates' | 'search_appearance' | 'search_appearance_pages' | 'search_appearance_queries' | 'search_appearance_page_queries'>;
@@ -1,3 +1,29 @@
1
+ import { ParquetCodec, QueryExecutor, Row } from "./storage.mjs";
2
+ interface DuckDBHandle {
3
+ query: (sql: string, params?: unknown[]) => Promise<Row[]>;
4
+ registerFileBuffer: (name: string, bytes: Uint8Array) => Promise<void>;
5
+ copyFileToBuffer: (name: string) => Promise<Uint8Array>;
6
+ dropFiles: (names: string[]) => Promise<void>;
7
+ /**
8
+ * Returns a unique path suitable for `COPY TO '…'` + `copyFileToBuffer`.
9
+ * In Node this is an absolute path under `os.tmpdir()` so DuckDB doesn't
10
+ * litter the CWD; in browsers/Workers it's a plain virtual-FS name.
11
+ */
12
+ makeTempPath: (ext: string) => string;
13
+ }
14
+ interface DuckDBFactory {
15
+ getDuckDB: () => Promise<DuckDBHandle>;
16
+ }
17
+ interface DuckDBReadOptions {
18
+ /**
19
+ * Maximum non-URI parquet reads held in flight while registering DuckDB
20
+ * buffers. Defaults to 16; callers handling unusually large files can lower
21
+ * it to trade latency for peak JS memory.
22
+ */
23
+ bufferReadConcurrency?: number;
24
+ }
25
+ declare function createDuckDBCodec(factory: DuckDBFactory, options?: DuckDBReadOptions): ParquetCodec;
26
+ declare function createDuckDBExecutor(factory: DuckDBFactory, options?: DuckDBReadOptions): QueryExecutor;
1
27
  /**
2
28
  * Describes a hot/cold snapshot set. Produced by the snapshot builder,
3
29
  * consumed by `attachSnapshotIndex`. Filenames are derived from `cold`
@@ -11,4 +37,4 @@ interface SnapshotIndex {
11
37
  hot: boolean;
12
38
  hotDays: number;
13
39
  }
14
- export { SnapshotIndex };
40
+ export { DuckDBFactory, DuckDBHandle, SnapshotIndex, createDuckDBCodec, createDuckDBExecutor };
@@ -1,8 +1,12 @@
1
1
  import { DataSource, StorageEngine } from "../_chunks/storage.mjs";
2
- import { NodeDuckDBOptions, createNodeDuckDBHandle, resetNodeDuckDB } from "./duckdb-node.mjs";
3
- import { SnapshotIndex } from "../_chunks/snapshot.mjs";
2
+ import { DuckDBHandle, SnapshotIndex } from "../_chunks/snapshot.mjs";
4
3
  import { SearchType } from "gscdump/query";
5
4
  import { Row, TableName } from "@gscdump/contracts";
5
+ interface NodeDuckDBOptions {
6
+ verbose?: boolean;
7
+ }
8
+ declare function createNodeDuckDBHandle(opts?: NodeDuckDBOptions): DuckDBHandle;
9
+ declare function resetNodeDuckDB(): void;
6
10
  interface NodeHarnessOptions {
7
11
  dataDir: string;
8
12
  /** Tenant user id. Defaults to `'local'` for single-user CLI installs. */