@gscdump/engine 0.38.2 → 0.40.1

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.
@@ -455,7 +455,6 @@ function createParquetResolverAdapter(options = {}) {
455
455
  return createResolverAdapter({
456
456
  ...PG_BASE_CONFIG,
457
457
  tableLabel: "parquet-resolver-adapter",
458
- canonicalFallback: options.canonicalFallback ?? false,
459
458
  queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
460
459
  tableRef: (tk) => sql.raw(`read_parquet({{FILES}}, union_by_name = true) AS "${tk}"`),
461
460
  queryDimTableRef: () => sql.raw("read_parquet({{QUERY_DIM}}, union_by_name = true) AS \"query_dim\"")
@@ -468,7 +467,6 @@ function createIcebergResolverAdapter(options = {}) {
468
467
  includeSiteId: true,
469
468
  includeSearchType: true,
470
469
  tableLabel: "iceberg-resolver-adapter",
471
- canonicalFallback: options.canonicalFallback ?? false,
472
470
  queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
473
471
  tableRef: (tk) => sql.raw(`"${tk}"`),
474
472
  queryDimTableRef: () => sql.raw("\"query_dim\"")
@@ -481,7 +479,6 @@ function createR2SqlResolverAdapter(options = {}) {
481
479
  includeSiteId: true,
482
480
  includeSearchType: true,
483
481
  tableLabel: "r2-sql-resolver-adapter",
484
- canonicalFallback: options.canonicalFallback ?? false,
485
482
  queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
486
483
  capabilities: {
487
484
  regex: false,
@@ -822,65 +819,67 @@ function buildExtrasQueries(state, options) {
822
819
  function mergeExtras(rows, extrasResults) {
823
820
  if (extrasResults.length === 0) return rows;
824
821
  const lookups = [];
822
+ const parseVariants = (raw) => {
823
+ if (raw.length === 0) return [];
824
+ const encoded = raw.split("||");
825
+ const variants = [];
826
+ for (const value of encoded) {
827
+ if (value.length === 0) continue;
828
+ const parts = value.split(":::");
829
+ variants.push({
830
+ query: parts[0],
831
+ clicks: Number(parts[1] || 0),
832
+ impressions: Number(parts[2] || 0),
833
+ position: Number(parts[3] || 0)
834
+ });
835
+ }
836
+ return variants;
837
+ };
825
838
  for (const { key, results } of extrasResults) {
826
839
  if (key === "canonicalExtras") {
827
- const variantCountMap = /* @__PURE__ */ new Map();
828
- const variantsMap = /* @__PURE__ */ new Map();
829
- const canonicalNameMap = /* @__PURE__ */ new Map();
840
+ const map = /* @__PURE__ */ new Map();
830
841
  for (const r of results) {
831
842
  const jk = String(r.joinKey);
832
- variantCountMap.set(jk, r.variantCount);
833
- canonicalNameMap.set(jk, r.canonicalName);
834
843
  const raw = r.variants;
835
- variantsMap.set(jk, typeof raw === "string" ? raw.split("||").filter(Boolean).map((v) => {
836
- const parts = v.split(":::");
837
- return {
838
- query: parts[0],
839
- clicks: Number(parts[1] || 0),
840
- impressions: Number(parts[2] || 0),
841
- position: Number(parts[3] || 0)
842
- };
843
- }) : []);
844
+ map.set(jk, {
845
+ variantCount: r.variantCount,
846
+ variants: typeof raw === "string" ? parseVariants(raw) : [],
847
+ canonicalName: r.canonicalName
848
+ });
844
849
  }
845
850
  lookups.push({
846
- key: "variantCount",
847
- map: variantCountMap
848
- });
849
- lookups.push({
850
- key: "variants",
851
- map: variantsMap
852
- });
853
- lookups.push({
854
- key: "canonicalName",
855
- map: canonicalNameMap
851
+ kind: "canonical",
852
+ map
856
853
  });
857
854
  continue;
858
855
  }
859
- const filtered = results.filter((r) => r.rn === void 0 || r.rn === 1);
860
856
  const map = /* @__PURE__ */ new Map();
861
- for (const r of filtered) {
857
+ for (const r of results) {
858
+ if (r.rn !== void 0 && r.rn !== 1) continue;
862
859
  let val = r[key];
863
- if (key === "variants" && typeof val === "string") val = val.split("||").filter(Boolean).map((v) => {
864
- const parts = v.split(":::");
865
- return {
866
- query: parts[0],
867
- clicks: Number(parts[1] || 0),
868
- impressions: Number(parts[2] || 0),
869
- position: Number(parts[3] || 0)
870
- };
871
- });
860
+ if (key === "variants" && typeof val === "string") val = parseVariants(val);
872
861
  map.set(String(r.joinKey), val);
873
862
  }
874
863
  lookups.push({
864
+ kind: "generic",
875
865
  key,
876
866
  map
877
867
  });
878
868
  }
879
869
  return rows.map((row) => {
880
870
  const enriched = { ...row };
881
- for (const { key, map } of lookups) {
882
- let joinValue;
883
- if (key === "variantCount" || key === "variants" || key === "canonicalName") joinValue = String(row.queryCanonical ?? row.query_canonical ?? "");
871
+ for (const lookup of lookups) {
872
+ if (lookup.kind === "canonical") {
873
+ const joinValue = String(row.queryCanonical ?? row.query_canonical ?? "");
874
+ const extra = joinValue ? lookup.map.get(joinValue) : void 0;
875
+ enriched.variantCount = extra?.variantCount ?? null;
876
+ enriched.variants = extra?.variants ?? [];
877
+ enriched.canonicalName = extra?.canonicalName ?? null;
878
+ if (enriched.canonicalName) enriched.queryCanonical = enriched.canonicalName;
879
+ continue;
880
+ }
881
+ const { key, map } = lookup;
882
+ const joinValue = key === "variantCount" || key === "variants" || key === "canonicalName" ? String(row.queryCanonical ?? row.query_canonical ?? "") : void 0;
884
883
  enriched[key] = (joinValue && map.get(joinValue)) ?? (key === "variants" ? [] : null);
885
884
  if (key === "canonicalName" && enriched[key]) enriched.queryCanonical = enriched[key];
886
885
  }
@@ -955,7 +954,10 @@ function matchesMetricFilter(row, filter) {
955
954
  }
956
955
  }
957
956
  function matchesTopLevelPage(row) {
958
- return (normalizeUrl(dimensionValue(row, "page")).match(/\//g)?.length ?? 0) <= 1;
957
+ const path = normalizeUrl(dimensionValue(row, "page"));
958
+ let slashes = 0;
959
+ for (let index = 0; index < path.length; index++) if (path.charCodeAt(index) === 47 && ++slashes > 1) return false;
960
+ return true;
959
961
  }
960
962
  var QuerySourceCoverageError = class extends Error {
961
963
  fallback;
@@ -2115,7 +2115,6 @@ declare const TABLE_METADATA: Record<TableName, {
2115
2115
  }>;
2116
2116
  declare const SCHEMAS: Record<TableName, TableSchema>;
2117
2117
  declare function currentSchemaVersion(table: TableName): number;
2118
- declare function schemaFor(table: TableName): TableSchema;
2119
2118
  /**
2120
2119
  * DATE column names for a table. The single schema-derived source every read
2121
2120
  * path uses to build the legacy-VARCHAR date canonicalization (see
@@ -2146,4 +2145,4 @@ declare function naturalKeyColumns(table: TableName): readonly string[];
2146
2145
  */
2147
2146
  declare function dedupeByNaturalKey(table: TableName, rows: readonly Row[]): Row[];
2148
2147
  declare function dimensionToColumn(dim: string, _table: TableName): string;
2149
- export { type ColumnDef$1 as ColumnDef, type ColumnType, DrizzleSchema, SCHEMAS, TABLE_METADATA, type TableSchema$1 as TableSchema, allTables, countries, currentSchemaVersion, dateColumnsFor, dates, dedupeByNaturalKey, dimensionToColumn, drizzleSchema, hourly_pages, inferTable, naturalKeyColumns, page_queries, pages, queries, schemaFor, search_appearance, search_appearance_page_queries, search_appearance_pages, search_appearance_queries };
2148
+ export { type ColumnDef$1 as ColumnDef, type ColumnType, DrizzleSchema, SCHEMAS, TABLE_METADATA, type TableSchema$1 as TableSchema, allTables, countries, currentSchemaVersion, dateColumnsFor, dates, dedupeByNaturalKey, dimensionToColumn, drizzleSchema, hourly_pages, inferTable, naturalKeyColumns, page_queries, pages, queries, search_appearance, search_appearance_page_queries, search_appearance_pages, search_appearance_queries };
@@ -219,9 +219,6 @@ const SCHEMAS = Object.fromEntries(METRIC_TABLES.map((t) => [t, tableSchemaFrom(
219
219
  function currentSchemaVersion(table) {
220
220
  return SCHEMAS[table].version;
221
221
  }
222
- function schemaFor(table) {
223
- return SCHEMAS[table];
224
- }
225
222
  function dateColumnsFor(table) {
226
223
  return SCHEMAS[table].columns.filter((c) => c.type === "DATE").map((c) => c.name);
227
224
  }
@@ -259,4 +256,4 @@ function dimensionToColumn(dim, _table) {
259
256
  if (dim === "queryCanonical") return "query_canonical";
260
257
  return dim;
261
258
  }
262
- export { SCHEMAS, TABLE_METADATA, allTables, countries, currentSchemaVersion, dateColumnsFor, dates, dedupeByNaturalKey, dimensionToColumn, drizzleSchema, hourly_pages, inferTable, naturalKeyColumns, page_queries, pages, queries, schemaFor, search_appearance, search_appearance_page_queries, search_appearance_pages, search_appearance_queries };
259
+ export { SCHEMAS, TABLE_METADATA, allTables, countries, currentSchemaVersion, dateColumnsFor, dates, dedupeByNaturalKey, dimensionToColumn, drizzleSchema, hourly_pages, inferTable, naturalKeyColumns, page_queries, pages, queries, search_appearance, search_appearance_page_queries, search_appearance_pages, search_appearance_queries };
@@ -137,21 +137,7 @@ async function runAnalyzerWithEngine(deps, ctx, params, registry) {
137
137
  searchType: params.searchType ?? "web"
138
138
  }), params, registry);
139
139
  }
140
- function typedQuery(state) {
141
- return { state };
142
- }
143
- function isTypedQuery(value) {
144
- return "state" in value;
145
- }
146
- async function queryRows(source, query) {
147
- const state = isTypedQuery(query) ? query.state : query;
140
+ async function queryRows(source, state) {
148
141
  return await source.queryRows(state);
149
142
  }
150
- async function queryComparisonRows(source, current, previous) {
151
- const [currentRows, previousRows] = await Promise.all([queryRows(source, current), queryRows(source, previous)]);
152
- return {
153
- current: currentRows,
154
- previous: previousRows
155
- };
156
- }
157
- export { AttachedTableMissingError, ENGINE_QUERY_CAPABILITIES, createAttachedTableSource, createEngineQuerySource, createSqlQuerySource, queryComparisonRows, queryRows, rewriteForTableSource, runAnalyzerWithEngine, typedQuery };
143
+ export { AttachedTableMissingError, ENGINE_QUERY_CAPABILITIES, createAttachedTableSource, createEngineQuerySource, createSqlQuerySource, queryRows, rewriteForTableSource, runAnalyzerWithEngine };
@@ -2,31 +2,71 @@ import { manifestEntryKey, matchesManifestEntryFilter, matchesSyncStateFilter, m
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { randomBytes } from "node:crypto";
5
- import { mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
5
+ import { mkdir, open, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
6
6
  import { lock } from "proper-lockfile";
7
7
  function createFilesystemDataSource(opts) {
8
8
  const root = resolve(opts.rootDir);
9
+ const readyDirectories = /* @__PURE__ */ new Map();
9
10
  function pathFor(key) {
10
11
  const resolved = resolve(root, key);
11
12
  if (!resolved.startsWith(`${root}/`) && resolved !== root) throw new Error(`path escapes root: ${key}`);
12
13
  return resolved;
13
14
  }
15
+ async function ensureDirectory(path) {
16
+ let pending = readyDirectories.get(path);
17
+ if (!pending) {
18
+ pending = mkdir(path, { recursive: true }).then(() => void 0);
19
+ readyDirectories.set(path, pending);
20
+ pending.catch(() => readyDirectories.delete(path));
21
+ }
22
+ await pending;
23
+ }
14
24
  return {
15
25
  async read(key, range, signal) {
16
- const bytes = await readFile(pathFor(key), { signal });
17
- if (!range) return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
18
- const sliced = bytes.subarray(range.offset, range.offset + range.length);
19
- return new Uint8Array(sliced.buffer, sliced.byteOffset, sliced.byteLength);
26
+ const path = pathFor(key);
27
+ if (!range) {
28
+ const bytes = await readFile(path, { signal });
29
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
30
+ }
31
+ signal?.throwIfAborted();
32
+ const handle = await open(path, "r");
33
+ try {
34
+ const bytes = Buffer.allocUnsafe(range.length);
35
+ let totalRead = 0;
36
+ while (totalRead < range.length) {
37
+ signal?.throwIfAborted();
38
+ const { bytesRead } = await handle.read(bytes, totalRead, range.length - totalRead, range.offset + totalRead);
39
+ if (bytesRead === 0) break;
40
+ totalRead += bytesRead;
41
+ }
42
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, totalRead);
43
+ } finally {
44
+ await handle.close();
45
+ }
20
46
  },
21
47
  async write(key, bytes) {
22
48
  const path = pathFor(key);
23
- await mkdir(dirname(path), { recursive: true });
24
- await writeFile(path, Buffer.from(bytes));
49
+ const dir = dirname(path);
50
+ await ensureDirectory(dir);
51
+ try {
52
+ await writeFile(path, bytes);
53
+ } catch (error) {
54
+ if (error.code !== "ENOENT") throw error;
55
+ readyDirectories.delete(dir);
56
+ await ensureDirectory(dir);
57
+ await writeFile(path, bytes);
58
+ }
25
59
  },
26
60
  async delete(keys) {
27
- await Promise.all(keys.map(async (k) => {
28
- await rm(pathFor(k), { force: true });
29
- }));
61
+ let next = 0;
62
+ async function worker() {
63
+ while (true) {
64
+ const index = next++;
65
+ if (index >= keys.length) return;
66
+ await rm(pathFor(keys[index]), { force: true });
67
+ }
68
+ }
69
+ await Promise.all(Array.from({ length: Math.min(32, keys.length) }, worker));
30
70
  },
31
71
  async list(prefix) {
32
72
  const full = pathFor(prefix);
@@ -61,14 +101,23 @@ async function* walkStream(dir) {
61
101
  }
62
102
  }
63
103
  async function walk(dir, out) {
64
- const entries = await readdir(dir, { withFileTypes: true }).catch((err) => {
65
- if (err.code === "ENOENT") return [];
66
- throw err;
67
- });
68
- for (const entry of entries) {
69
- const p = join(dir, String(entry.name));
70
- if (entry.isDirectory()) await walk(p, out);
71
- else out.push(p);
104
+ const pending = [dir];
105
+ let next = 0;
106
+ while (next < pending.length) {
107
+ const batch = pending.slice(next, next + 32);
108
+ next += batch.length;
109
+ const listings = await Promise.all(batch.map(async (current) => ({
110
+ current,
111
+ entries: await readdir(current, { withFileTypes: true }).catch((err) => {
112
+ if (err.code === "ENOENT") return [];
113
+ throw err;
114
+ })
115
+ })));
116
+ for (const { current, entries } of listings) for (const entry of entries) {
117
+ const p = join(current, String(entry.name));
118
+ if (entry.isDirectory()) pending.push(p);
119
+ else out.push(p);
120
+ }
72
121
  }
73
122
  }
74
123
  function lockFileFor(locksDir, scope) {
@@ -77,6 +126,16 @@ function lockFileFor(locksDir, scope) {
77
126
  function createFilesystemManifestStore(opts) {
78
127
  const manifestPath = resolve(opts.path);
79
128
  const locksDir = join(dirname(manifestPath), "locks");
129
+ const readyDirectories = /* @__PURE__ */ new Map();
130
+ async function ensureDirectory(path) {
131
+ let pending = readyDirectories.get(path);
132
+ if (!pending) {
133
+ pending = mkdir(path, { recursive: true }).then(() => void 0);
134
+ readyDirectories.set(path, pending);
135
+ pending.catch(() => readyDirectories.delete(path));
136
+ }
137
+ await pending;
138
+ }
80
139
  async function load() {
81
140
  const content = await readFile(manifestPath, "utf8").catch((err) => {
82
141
  if (err.code === "ENOENT") return null;
@@ -91,9 +150,16 @@ function createFilesystemManifestStore(opts) {
91
150
  return parsed;
92
151
  }
93
152
  async function save(data) {
94
- await mkdir(dirname(manifestPath), { recursive: true });
153
+ const manifestDir = dirname(manifestPath);
154
+ await ensureDirectory(manifestDir);
95
155
  const tmp = `${manifestPath}.${randomBytes(6).toString("hex")}.tmp`;
96
- await writeFile(tmp, JSON.stringify(data), "utf8");
156
+ const content = JSON.stringify(data);
157
+ await writeFile(tmp, content, "utf8").catch(async (error) => {
158
+ if (error.code !== "ENOENT") throw error;
159
+ readyDirectories.delete(manifestDir);
160
+ await ensureDirectory(manifestDir);
161
+ await writeFile(tmp, content, "utf8");
162
+ });
97
163
  await rename(tmp, manifestPath).catch(async (err) => {
98
164
  try {
99
165
  await unlink(tmp);
@@ -175,9 +241,14 @@ function createFilesystemManifestStore(opts) {
175
241
  });
176
242
  },
177
243
  async withLock(scope, fn) {
178
- await mkdir(locksDir, { recursive: true });
244
+ await ensureDirectory(locksDir);
179
245
  const path = lockFileFor(locksDir, scope);
180
- await writeFile(path, "", { flag: "a" });
246
+ await writeFile(path, "", { flag: "a" }).catch(async (error) => {
247
+ if (error.code !== "ENOENT") throw error;
248
+ readyDirectories.delete(locksDir);
249
+ await ensureDirectory(locksDir);
250
+ await writeFile(path, "", { flag: "a" });
251
+ });
181
252
  const release = await lock(path, {
182
253
  realpath: false,
183
254
  stale: 3e4,
@@ -244,24 +315,22 @@ async function filesystemStats(rootDir) {
244
315
  const keys = [];
245
316
  await walkForStats(resolve(rootDir), keys);
246
317
  let bytes = 0;
247
- for (const k of keys) {
248
- const s = await stat(k);
249
- bytes += s.size;
318
+ let next = 0;
319
+ async function worker() {
320
+ while (true) {
321
+ const index = next++;
322
+ if (index >= keys.length) return;
323
+ const s = await stat(keys[index]);
324
+ bytes += s.size;
325
+ }
250
326
  }
327
+ await Promise.all(Array.from({ length: Math.min(32, keys.length) }, worker));
251
328
  return {
252
329
  files: keys.length,
253
330
  bytes
254
331
  };
255
332
  }
256
333
  async function walkForStats(dir, out) {
257
- const entries = await readdir(dir, { withFileTypes: true }).catch((err) => {
258
- if (err.code === "ENOENT") return [];
259
- throw err;
260
- });
261
- for (const entry of entries) {
262
- const p = join(dir, String(entry.name));
263
- if (entry.isDirectory()) await walkForStats(p, out);
264
- else out.push(p);
265
- }
334
+ await walk(dir, out);
266
335
  }
267
336
  export { createFilesystemDataSource, createFilesystemManifestStore, filesystemStats };
@@ -45,6 +45,11 @@ interface HyparquetCodecOptions {
45
45
  * hyparquet to avoid WASM linear-memory growth. Defaults to hyparquet.
46
46
  */
47
47
  readRows?: (ctx: CodecCtx, key: string, dataSource: DataSource) => Promise<Row[]>;
48
+ /**
49
+ * Maximum input-file reads held in flight during compaction. Defaults to 8;
50
+ * lower it for unusually large files to trade latency for peak JS memory.
51
+ */
52
+ compactionReadConcurrency?: number;
48
53
  }
49
54
  declare function createHyparquetCodec(options?: HyparquetCodecOptions): ParquetCodec;
50
55
  export { DecodeParquetOptions, EncodeFlexOptions, HyparquetCodecOptions, createHyparquetCodec, decodeParquetToRows, encodeRowsToParquet, encodeRowsToParquetFlex };
@@ -2,6 +2,8 @@ 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;
5
+ const DEFAULT_COMPACTION_READ_CONCURRENCY = 8;
6
+ const MAX_COMPACTION_READ_CONCURRENCY = 32;
5
7
  function basicTypeFor(colType) {
6
8
  if (colType === "VARCHAR") return "STRING";
7
9
  if (colType === "BIGINT") return "INT64";
@@ -120,7 +122,13 @@ function sortRowsByClusterKey(table, rows) {
120
122
  return copy;
121
123
  }
122
124
  function naturalKeyFor(table, row) {
123
- return TABLE_METADATA[table].sortKey.map((col) => `${row[col] ?? ""}`).join("\0");
125
+ const key = TABLE_METADATA[table].sortKey;
126
+ let value = "";
127
+ for (let index = 0; index < key.length; index++) {
128
+ if (index > 0) value += "\0";
129
+ value += `${row[key[index]] ?? ""}`;
130
+ }
131
+ return value;
124
132
  }
125
133
  function encodeOrderedRows(rows, columns, rowGroupSize) {
126
134
  const schema = buildWriteSchema(columns);
@@ -204,6 +212,11 @@ function normalizeDecodedDates(rows) {
204
212
  return rows;
205
213
  }
206
214
  function createHyparquetCodec(options = {}) {
215
+ const readRows = options.readRows ?? (async (_ctx, key, dataSource) => {
216
+ return decodeParquetToRows(await dataSource.read(key));
217
+ });
218
+ const requestedConcurrency = options.compactionReadConcurrency;
219
+ const compactionReadConcurrency = typeof requestedConcurrency === "number" && Number.isFinite(requestedConcurrency) ? Math.max(1, Math.min(MAX_COMPACTION_READ_CONCURRENCY, Math.floor(requestedConcurrency))) : DEFAULT_COMPACTION_READ_CONCURRENCY;
207
220
  return {
208
221
  async writeRows(ctx, rows, key, dataSource) {
209
222
  const bytes = encodeRowsToParquet(ctx.table, rows);
@@ -213,9 +226,7 @@ function createHyparquetCodec(options = {}) {
213
226
  rowCount: rows.length
214
227
  };
215
228
  },
216
- readRows: options.readRows ?? (async (_ctx, key, dataSource) => {
217
- return decodeParquetToRows(await dataSource.read(key));
218
- }),
229
+ readRows,
219
230
  async compactRows(ctx, inputKeys, outputKey, dataSource) {
220
231
  if (inputKeys.length === 0) {
221
232
  const bytes = encodeRowsToParquet(ctx.table, []);
@@ -226,9 +237,13 @@ function createHyparquetCodec(options = {}) {
226
237
  };
227
238
  }
228
239
  const byNaturalKey = /* @__PURE__ */ new Map();
229
- for (const key of inputKeys) {
230
- const rows = await decodeParquetToRows(await dataSource.read(key));
231
- for (let i = 0; i < rows.length; i++) byNaturalKey.set(naturalKeyFor(ctx.table, rows[i]), rows[i]);
240
+ for (let offset = 0; offset < inputKeys.length; offset += compactionReadConcurrency) {
241
+ const batch = inputKeys.slice(offset, offset + compactionReadConcurrency);
242
+ const inputs = await Promise.all(batch.map((key) => dataSource.read(key)));
243
+ for (const input of inputs) {
244
+ const rows = await decodeParquetToRows(input);
245
+ for (let i = 0; i < rows.length; i++) byNaturalKey.set(naturalKeyFor(ctx.table, rows[i]), rows[i]);
246
+ }
232
247
  }
233
248
  const rows = [...byNaturalKey.values()];
234
249
  const bytes = encodeRowsToParquet(ctx.table, rows);
@@ -6,6 +6,7 @@ const SHARD_RE = /^u_[^/]+\/manifest\/(?<siteId>[^/]+)\/(?<table>[^/]+)\/HEAD$/;
6
6
  const CAS_BACKOFF_BASE_MS = 5;
7
7
  const CAS_BACKOFF_CAP_MS = 250;
8
8
  const SHARD_IO_CONCURRENCY = 8;
9
+ const R2_DELETE_BATCH_SIZE = 1e3;
9
10
  async function casBackoff(attempt) {
10
11
  const ceil = Math.min(CAS_BACKOFF_CAP_MS, CAS_BACKOFF_BASE_MS * 2 ** attempt);
11
12
  await new Promise((resolve) => setTimeout(resolve, Math.random() * ceil));
@@ -331,15 +332,8 @@ function createR2ManifestStore(opts) {
331
332
  },
332
333
  async purgeTenant(filter) {
333
334
  if (filter.userId !== userId) throw new Error(`purgeTenant: store is scoped to userId=${userId}, got ${filter.userId}`);
334
- const shards = await shardsForFilter({ siteId: filter.siteId });
335
- let entriesRemoved = 0;
336
- let watermarksRemoved = 0;
337
- let syncStatesRemoved = 0;
338
- for (const { siteId, table } of shards) {
335
+ const purged = await mapWithConcurrency(await shardsForFilter({ siteId: filter.siteId }), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
339
336
  const { snapshot } = await readShard(siteId, table);
340
- entriesRemoved += snapshot.entries.length;
341
- watermarksRemoved += snapshot.watermarks.length;
342
- syncStatesRemoved += snapshot.syncStates.length;
343
337
  const prefix = shardPrefix(userId, siteId, table);
344
338
  const keys = [];
345
339
  let cursor;
@@ -352,12 +346,17 @@ function createR2ManifestStore(opts) {
352
346
  for (const obj of res.objects) keys.push(obj.key);
353
347
  cursor = res.truncated ? res.cursor : void 0;
354
348
  } while (cursor);
355
- if (keys.length > 0) await bucket.delete(keys);
356
- }
349
+ for (let offset = 0; offset < keys.length; offset += R2_DELETE_BATCH_SIZE) await bucket.delete(keys.slice(offset, offset + R2_DELETE_BATCH_SIZE));
350
+ return {
351
+ entriesRemoved: snapshot.entries.length,
352
+ watermarksRemoved: snapshot.watermarks.length,
353
+ syncStatesRemoved: snapshot.syncStates.length
354
+ };
355
+ });
357
356
  return {
358
- entriesRemoved,
359
- watermarksRemoved,
360
- syncStatesRemoved
357
+ entriesRemoved: purged.reduce((sum, result) => sum + result.entriesRemoved, 0),
358
+ watermarksRemoved: purged.reduce((sum, result) => sum + result.watermarksRemoved, 0),
359
+ syncStatesRemoved: purged.reduce((sum, result) => sum + result.syncStatesRemoved, 0)
361
360
  };
362
361
  }
363
362
  };
@@ -1,5 +1,7 @@
1
1
  const BUCKET_NAME_RE = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/;
2
2
  const KEY_RE = /^[\w./=-]+$/;
3
+ const DELETE_CHUNK_SIZE = 1e3;
4
+ const DELETE_CONCURRENCY = 4;
3
5
  function assertKey(key) {
4
6
  if (!KEY_RE.test(key)) throw new Error(`createR2DataSource: refusing unsafe key ${JSON.stringify(key)}`);
5
7
  }
@@ -22,8 +24,17 @@ function createR2DataSource(options) {
22
24
  },
23
25
  async delete(keys) {
24
26
  if (keys.length === 0) return;
25
- const CHUNK = 1e3;
26
- for (let i = 0; i < keys.length; i += CHUNK) await bucket.delete(keys.slice(i, i + CHUNK));
27
+ let next = 0;
28
+ async function worker() {
29
+ while (true) {
30
+ const offset = next;
31
+ next += DELETE_CHUNK_SIZE;
32
+ if (offset >= keys.length) return;
33
+ await bucket.delete(keys.slice(offset, offset + DELETE_CHUNK_SIZE));
34
+ }
35
+ }
36
+ const chunkCount = Math.ceil(keys.length / DELETE_CHUNK_SIZE);
37
+ await Promise.all(Array.from({ length: Math.min(DELETE_CONCURRENCY, chunkCount) }, worker));
27
38
  },
28
39
  async list(prefix) {
29
40
  const out = [];
package/dist/errors.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { EngineError, EngineErrorKind, engineErrorToException, engineErrors, formatEngineError, isEngineError } from "./_chunks/errors.mjs";
2
- export { EngineError, EngineErrorKind, engineErrorToException, engineErrors, formatEngineError, isEngineError };
1
+ import { EngineError, EngineErrorKind, engineErrorToException, engineErrors, isEngineError } from "./_chunks/errors.mjs";
2
+ export { EngineError, EngineErrorKind, engineErrorToException, engineErrors, isEngineError };
package/dist/errors.mjs CHANGED
@@ -190,13 +190,10 @@ const ENGINE_ERROR_KINDS = /* @__PURE__ */ new Set([
190
190
  function isEngineError(value) {
191
191
  return typeof value === "object" && value !== null && ENGINE_ERROR_KINDS.has(value.kind) && typeof value.message === "string";
192
192
  }
193
- function formatEngineError(error) {
194
- return error.message;
195
- }
196
193
  function engineErrorToException(error) {
197
194
  const exception = new Error(error.message);
198
195
  if ("cause" in error && error.cause !== void 0) exception.cause = error.cause;
199
196
  exception.engineError = error;
200
197
  return exception;
201
198
  }
202
- export { engineErrorToException, engineErrors, formatEngineError, isEngineError };
199
+ export { engineErrorToException, engineErrors, isEngineError };
@@ -137,8 +137,12 @@ function dedupeByIdentity(table, records) {
137
137
  const key = gscDataset(table, "int").tableSpec.identityColumns;
138
138
  const seen = /* @__PURE__ */ new Map();
139
139
  for (const rec of records) {
140
- const k = key.map((col) => `${rec[col] ?? ""}`).join("\0");
141
- seen.set(k, rec);
140
+ let identity = "";
141
+ for (let index = 0; index < key.length; index++) {
142
+ if (index > 0) identity += "\0";
143
+ identity += `${rec[key[index]] ?? ""}`;
144
+ }
145
+ seen.set(identity, rec);
142
146
  }
143
147
  return seen.size === records.length ? records : [...seen.values()];
144
148
  }
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CodecCtx, CompactionThresholds, CompactionTier, DataSource, EngineOptions, FileSetRef, GcCtx, Grain, ListLiveFilter, LockScope, ManifestEntry, ManifestPurgeResult, ManifestStore, ParquetCodec, PurgeFilter, PurgeResult, PurgeUrlsResult, QueryCtx, QueryExecuteOptions, QueryExecuteResult, QueryExecutor, QueryProfiler, QueryResult, QuerySpan, Row, RunSQLOptions, SearchType, StorageEngine, SyncState, SyncStateDetail, SyncStateFilter, SyncStateKind, SyncStateScope, TableName, TenantCtx, Watermark, WatermarkFilter, WatermarkScope, WriteCtx, WriteResult, enumeratePartitions } from "./_chunks/storage.mjs";
2
2
  import { DuckDBFactory, DuckDBHandle, canonicalEmptyParquetSchema, createDuckDBCodec, createDuckDBExecutor } from "./_chunks/duckdb.mjs";
3
3
  import { ColumnDef, ColumnType, DrizzleSchema, SCHEMAS, TABLE_METADATA, TableSchema, allTables, countries, currentSchemaVersion, dates, dimensionToColumn, drizzleSchema, hourly_pages, inferTable, page_queries, pages, queries } from "./_chunks/schema.mjs";
4
- import { EngineError, EngineErrorKind, engineErrorToException, engineErrors, formatEngineError, isEngineError } from "./_chunks/errors.mjs";
4
+ import { EngineError, EngineErrorKind, engineErrorToException, engineErrors, isEngineError } from "./_chunks/errors.mjs";
5
5
  import { InspectionVerdict, SchedulePolicy, ScheduleState, fixedPolicy, inspectionPolicy, sitemapPolicy } from "./schedule.mjs";
6
6
  import { IcebergTableName, Sink, SinkCapabilities, SinkCloseResult, SinkOptions, SinkSlice, SinkWriteResult } from "./_chunks/sink.mjs";
7
7
  import { GscApiRow, IngestOptions, RowAccumulator, RowAccumulatorOptions, assembleDatesRow, createRowAccumulator, toPath, toSumPosition, transformGscRow } from "./ingest.mjs";
@@ -109,7 +109,6 @@ interface CreateIngestAccumulatorOptions extends RowAccumulatorOptions {
109
109
  ctx: IngestAccumulatorCtx;
110
110
  hooks: IngestAccumulatorHooks;
111
111
  }
112
- declare function createNoopIngestAccumulator(): IngestAccumulator;
113
112
  declare function createIngestAccumulator(opts: CreateIngestAccumulatorOptions): IngestAccumulator;
114
113
  declare function dayPartition(date: string): string;
115
114
  /**
@@ -209,4 +208,4 @@ declare const MIN_SYNC_IMPRESSIONS = 1;
209
208
  declare const MIN_COUNTRY_IMPRESSIONS = 10;
210
209
  declare const MAX_SITEMAP_URLS_PER_SITE = 50000;
211
210
  declare const MAX_TRACKED_URLS_PER_SITE = 200000;
212
- export { type CodecCtx, type ColumnDef, type ColumnType, type CompactionThresholds, type CompactionTier, type CreateIngestAccumulatorOptions, DEFAULT_SEARCH_TYPE, type DataSource, type DateWeight, type DrizzleSchema, type DuckDBFactory, type DuckDBHandle, ENGINE_QUERY_CAPABILITIES, EngineError, EngineErrorKind, type EngineOptions, FILES_PLACEHOLDER, type FileSetRef, type FinalizeOptions, type FinalizeResult, type GcCtx, type Grain, type GscApiRow, type InMemorySink, type IngestAccumulator, type IngestAccumulatorCtx, type IngestAccumulatorEngine, type IngestAccumulatorHooks, type IngestOptions, type InspectionVerdict, type ListLiveFilter, type LockScope, MAX_DAY_BYTES, MAX_GSC_PAGES_R2, MAX_SITEMAP_URLS_PER_SITE, MAX_TRACKED_URLS_PER_SITE, MIN_COUNTRY_IMPRESSIONS, MIN_SYNC_IMPRESSIONS, type ManifestEntry, type ManifestPurgeResult, type ManifestStore, type ParquetCodec, type PurgeFilter, type PurgeResult, type PurgeUrlsResult, type QueryCtx, type QueryExecuteOptions, type QueryExecuteResult, type QueryExecutor, type QueryProfiler, type QueryResult, type QuerySpan, ROW_LIMIT_R2, type ResolvedQuery, type Row, type RowAccumulator, type RowAccumulatorOptions, type RunSQLOptions, SCHEMAS, type SchedulePolicy, type ScheduleState, type SearchType, type Sink, type SinkCapabilities, type SinkCloseResult, type SinkOptions, type SinkSlice, type SinkWriteResult, type StorageEngine, type StoredRow, type SyncState, type SyncStateDetail, type SyncStateFilter, type SyncStateKind, type SyncStateScope, type SyncTableName, TABLES_BY_SEARCH_TYPE, TABLE_METADATA, TABLE_TIERS, TIER_PRIORITY, type TableName, type TableSchema, type TableTier, type TenantCtx, type TieredTableName, WEIGHT_PRIORITY, type Watermark, type WatermarkFilter, type WatermarkScope, type WriteCtx, type WriteResult, allTables, assembleDatesRow, bindLiterals, canonicalEmptyParquetSchema, coerceRow, coerceRows, collectSpans, countries, createDuckDBCodec, createDuckDBExecutor, createIcebergResolverAdapter, createInMemorySink, createIngestAccumulator, createNoopIngestAccumulator, createParquetResolverAdapter, createQueryProfiler, createR2SqlResolverAdapter, createRowAccumulator, createSqlQuerySource, createStorageEngine, currentSchemaVersion, dates, dayPartition, dimensionToColumn, drizzleSchema, engineErrorToException, engineErrors, enumeratePartitions, fixedPolicy, formatEngineError, formatLiteral, getDateWeight, getTableTier, getTablesForTier, hourPartition, hourly_pages, inferLegacyTier, inferSearchType, inferTable, inspectionPolicy, isEngineError, objectKey, page_queries, pages, parseEnabledSearchTypes, pgResolverAdapter, queries, rebuildDailyFromHourly, resolveParquetSQL, sitemapPolicy, substituteNamedFiles, toPath, toSumPosition, transformGscRow, validateEnabledSearchTypes, validateEnabledSearchTypesResult };
211
+ export { type CodecCtx, type ColumnDef, type ColumnType, type CompactionThresholds, type CompactionTier, type CreateIngestAccumulatorOptions, DEFAULT_SEARCH_TYPE, type DataSource, type DateWeight, type DrizzleSchema, type DuckDBFactory, type DuckDBHandle, ENGINE_QUERY_CAPABILITIES, EngineError, EngineErrorKind, type EngineOptions, FILES_PLACEHOLDER, type FileSetRef, type FinalizeOptions, type FinalizeResult, type GcCtx, type Grain, type GscApiRow, type InMemorySink, type IngestAccumulator, type IngestAccumulatorCtx, type IngestAccumulatorEngine, type IngestAccumulatorHooks, type IngestOptions, type InspectionVerdict, type ListLiveFilter, type LockScope, MAX_DAY_BYTES, MAX_GSC_PAGES_R2, MAX_SITEMAP_URLS_PER_SITE, MAX_TRACKED_URLS_PER_SITE, MIN_COUNTRY_IMPRESSIONS, MIN_SYNC_IMPRESSIONS, type ManifestEntry, type ManifestPurgeResult, type ManifestStore, type ParquetCodec, type PurgeFilter, type PurgeResult, type PurgeUrlsResult, type QueryCtx, type QueryExecuteOptions, type QueryExecuteResult, type QueryExecutor, type QueryProfiler, type QueryResult, type QuerySpan, ROW_LIMIT_R2, type ResolvedQuery, type Row, type RowAccumulator, type RowAccumulatorOptions, type RunSQLOptions, SCHEMAS, type SchedulePolicy, type ScheduleState, type SearchType, type Sink, type SinkCapabilities, type SinkCloseResult, type SinkOptions, type SinkSlice, type SinkWriteResult, type StorageEngine, type StoredRow, type SyncState, type SyncStateDetail, type SyncStateFilter, type SyncStateKind, type SyncStateScope, type SyncTableName, TABLES_BY_SEARCH_TYPE, TABLE_METADATA, TABLE_TIERS, TIER_PRIORITY, type TableName, type TableSchema, type TableTier, type TenantCtx, type TieredTableName, WEIGHT_PRIORITY, type Watermark, type WatermarkFilter, type WatermarkScope, type WriteCtx, type WriteResult, allTables, assembleDatesRow, bindLiterals, canonicalEmptyParquetSchema, coerceRow, coerceRows, collectSpans, countries, createDuckDBCodec, createDuckDBExecutor, createIcebergResolverAdapter, createInMemorySink, createIngestAccumulator, createParquetResolverAdapter, createQueryProfiler, 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 };