@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.
@@ -6,6 +6,20 @@ import { compileLogicalQueryPlan, substituteNamedFiles } from "./parquet-plan.mj
6
6
  import { sqlEscape } from "../sql-bind.mjs";
7
7
  import { buildLogicalPlan } from "gscdump/query/plan";
8
8
  import { normalizeUrl } from "gscdump/normalize";
9
+ const BUFFER_READ_BATCH_SIZE = 8;
10
+ async function registerBufferedFiles(db, files, dataSource, registered, signal) {
11
+ for (let i = 0; i < files.length; i += BUFFER_READ_BATCH_SIZE) {
12
+ signal?.throwIfAborted();
13
+ const batch = files.slice(i, i + BUFFER_READ_BATCH_SIZE);
14
+ const buffers = await Promise.all(batch.map((file) => dataSource.read(file.key, void 0, signal)));
15
+ for (let j = 0; j < batch.length; j++) {
16
+ signal?.throwIfAborted();
17
+ const file = batch[j];
18
+ await db.registerFileBuffer(file.name, buffers[j]);
19
+ registered.push(file.name);
20
+ }
21
+ }
22
+ }
9
23
  async function encodeBytes(db, table, rows) {
10
24
  const inName = db.makeTempPath("json");
11
25
  const outName = db.makeTempPath("parquet");
@@ -72,17 +86,19 @@ function createDuckDBCodec(factory) {
72
86
  await db.dropFiles([outName]);
73
87
  }
74
88
  }
75
- const inputs = await Promise.all(inputKeys.map((k) => dataSource.read(k)));
76
89
  const inNames = [];
77
90
  const outName = db.makeTempPath("parquet");
78
91
  const registered = [];
79
- for (let i = 0; i < inputs.length; i++) {
92
+ const bufferedInputs = inputKeys.map((key) => {
80
93
  const name = db.makeTempPath("parquet");
81
- await db.registerFileBuffer(name, inputs[i]);
82
94
  inNames.push(name);
83
- registered.push(name);
84
- }
95
+ return {
96
+ key,
97
+ name
98
+ };
99
+ });
85
100
  try {
101
+ await registerBufferedFiles(db, bufferedInputs, dataSource, registered);
86
102
  const fileList = inNames.map((n) => `'${sqlEscape(n)}'`).join(", ");
87
103
  await db.query(`COPY (${dedupedMergeSql(ctx.table, fileList)}) TO '${sqlEscape(outName)}' (FORMAT PARQUET, COMPRESSION ZSTD)`);
88
104
  registered.push(outName);
@@ -127,23 +143,26 @@ function createDuckDBExecutor(factory) {
127
143
  const registered = [];
128
144
  const totalFiles = Object.values(fileKeys).reduce((n, keys) => n + keys.length, 0);
129
145
  const endRegister = profiler?.start("files.register", { files: totalFiles });
130
- await Promise.all(Object.entries(fileKeys).map(async ([name, keys]) => {
131
- const uris = keys.map((key) => dataSource.uri?.(key));
132
- const buffers = await Promise.all(keys.map((key, i) => uris[i] !== void 0 ? Promise.resolve(void 0) : dataSource.read(key, void 0, signal)));
133
- const resolved = [];
134
- for (let i = 0; i < keys.length; i++) {
135
- const uri = uris[i];
136
- if (uri !== void 0) resolved.push(uri);
137
- else {
138
- await db.registerFileBuffer(keys[i], buffers[i]);
139
- registered.push(keys[i]);
140
- resolved.push(keys[i]);
141
- }
142
- }
143
- placeholders[name] = resolved;
144
- }));
145
- endRegister?.({ buffered: registered.length });
146
146
  try {
147
+ await Promise.all(Object.entries(fileKeys).map(async ([name, keys]) => {
148
+ const resolved = [];
149
+ const buffered = [];
150
+ for (let i = 0; i < keys.length; i++) {
151
+ const key = keys[i];
152
+ const uri = dataSource.uri?.(key);
153
+ if (uri !== void 0) resolved.push(uri);
154
+ else {
155
+ buffered.push({
156
+ key,
157
+ name: key
158
+ });
159
+ resolved.push(key);
160
+ }
161
+ }
162
+ await registerBufferedFiles(db, buffered, dataSource, registered, signal);
163
+ placeholders[name] = resolved;
164
+ }));
165
+ endRegister?.({ buffered: registered.length });
147
166
  signal?.throwIfAborted();
148
167
  const finalSql = substituteNamedFiles(rewriteEmptyFileSets(sql, placeholders, table, placeholderTables), placeholders);
149
168
  const endQuery = profiler?.start("query.run");
@@ -274,13 +293,23 @@ async function gcOrphansImpl(deps, now, graceMs, opts = {}) {
274
293
  }
275
294
  return { deleted: retired.length + sweptOrphans + hourlyDeleted };
276
295
  }
277
- const PUSHABLE_COLUMN = { query: "query" };
296
+ const PUSHABLE_COLUMN = {
297
+ country: "country",
298
+ query: "query",
299
+ searchAppearance: "searchAppearance"
300
+ };
278
301
  function txLeaf(leaf, columns) {
279
302
  if (leaf.operator !== "equals") return null;
280
303
  const column = PUSHABLE_COLUMN[leaf.dimension];
281
304
  if (!column || !columns.has(column)) return null;
282
305
  return { [column]: { $eq: leaf.expression } };
283
306
  }
307
+ function combineFilters(parts, groupType) {
308
+ const first = parts[0];
309
+ if (!first) return null;
310
+ if (parts.length === 1) return first;
311
+ return groupType === "or" ? { $or: [...parts] } : { $and: [...parts] };
312
+ }
284
313
  function txExact(node, columns) {
285
314
  const groupType = node._groupType ?? "and";
286
315
  const leafParts = [];
@@ -291,7 +320,7 @@ function txExact(node, columns) {
291
320
  }
292
321
  if (groupType === "or") {
293
322
  if (node._nestedGroups?.length || leafParts.length === 0) return null;
294
- return leafParts.length === 1 ? leafParts[0] : { $or: leafParts };
323
+ return combineFilters(leafParts, "or");
295
324
  }
296
325
  const parts = leafParts;
297
326
  for (const group of node._nestedGroups ?? []) {
@@ -300,7 +329,7 @@ function txExact(node, columns) {
300
329
  parts.push(t);
301
330
  }
302
331
  if (parts.length === 0) return null;
303
- return parts.length === 1 ? parts[0] : { $and: parts };
332
+ return combineFilters(parts, "and");
304
333
  }
305
334
  function extractParquetPushdown(state, table) {
306
335
  const filter = state?.filter;
@@ -318,7 +347,7 @@ function extractParquetPushdown(state, table) {
318
347
  if (t) parts.push(t);
319
348
  }
320
349
  if (parts.length === 0) return void 0;
321
- return parts.length === 1 ? parts[0] : { $and: parts };
350
+ return combineFilters(parts, "and") ?? void 0;
322
351
  }
323
352
  const URL_PURGE_TABLES = ["pages", "page_queries"];
324
353
  const MAX_DAY_BYTES = 100 * 1024 * 1024;
@@ -737,6 +737,7 @@ function createSitemapStore(opts) {
737
737
  const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
738
738
  if (!m) continue;
739
739
  const date = m[1];
740
+ if (!date) continue;
740
741
  if (from && date < from) continue;
741
742
  if (to && date > to) continue;
742
743
  const bytes = await readOptional(ds, key);
@@ -763,9 +764,11 @@ function createSitemapStore(opts) {
763
764
  for (const key of deltaKeys) {
764
765
  const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
765
766
  if (!m) continue;
766
- const list = deltasByFeed.get(m[2]) ?? [];
767
+ const feedpathHash = m[2];
768
+ if (!feedpathHash) continue;
769
+ const list = deltasByFeed.get(feedpathHash) ?? [];
767
770
  list.push(key);
768
- deltasByFeed.set(m[2], list);
771
+ deltasByFeed.set(feedpathHash, list);
769
772
  }
770
773
  for (const [fpHash, feedDeltaKeys] of deltasByFeed) {
771
774
  const indexKey = sitemapUrlsIndexKey(ctx, fpHash);
@@ -0,0 +1,56 @@
1
+ import { inferLegacyTier, inferSearchType } from "./layout.mjs";
2
+ function manifestEntryKey(entry) {
3
+ return entry.objectKey;
4
+ }
5
+ function watermarkKey(watermark) {
6
+ return `${watermark.userId}|${watermark.siteId ?? ""}|${watermark.table}`;
7
+ }
8
+ function syncStateKey(state) {
9
+ return `${state.userId}|${state.siteId ?? ""}|${state.table}|${state.date}|${inferSearchType(state)}`;
10
+ }
11
+ function matchesManifestEntryFilter(entry, filter, options = {}) {
12
+ if (!options.ignoreUserId && entry.userId !== filter.userId) return false;
13
+ if (filter.siteId !== void 0 && entry.siteId !== filter.siteId) return false;
14
+ if (filter.table !== void 0 && entry.table !== filter.table) return false;
15
+ if (filter.partitions && !filter.partitions.includes(entry.partition)) return false;
16
+ if (filter.tier !== void 0 && inferLegacyTier(entry) !== filter.tier) return false;
17
+ if (filter.searchType !== void 0 && inferSearchType(entry) !== filter.searchType) return false;
18
+ return true;
19
+ }
20
+ function matchesWatermarkFilter(watermark, filter, options = {}) {
21
+ if (!options.ignoreUserId && watermark.userId !== filter.userId) return false;
22
+ if (filter.siteId !== void 0 && watermark.siteId !== filter.siteId) return false;
23
+ if (filter.table !== void 0 && watermark.table !== filter.table) return false;
24
+ return true;
25
+ }
26
+ function matchesSyncStateFilter(state, filter, options = {}) {
27
+ if (!options.ignoreUserId && state.userId !== filter.userId) return false;
28
+ if (filter.siteId !== void 0 && state.siteId !== filter.siteId) return false;
29
+ if (filter.table !== void 0 && state.table !== filter.table) return false;
30
+ if (filter.state !== void 0 && state.state !== filter.state) return false;
31
+ if (filter.searchType !== void 0 && inferSearchType(state) !== filter.searchType) return false;
32
+ return true;
33
+ }
34
+ function mergeSyncState(existing, scope, state, detail) {
35
+ const at = detail?.at ?? Date.now();
36
+ const attemptsBump = state === "inflight" ? 1 : 0;
37
+ if (!existing) return {
38
+ userId: scope.userId,
39
+ siteId: scope.siteId,
40
+ table: scope.table,
41
+ date: scope.date,
42
+ state,
43
+ updatedAt: at,
44
+ attempts: attemptsBump,
45
+ error: detail?.error,
46
+ ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {}
47
+ };
48
+ return {
49
+ ...existing,
50
+ state,
51
+ updatedAt: at,
52
+ attempts: existing.attempts + attemptsBump,
53
+ error: state === "done" ? void 0 : detail?.error ?? existing.error
54
+ };
55
+ }
56
+ export { manifestEntryKey, matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter, mergeSyncState, syncStateKey, watermarkKey };
@@ -15,11 +15,25 @@ declare const pgResolverAdapter: ResolverAdapter<PgTableKey>;
15
15
  */
16
16
  interface ResolverAdapterOptions {
17
17
  /**
18
- * Opt-in canonical-primary correctness: fold NULL/'' `query_canonical` back
19
- * to the raw `query` so canonical is a total GROUP BY / join key. Default
20
- * false preserves the legacy raw-column behaviour. See ADR-0018.
18
+ * Deprecated compatibility flag. Fact tables no longer carry
19
+ * `query_canonical`; canonical reads derive from `query_dim`, or from a
20
+ * canonical rollup relation when `queryCanonicalSource: 'column'` is set.
21
21
  */
22
22
  canonicalFallback?: boolean;
23
+ /**
24
+ * `queryDim` reads canonical from a joined query dimension. `column` is only
25
+ * for derived canonical rollup relations whose primary relation already
26
+ * carries a null-free `query_canonical` output column.
27
+ */
28
+ queryCanonicalSource?: 'queryDim' | 'column';
29
+ }
30
+ interface R2SqlResolverAdapterOptions extends ResolverAdapterOptions {
31
+ /**
32
+ * R2 SQL string partition equality can undercount on identity partitions;
33
+ * string-encoded catalogs use CONCAT(col, '') in partition predicates. Int
34
+ * catalogs do not need the workaround and keep bare equality for pruning.
35
+ */
36
+ partitionKeyEncoding?: 'string' | 'int';
23
37
  }
24
38
  declare function createParquetResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
25
39
  /**
@@ -33,4 +47,16 @@ declare function createParquetResolverAdapter(options?: ResolverAdapterOptions):
33
47
  * `${namespace}.pages`) before sending to R2 SQL.
34
48
  */
35
49
  declare function createIcebergResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
36
- export { PgTableKey, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, pgResolverAdapter };
50
+ /**
51
+ * R2 SQL adapter for the Iceberg fact tables.
52
+ *
53
+ * It shares the multi-tenant Iceberg schema with `createIcebergResolverAdapter`
54
+ * but models R2 SQL's narrower execution surface: no window-total plans and no
55
+ * comparison joins. Int-partition catalogs are the default and emit bare
56
+ * equality predicates for pruning. Legacy string-partition catalogs must pass
57
+ * `partitionKeyEncoding: 'string'` to emit `CONCAT(partition_col, '') = ?`,
58
+ * working around R2 SQL's partition-string equality undercount while preserving
59
+ * bound params.
60
+ */
61
+ declare function createR2SqlResolverAdapter(options?: R2SqlResolverAdapterOptions): ResolverAdapter<PgTableKey>;
62
+ export { PgTableKey, R2SqlResolverAdapterOptions, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter };