@gscdump/engine 0.32.12 → 0.33.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.
@@ -14,7 +14,6 @@ const pages = pgTable("pages", {
14
14
  });
15
15
  const queries = pgTable("queries", {
16
16
  query: varchar("query").notNull(),
17
- query_canonical: varchar("query_canonical"),
18
17
  date: dateCol(),
19
18
  ...metricCols()
20
19
  });
@@ -26,7 +25,6 @@ const countries = pgTable("countries", {
26
25
  const page_queries = pgTable("page_queries", {
27
26
  url: varchar("url").notNull(),
28
27
  query: varchar("query").notNull(),
29
- query_canonical: varchar("query_canonical"),
30
28
  date: dateCol(),
31
29
  ...metricCols()
32
30
  });
@@ -60,7 +58,6 @@ const search_appearance_pages = pgTable("search_appearance_pages", {
60
58
  const search_appearance_queries = pgTable("search_appearance_queries", {
61
59
  searchAppearance: varchar("searchAppearance").notNull(),
62
60
  query: varchar("query").notNull(),
63
- query_canonical: varchar("query_canonical"),
64
61
  date: dateCol(),
65
62
  ...metricCols()
66
63
  });
@@ -68,7 +65,6 @@ const search_appearance_page_queries = pgTable("search_appearance_page_queries",
68
65
  searchAppearance: varchar("searchAppearance").notNull(),
69
66
  url: varchar("url").notNull(),
70
67
  query: varchar("query").notNull(),
71
- query_canonical: varchar("query_canonical"),
72
68
  date: dateCol(),
73
69
  ...metricCols()
74
70
  });
@@ -99,7 +95,7 @@ const TABLE_METADATA = {
99
95
  queries: {
100
96
  sortKey: ["date", "query"],
101
97
  clusterKey: ["query", "date"],
102
- version: 2
98
+ version: 3
103
99
  },
104
100
  countries: {
105
101
  sortKey: ["date", "country"],
@@ -117,7 +113,7 @@ const TABLE_METADATA = {
117
113
  "query",
118
114
  "date"
119
115
  ],
120
- version: 2
116
+ version: 3
121
117
  },
122
118
  dates: {
123
119
  sortKey: ["date"],
@@ -153,7 +149,7 @@ const TABLE_METADATA = {
153
149
  "query",
154
150
  "date"
155
151
  ],
156
- version: 1
152
+ version: 2
157
153
  },
158
154
  search_appearance_page_queries: {
159
155
  sortKey: [
@@ -168,7 +164,7 @@ const TABLE_METADATA = {
168
164
  "query",
169
165
  "date"
170
166
  ],
171
- version: 1
167
+ version: 2
172
168
  },
173
169
  hourly_pages: {
174
170
  sortKey: [
@@ -10,6 +10,7 @@ const ICEBERG_TABLES = [
10
10
  "search_appearance_queries",
11
11
  "search_appearance_page_queries"
12
12
  ];
13
+ const DEFAULT_PARTITION_KEY_ENCODING = "int";
13
14
  const SEARCH_TYPE_INT = {
14
15
  web: 1,
15
16
  image: 2,
@@ -30,7 +31,7 @@ const ICEBERG_PARTITION_COLUMNS = [{
30
31
  required: true,
31
32
  fieldId: 2
32
33
  }];
33
- function icebergPartitionColumns(encoding = "string") {
34
+ function icebergPartitionColumns(encoding = "int") {
34
35
  if (encoding === "string") return ICEBERG_PARTITION_COLUMNS;
35
36
  return [{
36
37
  name: "site_id",
@@ -71,7 +72,7 @@ function mapColumnType(t) {
71
72
  case "DATE": return "DATE";
72
73
  }
73
74
  }
74
- function icebergTableSpec(table, encoding = "string") {
75
+ function icebergTableSpec(table, encoding = "int") {
75
76
  const base = SCHEMAS[table];
76
77
  const dataColumns = base.columns.map((col, i) => ({
77
78
  name: col.name,
@@ -90,10 +91,11 @@ function icebergTableSpec(table, encoding = "string") {
90
91
  ]
91
92
  };
92
93
  }
93
- const ICEBERG_SCHEMAS = Object.fromEntries(ICEBERG_TABLES.map((t) => [t, icebergTableSpec(t)]));
94
+ const ICEBERG_SCHEMAS_STRING = Object.fromEntries(ICEBERG_TABLES.map((t) => [t, icebergTableSpec(t, "string")]));
94
95
  const ICEBERG_SCHEMAS_INT = Object.fromEntries(ICEBERG_TABLES.map((t) => [t, icebergTableSpec(t, "int")]));
95
- function icebergSchemasFor(encoding = "string") {
96
- return encoding === "int" ? ICEBERG_SCHEMAS_INT : ICEBERG_SCHEMAS;
96
+ const ICEBERG_SCHEMAS = ICEBERG_SCHEMAS_INT;
97
+ function icebergSchemasFor(encoding = "int") {
98
+ return encoding === "int" ? ICEBERG_SCHEMAS_INT : ICEBERG_SCHEMAS_STRING;
97
99
  }
98
100
  const ICEBERG_TABLE_SET = new Set(ICEBERG_TABLES);
99
101
  function isIcebergTable(table) {
@@ -103,4 +105,4 @@ function assertIcebergTable(table) {
103
105
  if (!isIcebergTable(table)) throw new Error(`Unknown Iceberg table '${table}'. Expected one of: ${ICEBERG_TABLES.join(", ")}`);
104
106
  return table;
105
107
  }
106
- export { ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_TABLES, INT_SEARCH_TYPE, SEARCH_TYPE_INT, assertIcebergTable, icebergPartitionColumns, icebergSchemasFor, icebergTableSpec, isIcebergTable };
108
+ export { DEFAULT_PARTITION_KEY_ENCODING, ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_SCHEMAS_STRING, ICEBERG_TABLES, INT_SEARCH_TYPE, SEARCH_TYPE_INT, assertIcebergTable, icebergPartitionColumns, icebergSchemasFor, icebergTableSpec, isIcebergTable };
@@ -53,7 +53,7 @@ interface IcebergColumn {
53
53
  /**
54
54
  * Partition-key encoding for the two identity columns (`site_id`, `search_type`).
55
55
  *
56
- * - `'string'` (default, legacy): both columns are STRING. Correct, but R2 SQL's
56
+ * - `'string'` (legacy): both columns are STRING. Correct, but R2 SQL's
57
57
  * string min/max statistics are truncated in predicate pushdown, so a bare
58
58
  * `WHERE site_id='<uuid>'` UNDERCOUNTS — callers must CONCAT(col,'') to stay
59
59
  * correct, which defeats partition pruning.
@@ -66,10 +66,12 @@ interface IcebergColumn {
66
66
  * {@link SEARCH_TYPE_INT} for `search_type` (engine-owned, fixed enum).
67
67
  *
68
68
  * New per-team catalogs are provisioned `'int'`; existing catalogs stay
69
- * `'string'`. Purely additive: `'string'` is the default everywhere so existing
70
- * tables, writers, and readers are unchanged.
69
+ * `'string'`. Callers that read or write legacy catalogs must pass
70
+ * `encoding: 'string'` explicitly.
71
71
  */
72
72
  type PartitionKeyEncoding = 'string' | 'int';
73
+ /** Default for new Iceberg/R2 Data Catalog tables. */
74
+ declare const DEFAULT_PARTITION_KEY_ENCODING: PartitionKeyEncoding;
73
75
  /**
74
76
  * Stable `search_type` enum → int map for `'int'`-encoded catalogs. Engine-owned
75
77
  * and FROZEN: never renumber or reuse an id (it's the on-disk partition value).
@@ -142,11 +144,13 @@ declare const ICEBERG_PARTITION_SPEC: readonly IcebergPartitionField[];
142
144
  * source of truth — do not hand-list columns elsewhere.
143
145
  */
144
146
  declare function icebergTableSpec(table: IcebergTableName, encoding?: PartitionKeyEncoding): IcebergTableSpec;
145
- /** All Iceberg table specs (legacy `'string'` encoding), keyed by table name. */
146
- declare const ICEBERG_SCHEMAS: Record<IcebergTableName, IcebergTableSpec>;
147
+ /** All Iceberg table specs in legacy `'string'` encoding, keyed by table name. */
148
+ declare const ICEBERG_SCHEMAS_STRING: Record<IcebergTableName, IcebergTableSpec>;
147
149
  /** All Iceberg table specs in `'int'` encoding (INT site_id + INT search_type). */
148
150
  declare const ICEBERG_SCHEMAS_INT: Record<IcebergTableName, IcebergTableSpec>;
149
- /** Table specs for the given encoding (`'string'` default). */
151
+ /** All Iceberg table specs for the default new-catalog encoding. */
152
+ declare const ICEBERG_SCHEMAS: Record<IcebergTableName, IcebergTableSpec>;
153
+ /** Table specs for the given encoding (`'int'` default). */
150
154
  declare function icebergSchemasFor(encoding?: PartitionKeyEncoding): Record<IcebergTableName, IcebergTableSpec>;
151
155
  /** True when `table` is one of the canonical {@link ICEBERG_TABLES}. */
152
156
  declare function isIcebergTable(table: string): table is IcebergTableName;
@@ -385,7 +389,8 @@ interface ListIcebergDataFilesOptions {
385
389
  /**
386
390
  * Partition-key encoding of the catalog. `'int'` changes how manifest-summary
387
391
  * bounds are decoded (int bytes vs UTF-8) and how the per-file partition value
388
- * is compared. Defaults to `'string'`.
392
+ * is compared. Defaults to `'int'` for new catalogs; pass `'string'` for
393
+ * legacy catalogs.
389
394
  */
390
395
  encoding?: PartitionKeyEncoding;
391
396
  /**
@@ -542,10 +547,11 @@ interface IcebergAppendSinkOptions extends SinkOptions {
542
547
  */
543
548
  commitRetry?: CommitRetryOptions;
544
549
  /**
545
- * Partition-key encoding (default `'string'`). `'int'` writes BOTH `site_id`
546
- * and `search_type` as INT — the caller MUST pass the numeric `site_id` (a
547
- * numeric string is fine; it's `Number()`-coerced) in `slice.ctx.siteId`. A
548
- * small INT is ample (≪ 2.1B sites), so no LONG/BigInt is involved. See
550
+ * Partition-key encoding (default `'int'` for new catalogs). `'int'` writes
551
+ * BOTH `site_id` and `search_type` as INT — the caller MUST pass the numeric
552
+ * `site_id` (a numeric string is fine; it's `Number()`-coerced) in
553
+ * `slice.ctx.siteId`. Pass `'string'` explicitly for legacy catalogs. A small
554
+ * INT is ample (≪ 2.1B sites), so no LONG/BigInt is involved. See
549
555
  * {@link import('./iceberg/schema').PartitionKeyEncoding}.
550
556
  */
551
557
  encoding?: PartitionKeyEncoding;
@@ -559,4 +565,4 @@ interface LocalIcebergSinkOptions extends SinkOptions {
559
565
  /** S3-compatible warehouse location (POC: MinIO). */
560
566
  warehouse: string;
561
567
  }
562
- export { CatalogCache, CommitRetryOptions, ConnectIcebergOptions, ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_TABLES, INT_SEARCH_TYPE, IcebergAppendSinkOptions, IcebergCatalogConfig, IcebergColumn, IcebergColumnType, IcebergConnection, IcebergListedDataFile, IcebergPartitionField, IcebergPartitionSpec, IcebergPartitionSpecField, IcebergPartitionTransform, IcebergPrimitiveType, IcebergS3Config, IcebergSchema, IcebergSchemaField, IcebergSortOrder, IcebergSortOrderField, IcebergTableName, IcebergTableOpResult, IcebergTableSpec, ListIcebergDataFilesOptions, LocalIcebergSinkOptions, PartitionKeyEncoding, SEARCH_TYPE_INT, Sink, SinkCapabilities, SinkCloseResult, SinkOptions, SinkSlice, SinkWriteResult, SliceOverwriteWriter, assertIcebergTable, connectIcebergCatalog, createIcebergTables, dropIcebergTables, ensureIcebergNamespace, icebergAppendRetrying, icebergPartitionColumns, icebergPartitionSpecFor, icebergSchemaFor, icebergSchemasFor, icebergSortOrderFor, icebergTableSpec, isCommitRateLimited, isIcebergTable, listIcebergDataFiles, listIcebergTables };
568
+ export { CatalogCache, CommitRetryOptions, ConnectIcebergOptions, DEFAULT_PARTITION_KEY_ENCODING, ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_SCHEMAS_STRING, ICEBERG_TABLES, INT_SEARCH_TYPE, IcebergAppendSinkOptions, IcebergCatalogConfig, IcebergColumn, IcebergColumnType, IcebergConnection, IcebergListedDataFile, IcebergPartitionField, IcebergPartitionSpec, IcebergPartitionSpecField, IcebergPartitionTransform, IcebergPrimitiveType, IcebergS3Config, IcebergSchema, IcebergSchemaField, IcebergSortOrder, IcebergSortOrderField, IcebergTableName, IcebergTableOpResult, IcebergTableSpec, ListIcebergDataFilesOptions, LocalIcebergSinkOptions, PartitionKeyEncoding, SEARCH_TYPE_INT, Sink, SinkCapabilities, SinkCloseResult, SinkOptions, SinkSlice, SinkWriteResult, SliceOverwriteWriter, assertIcebergTable, connectIcebergCatalog, createIcebergTables, dropIcebergTables, ensureIcebergNamespace, icebergAppendRetrying, icebergPartitionColumns, icebergPartitionSpecFor, icebergSchemaFor, icebergSchemasFor, icebergSortOrderFor, icebergTableSpec, isCommitRateLimited, isIcebergTable, listIcebergDataFiles, listIcebergTables };
@@ -10,6 +10,9 @@ interface ResolverAdapter<TableKey extends string = string> {
10
10
  dimColumn: (dim: Dimension, tableKey: TableKey) => string;
11
11
  isMetricDimension: (dim: string) => dim is Metric;
12
12
  tableRef: (tableKey: TableKey) => SQL;
13
+ fromSql: (tableKey: TableKey, options?: {
14
+ queryCanonical?: boolean;
15
+ }) => SQL;
13
16
  dateColRef: (tableKey: TableKey) => SQL;
14
17
  urlToPathExpr: (col: string) => string;
15
18
  siteIdColRef?: (tableKey: TableKey) => SQL;
@@ -1,4 +1,4 @@
1
- import { inferLegacyTier, inferSearchType } from "../_chunks/layout.mjs";
1
+ import { manifestEntryKey, matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter, mergeSyncState, syncStateKey, watermarkKey } from "../_chunks/manifest-store-utils.mjs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { randomBytes } from "node:crypto";
@@ -29,13 +29,13 @@ function createFilesystemDataSource(opts) {
29
29
  }));
30
30
  },
31
31
  async list(prefix) {
32
- const full = resolve(root, prefix);
32
+ const full = pathFor(prefix);
33
33
  const out = [];
34
34
  await walk(full, out);
35
35
  return out.map((p) => p.slice(root.length + 1));
36
36
  },
37
37
  async *streamList(prefix) {
38
- const full = resolve(root, prefix);
38
+ const full = pathFor(prefix);
39
39
  for await (const p of walkStream(full)) yield p.slice(root.length + 1);
40
40
  },
41
41
  async head(key) {
@@ -71,57 +71,6 @@ async function walk(dir, out) {
71
71
  else out.push(p);
72
72
  }
73
73
  }
74
- function watermarkKey(w) {
75
- return `${w.userId}|${w.siteId ?? ""}|${w.table}`;
76
- }
77
- function matchesWatermarkFilter(w, filter) {
78
- if (w.userId !== filter.userId) return false;
79
- if (filter.siteId !== void 0 && w.siteId !== filter.siteId) return false;
80
- if (filter.table !== void 0 && w.table !== filter.table) return false;
81
- return true;
82
- }
83
- function syncStateKey(s) {
84
- return `${s.userId}|${s.siteId ?? ""}|${s.table}|${s.date}|${inferSearchType(s)}`;
85
- }
86
- function matchesSyncStateFilter(s, filter) {
87
- if (s.userId !== filter.userId) return false;
88
- if (filter.siteId !== void 0 && s.siteId !== filter.siteId) return false;
89
- if (filter.table !== void 0 && s.table !== filter.table) return false;
90
- if (filter.state !== void 0 && s.state !== filter.state) return false;
91
- if (filter.searchType !== void 0 && inferSearchType(s) !== filter.searchType) return false;
92
- return true;
93
- }
94
- function mergeSyncState(existing, scope, state, detail) {
95
- const at = detail?.at ?? Date.now();
96
- const attemptsBump = state === "inflight" ? 1 : 0;
97
- if (!existing) return {
98
- userId: scope.userId,
99
- siteId: scope.siteId,
100
- table: scope.table,
101
- date: scope.date,
102
- state,
103
- updatedAt: at,
104
- attempts: attemptsBump,
105
- error: detail?.error,
106
- ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {}
107
- };
108
- return {
109
- ...existing,
110
- state,
111
- updatedAt: at,
112
- attempts: existing.attempts + attemptsBump,
113
- error: state === "done" ? void 0 : detail?.error ?? existing.error
114
- };
115
- }
116
- function matchesFilter(entry, filter) {
117
- if (entry.userId !== filter.userId) return false;
118
- if (filter.siteId !== void 0 && entry.siteId !== filter.siteId) return false;
119
- if (filter.table !== void 0 && entry.table !== filter.table) return false;
120
- if (filter.partitions && !filter.partitions.includes(entry.partition)) return false;
121
- if (filter.tier !== void 0 && inferLegacyTier(entry) !== filter.tier) return false;
122
- if (filter.searchType !== void 0 && inferSearchType(entry) !== filter.searchType) return false;
123
- return true;
124
- }
125
74
  function lockFileFor(locksDir, scope) {
126
75
  return join(locksDir, `${`${scope.userId}|${scope.siteId ?? ""}|${scope.table}|${scope.partition}`.replace(/[^\w.-]/g, "_")}.lock`);
127
76
  }
@@ -166,30 +115,27 @@ function createFilesystemManifestStore(opts) {
166
115
  while (queue.length > 0) await queue.shift()().catch(() => {});
167
116
  running = false;
168
117
  }
169
- function entryKey(e) {
170
- return e.objectKey;
171
- }
172
118
  async function registerVersionsImpl(newEntries, superseding) {
173
119
  const data = await load();
174
120
  const supersededAt = newEntries[0]?.createdAt ?? Date.now();
175
- const byKey = new Map(data.entries.map((e) => [entryKey(e), e]));
121
+ const byKey = new Map(data.entries.map((e) => [manifestEntryKey(e), e]));
176
122
  if (superseding) for (const s of superseding) {
177
- const existing = byKey.get(entryKey(s));
178
- if (existing && existing.retiredAt === void 0) byKey.set(entryKey(s), {
123
+ const existing = byKey.get(manifestEntryKey(s));
124
+ if (existing && existing.retiredAt === void 0) byKey.set(manifestEntryKey(s), {
179
125
  ...existing,
180
126
  retiredAt: supersededAt
181
127
  });
182
128
  }
183
- for (const e of newEntries) byKey.set(entryKey(e), e);
129
+ for (const e of newEntries) byKey.set(manifestEntryKey(e), e);
184
130
  data.entries = Array.from(byKey.values());
185
131
  await save(data);
186
132
  }
187
133
  return {
188
134
  async listLive(filter) {
189
- return (await load()).entries.filter((e) => e.retiredAt === void 0 && matchesFilter(e, filter));
135
+ return (await load()).entries.filter((e) => e.retiredAt === void 0 && matchesManifestEntryFilter(e, filter));
190
136
  },
191
137
  async listAll(filter) {
192
- return (await load()).entries.filter((e) => matchesFilter(e, filter));
138
+ return (await load()).entries.filter((e) => matchesManifestEntryFilter(e, filter));
193
139
  },
194
140
  async registerVersion(entry, superseding) {
195
141
  return enqueue(() => registerVersionsImpl([entry], superseding));
@@ -203,8 +149,8 @@ function createFilesystemManifestStore(opts) {
203
149
  async delete(toDelete) {
204
150
  return enqueue(async () => {
205
151
  const data = await load();
206
- const toDeleteKeys = new Set(toDelete.map(entryKey));
207
- data.entries = data.entries.filter((e) => !toDeleteKeys.has(entryKey(e)));
152
+ const toDeleteKeys = new Set(toDelete.map(manifestEntryKey));
153
+ data.entries = data.entries.filter((e) => !toDeleteKeys.has(manifestEntryKey(e)));
208
154
  await save(data);
209
155
  });
210
156
  },
@@ -1,4 +1,4 @@
1
- import { SCHEMAS, TABLE_METADATA, dedupeByNaturalKey } from "../_chunks/schema.mjs";
1
+ import { SCHEMAS, TABLE_METADATA } from "../_chunks/schema.mjs";
2
2
  import { parquetReadObjects } from "../_chunks/libs/hyparquet.mjs";
3
3
  import { ByteWriter, parquetWriteRows } from "../_chunks/libs/hyparquet-writer.mjs";
4
4
  const ROW_GROUP_SIZE = 25e3;
@@ -119,10 +119,16 @@ function sortRowsByClusterKey(table, rows) {
119
119
  });
120
120
  return copy;
121
121
  }
122
+ function naturalKeyFor(table, row) {
123
+ return TABLE_METADATA[table].sortKey.map((col) => `${row[col] ?? ""}`).join("\0");
124
+ }
122
125
  function encodeOrderedRows(rows, columns, rowGroupSize) {
123
126
  const schema = buildWriteSchema(columns);
124
- const isDate = columns.map((col) => col.type === "DATE");
125
- const types = columns.map((col) => basicTypeFor(col.type));
127
+ const codecs = columns.map((col) => ({
128
+ name: col.name,
129
+ isDate: col.type === "DATE",
130
+ type: basicTypeFor(col.type)
131
+ }));
126
132
  const columnSpecs = columns.map((col) => ({
127
133
  name: col.name,
128
134
  nullable: col.nullable,
@@ -131,10 +137,7 @@ function encodeOrderedRows(rows, columns, rowGroupSize) {
131
137
  function* coercedRows() {
132
138
  for (const r of rows) {
133
139
  const out = {};
134
- for (let c = 0; c < columns.length; c++) {
135
- const name = columns[c].name;
136
- out[name] = isDate[c] ? toEpochDays(r[name]) : coerceValue(r[name], types[c]);
137
- }
140
+ for (const codec of codecs) out[codec.name] = codec.isDate ? toEpochDays(r[codec.name]) : coerceValue(r[codec.name], codec.type);
138
141
  yield out;
139
142
  }
140
143
  }
@@ -222,12 +225,12 @@ function createHyparquetCodec(options = {}) {
222
225
  rowCount: 0
223
226
  };
224
227
  }
225
- const allRows = [];
228
+ const byNaturalKey = /* @__PURE__ */ new Map();
226
229
  for (const key of inputKeys) {
227
230
  const rows = await decodeParquetToRows(await dataSource.read(key));
228
- for (let i = 0; i < rows.length; i++) allRows.push(rows[i]);
231
+ for (let i = 0; i < rows.length; i++) byNaturalKey.set(naturalKeyFor(ctx.table, rows[i]), rows[i]);
229
232
  }
230
- const rows = dedupeByNaturalKey(ctx.table, allRows);
233
+ const rows = [...byNaturalKey.values()];
231
234
  const bytes = encodeRowsToParquet(ctx.table, rows);
232
235
  await dataSource.write(outputKey, bytes);
233
236
  return {
@@ -1,13 +1,30 @@
1
- import { inferLegacyTier, inferSearchType } from "../_chunks/layout.mjs";
1
+ import { inferSearchType } from "../_chunks/layout.mjs";
2
2
  import { engineErrorToException, engineErrors } from "../errors.mjs";
3
+ import { matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter } from "../_chunks/manifest-store-utils.mjs";
3
4
  import { err, ok, unwrapResult } from "gscdump/result";
4
5
  const SHARD_RE = /^u_[^/]+\/manifest\/(?<siteId>[^/]+)\/(?<table>[^/]+)\/HEAD$/;
5
6
  const CAS_BACKOFF_BASE_MS = 5;
6
7
  const CAS_BACKOFF_CAP_MS = 250;
8
+ const SHARD_IO_CONCURRENCY = 8;
7
9
  async function casBackoff(attempt) {
8
10
  const ceil = Math.min(CAS_BACKOFF_CAP_MS, CAS_BACKOFF_BASE_MS * 2 ** attempt);
9
11
  await new Promise((resolve) => setTimeout(resolve, Math.random() * ceil));
10
12
  }
13
+ async function mapWithConcurrency(items, concurrency, fn) {
14
+ if (items.length === 0) return [];
15
+ const workerCount = Math.max(1, Math.min(items.length, Math.floor(concurrency)));
16
+ const results = Array.from({ length: items.length }, () => void 0);
17
+ let nextIndex = 0;
18
+ async function worker() {
19
+ while (true) {
20
+ const index = nextIndex++;
21
+ if (index >= items.length) return;
22
+ results[index] = await fn(items[index], index);
23
+ }
24
+ }
25
+ await Promise.all(Array.from({ length: workerCount }, worker));
26
+ return results;
27
+ }
11
28
  function defaultSnapshotId() {
12
29
  return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
13
30
  }
@@ -36,26 +53,6 @@ function shardScopesFromEntries(entries) {
36
53
  }
37
54
  return out;
38
55
  }
39
- function matchesEntryFilter(entry, filter) {
40
- if (filter.siteId !== void 0 && entry.siteId !== filter.siteId) return false;
41
- if (filter.table !== void 0 && entry.table !== filter.table) return false;
42
- if (filter.partitions && !filter.partitions.includes(entry.partition)) return false;
43
- if (filter.tier !== void 0 && inferLegacyTier(entry) !== filter.tier) return false;
44
- if (filter.searchType !== void 0 && inferSearchType(entry) !== filter.searchType) return false;
45
- return true;
46
- }
47
- function matchesWatermarkFilter(w, filter) {
48
- if (filter.siteId !== void 0 && w.siteId !== filter.siteId) return false;
49
- if (filter.table !== void 0 && w.table !== filter.table) return false;
50
- return true;
51
- }
52
- function matchesSyncStateFilter(s, filter) {
53
- if (filter.siteId !== void 0 && s.siteId !== filter.siteId) return false;
54
- if (filter.table !== void 0 && s.table !== filter.table) return false;
55
- if (filter.state !== void 0 && s.state !== filter.state) return false;
56
- if (filter.searchType !== void 0 && inferSearchType(s) !== filter.searchType) return false;
57
- return true;
58
- }
59
56
  function createR2ManifestStore(opts) {
60
57
  const { bucket, userId } = opts;
61
58
  const newSnapshotId = opts.newSnapshotId ?? defaultSnapshotId;
@@ -154,17 +151,20 @@ function createR2ManifestStore(opts) {
154
151
  }];
155
152
  return (await listShards()).filter((s) => (filter.siteId === void 0 || s.siteId === filter.siteId) && (filter.table === void 0 || s.table === filter.table));
156
153
  }
154
+ function assertScopedUser(got, op) {
155
+ if (got !== userId) throw new Error(`${op}: R2 manifest store is scoped to userId=${userId}, got ${got}`);
156
+ }
157
157
  async function readEntriesAcrossShards(filter, includeRetired) {
158
- const shards = await shardsForFilter(filter);
159
- const all = [];
160
- for (const { siteId, table } of shards) {
158
+ assertScopedUser(filter.userId, includeRetired ? "listAll" : "listLive");
159
+ return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
161
160
  const { snapshot } = await readShard(siteId, table);
161
+ const entries = [];
162
162
  for (const entry of snapshot.entries) {
163
163
  if (!includeRetired && entry.retiredAt !== void 0) continue;
164
- if (matchesEntryFilter(entry, filter)) all.push(entry);
164
+ if (matchesManifestEntryFilter(entry, filter, { ignoreUserId: true })) entries.push(entry);
165
165
  }
166
- }
167
- return all;
166
+ return entries;
167
+ })).flat();
168
168
  }
169
169
  function groupBySiteTable(entries) {
170
170
  const out = /* @__PURE__ */ new Map();
@@ -180,6 +180,7 @@ function createR2ManifestStore(opts) {
180
180
  const supersededAt = newEntries[0]?.createdAt ?? now();
181
181
  const byShard = /* @__PURE__ */ new Map();
182
182
  function bucket(entry, kind) {
183
+ assertScopedUser(entry.userId, "registerVersions");
183
184
  if (entry.siteId === void 0) throw new Error("R2 manifest store requires entries to carry siteId");
184
185
  const key = `${entry.siteId}\0${entry.table}`;
185
186
  let bag = byShard.get(key);
@@ -195,7 +196,7 @@ function createR2ManifestStore(opts) {
195
196
  }
196
197
  for (const e of newEntries) bucket(e, "new");
197
198
  if (superseding) for (const e of superseding) bucket(e, "super");
198
- for (const [shardKey, { newEntries: news, superseding: supers }] of byShard) {
199
+ await mapWithConcurrency([...byShard], SHARD_IO_CONCURRENCY, async ([shardKey, { newEntries: news, superseding: supers }]) => {
199
200
  const [siteId, table] = shardKey.split("\0");
200
201
  await mutateShard(siteId, table, (snap) => {
201
202
  const byObjectKey = new Map(snap.entries.map((e) => [e.objectKey, e]));
@@ -209,7 +210,7 @@ function createR2ManifestStore(opts) {
209
210
  for (const n of news) byObjectKey.set(n.objectKey, n);
210
211
  snap.entries = Array.from(byObjectKey.values());
211
212
  });
212
- }
213
+ });
213
214
  }
214
215
  return {
215
216
  async listLive(filter) {
@@ -226,34 +227,33 @@ function createR2ManifestStore(opts) {
226
227
  return registerVersionsImpl(entries, superseding);
227
228
  },
228
229
  async listRetired(olderThan) {
229
- const shards = await listShards();
230
- const out = [];
231
- for (const { siteId, table } of shards) {
230
+ return (await mapWithConcurrency(await listShards(), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
232
231
  const { snapshot } = await readShard(siteId, table);
233
- for (const e of snapshot.entries) if (e.retiredAt !== void 0 && e.retiredAt <= olderThan) out.push(e);
234
- }
235
- return out;
232
+ const retired = [];
233
+ for (const e of snapshot.entries) if (e.retiredAt !== void 0 && e.retiredAt <= olderThan) retired.push(e);
234
+ return retired;
235
+ })).flat();
236
236
  },
237
237
  async delete(toDelete) {
238
- const grouped = groupBySiteTable(toDelete);
239
- for (const [shardKey, entries] of grouped) {
238
+ await mapWithConcurrency([...groupBySiteTable(toDelete)], SHARD_IO_CONCURRENCY, async ([shardKey, entries]) => {
240
239
  const [siteId, table] = shardKey.split("\0");
241
240
  await mutateShard(siteId, table, (snap) => {
242
241
  const drop = new Set(entries.map((e) => e.objectKey));
243
242
  snap.entries = snap.entries.filter((e) => !drop.has(e.objectKey));
244
243
  });
245
- }
244
+ });
246
245
  },
247
246
  async getWatermarks(filter) {
248
- const shards = await shardsForFilter(filter);
249
- const out = [];
250
- for (const { siteId, table } of shards) {
247
+ assertScopedUser(filter.userId, "getWatermarks");
248
+ return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
251
249
  const { snapshot } = await readShard(siteId, table);
252
- for (const w of snapshot.watermarks) if (matchesWatermarkFilter(w, filter)) out.push(w);
253
- }
254
- return out;
250
+ const watermarks = [];
251
+ for (const w of snapshot.watermarks) if (matchesWatermarkFilter(w, filter, { ignoreUserId: true })) watermarks.push(w);
252
+ return watermarks;
253
+ })).flat();
255
254
  },
256
255
  async bumpWatermark(scope, date, at) {
256
+ assertScopedUser(scope.userId, "bumpWatermark");
257
257
  if (scope.siteId === void 0) throw new Error("R2 manifest store requires watermarks to carry siteId");
258
258
  const ts = at ?? now();
259
259
  await mutateShard(scope.siteId, scope.table, (snap) => {
@@ -282,15 +282,16 @@ function createR2ManifestStore(opts) {
282
282
  });
283
283
  },
284
284
  async getSyncStates(filter) {
285
- const shards = await shardsForFilter(filter);
286
- const out = [];
287
- for (const { siteId, table } of shards) {
285
+ assertScopedUser(filter.userId, "getSyncStates");
286
+ return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
288
287
  const { snapshot } = await readShard(siteId, table);
289
- for (const s of snapshot.syncStates) if (matchesSyncStateFilter(s, filter)) out.push(s);
290
- }
291
- return out;
288
+ const states = [];
289
+ for (const s of snapshot.syncStates) if (matchesSyncStateFilter(s, filter, { ignoreUserId: true })) states.push(s);
290
+ return states;
291
+ })).flat();
292
292
  },
293
293
  async setSyncState(scope, state, detail) {
294
+ assertScopedUser(scope.userId, "setSyncState");
294
295
  if (scope.siteId === void 0) throw new Error("R2 manifest store requires sync states to carry siteId");
295
296
  const at = detail?.at ?? now();
296
297
  const scopeSearchType = inferSearchType(scope);
@@ -34,8 +34,8 @@ interface QueryDimDeps {
34
34
  }
35
35
  /**
36
36
  * Pure: distinct raw queries → dimension records. De-dupes, drops empties, and
37
- * folds an empty/whitespace canonical back to the raw query so the key is
38
- * total (matches the read path's `COALESCE(NULLIF(query_canonical, ''), query)`).
37
+ * folds an empty/whitespace canonical back to the raw query so the dimension
38
+ * is total for read-time joins.
39
39
  */
40
40
  declare function buildQueryDimRecords(queries: Iterable<string>, deps: QueryDimDeps): QueryDimRecord[];
41
41
  interface QueryDimStore {
@@ -1,4 +1,4 @@
1
- import { CatalogCache, CommitRetryOptions, ConnectIcebergOptions, ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_TABLES, INT_SEARCH_TYPE, IcebergAppendSinkOptions, IcebergCatalogConfig, IcebergColumn, IcebergColumnType, IcebergConnection, IcebergListedDataFile, IcebergPartitionField, IcebergPartitionSpec, IcebergPartitionSpecField, IcebergPartitionTransform, IcebergPrimitiveType, IcebergS3Config, IcebergSchema, IcebergSchemaField, IcebergSortOrder, IcebergSortOrderField, IcebergTableName, IcebergTableOpResult, IcebergTableSpec, ListIcebergDataFilesOptions, PartitionKeyEncoding, SEARCH_TYPE_INT, Sink, assertIcebergTable, connectIcebergCatalog, createIcebergTables, dropIcebergTables, ensureIcebergNamespace, icebergAppendRetrying, icebergPartitionColumns, icebergPartitionSpecFor, icebergSchemaFor, icebergSchemasFor, icebergSortOrderFor, icebergTableSpec, isCommitRateLimited, isIcebergTable, listIcebergDataFiles, listIcebergTables } from "../_chunks/sink.mjs";
1
+ import { CatalogCache, CommitRetryOptions, ConnectIcebergOptions, DEFAULT_PARTITION_KEY_ENCODING, ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_SCHEMAS_STRING, ICEBERG_TABLES, INT_SEARCH_TYPE, IcebergAppendSinkOptions, IcebergCatalogConfig, IcebergColumn, IcebergColumnType, IcebergConnection, IcebergListedDataFile, IcebergPartitionField, IcebergPartitionSpec, IcebergPartitionSpecField, IcebergPartitionTransform, IcebergPrimitiveType, IcebergS3Config, IcebergSchema, IcebergSchemaField, IcebergSortOrder, IcebergSortOrderField, IcebergTableName, IcebergTableOpResult, IcebergTableSpec, ListIcebergDataFilesOptions, PartitionKeyEncoding, SEARCH_TYPE_INT, Sink, assertIcebergTable, connectIcebergCatalog, createIcebergTables, dropIcebergTables, ensureIcebergNamespace, icebergAppendRetrying, icebergPartitionColumns, icebergPartitionSpecFor, icebergSchemaFor, icebergSchemasFor, icebergSortOrderFor, icebergTableSpec, isCommitRateLimited, isIcebergTable, listIcebergDataFiles, listIcebergTables } from "../_chunks/sink.mjs";
2
2
  import { icebergCreateTable, icebergManifests, restCatalogLoadTable } from "../_chunks/libs/icebird.mjs";
3
3
  type IcebergAppendSink = Sink;
4
4
  /**
@@ -10,4 +10,4 @@ type IcebergAppendSink = Sink;
10
10
  * with no rows never touches the network.
11
11
  */
12
12
  declare function createIcebergAppendSink(options: IcebergAppendSinkOptions): IcebergAppendSink;
13
- export { type CatalogCache, type CommitRetryOptions, type ConnectIcebergOptions, ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_TABLES, INT_SEARCH_TYPE, type IcebergAppendSink, type IcebergAppendSinkOptions, type IcebergCatalogConfig, type IcebergColumn, type IcebergColumnType, type IcebergConnection, type IcebergListedDataFile, type IcebergPartitionField, type IcebergPartitionSpec, type IcebergPartitionSpecField, type IcebergPartitionTransform, type IcebergPrimitiveType, type IcebergS3Config, type IcebergSchema, type IcebergSchemaField, type IcebergSortOrder, type IcebergSortOrderField, type IcebergTableName, type IcebergTableOpResult, type IcebergTableSpec, type ListIcebergDataFilesOptions, type PartitionKeyEncoding, SEARCH_TYPE_INT, assertIcebergTable, connectIcebergCatalog, createIcebergAppendSink, createIcebergTables, dropIcebergTables, ensureIcebergNamespace, icebergAppendRetrying, icebergCreateTable, icebergManifests, icebergPartitionColumns, icebergPartitionSpecFor, icebergSchemaFor, icebergSchemasFor, icebergSortOrderFor, icebergTableSpec, isCommitRateLimited, isIcebergTable, listIcebergDataFiles, listIcebergTables, restCatalogLoadTable };
13
+ export { type CatalogCache, type CommitRetryOptions, type ConnectIcebergOptions, DEFAULT_PARTITION_KEY_ENCODING, ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_SCHEMAS_STRING, ICEBERG_TABLES, INT_SEARCH_TYPE, type IcebergAppendSink, type IcebergAppendSinkOptions, type IcebergCatalogConfig, type IcebergColumn, type IcebergColumnType, type IcebergConnection, type IcebergListedDataFile, type IcebergPartitionField, type IcebergPartitionSpec, type IcebergPartitionSpecField, type IcebergPartitionTransform, type IcebergPrimitiveType, type IcebergS3Config, type IcebergSchema, type IcebergSchemaField, type IcebergSortOrder, type IcebergSortOrderField, type IcebergTableName, type IcebergTableOpResult, type IcebergTableSpec, type ListIcebergDataFilesOptions, type PartitionKeyEncoding, SEARCH_TYPE_INT, assertIcebergTable, connectIcebergCatalog, createIcebergAppendSink, createIcebergTables, dropIcebergTables, ensureIcebergNamespace, icebergAppendRetrying, icebergCreateTable, icebergManifests, icebergPartitionColumns, icebergPartitionSpecFor, icebergSchemaFor, icebergSchemasFor, icebergSortOrderFor, icebergTableSpec, isCommitRateLimited, isIcebergTable, listIcebergDataFiles, listIcebergTables, restCatalogLoadTable };