@gscdump/engine 0.35.12 → 0.36.0

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.
@@ -432,7 +432,8 @@ function createStorageEngine(opts) {
432
432
  await manifestStore.bumpWatermark({
433
433
  userId: ctx.userId,
434
434
  siteId: ctx.siteId,
435
- table: ctx.table
435
+ table: ctx.table,
436
+ ...searchType !== void 0 ? { searchType } : {}
436
437
  }, date, now);
437
438
  });
438
439
  }
@@ -499,7 +500,8 @@ function createStorageEngine(opts) {
499
500
  await manifestStore.bumpWatermark({
500
501
  userId: ctx.userId,
501
502
  siteId: ctx.siteId,
502
- table: ctx.table
503
+ table: ctx.table,
504
+ ...searchType !== void 0 ? { searchType } : {}
503
505
  }, date, now);
504
506
  });
505
507
  }
@@ -613,11 +615,11 @@ function createStorageEngine(opts) {
613
615
  for (const k of await dataSource.list(prefix)) yield k;
614
616
  }();
615
617
  for await (const key of keyStream) keys.push(key);
616
- if (keys.length > 0) await dataSource.delete(keys);
617
618
  const manifestResult = await manifestStore.purgeTenant({
618
619
  userId: ctx.userId,
619
620
  siteId: ctx.siteId
620
621
  });
622
+ if (keys.length > 0) await dataSource.delete(keys);
621
623
  return {
622
624
  userId: ctx.userId,
623
625
  siteId: ctx.siteId,
@@ -3,7 +3,7 @@ function manifestEntryKey(entry) {
3
3
  return entry.objectKey;
4
4
  }
5
5
  function watermarkKey(watermark) {
6
- return `${watermark.userId}|${watermark.siteId ?? ""}|${watermark.table}`;
6
+ return `${watermark.userId}|${watermark.siteId ?? ""}|${watermark.table}|${inferSearchType(watermark)}`;
7
7
  }
8
8
  function syncStateKey(state) {
9
9
  return `${state.userId}|${state.siteId ?? ""}|${state.table}|${state.date}|${inferSearchType(state)}`;
@@ -21,6 +21,7 @@ function matchesWatermarkFilter(watermark, filter, options = {}) {
21
21
  if (!options.ignoreUserId && watermark.userId !== filter.userId) return false;
22
22
  if (filter.siteId !== void 0 && watermark.siteId !== filter.siteId) return false;
23
23
  if (filter.table !== void 0 && watermark.table !== filter.table) return false;
24
+ if (filter.searchType !== void 0 && inferSearchType(watermark) !== filter.searchType) return false;
24
25
  return true;
25
26
  }
26
27
  function matchesSyncStateFilter(state, filter, options = {}) {
@@ -525,9 +525,10 @@ function joinAnd(parts) {
525
525
  function joinComma(parts) {
526
526
  return sql.join(parts, sql`, `);
527
527
  }
528
- function orderByClause(state, prefix = "") {
528
+ const ORDER_BY_HELPER_PREFIX = "__order_";
529
+ function orderByClause(state, prefix = "", columnOverride) {
529
530
  if (state.orderBy) {
530
- const safeCol = state.orderBy.column.replace(/\W/g, "");
531
+ const safeCol = (columnOverride ?? state.orderBy.column).replace(/\W/g, "");
531
532
  const safeDir = state.orderBy.dir.toUpperCase() === "ASC" ? "ASC" : "DESC";
532
533
  return sql.raw(`ORDER BY ${prefix}${safeCol} ${safeDir}`);
533
534
  }
@@ -670,12 +671,31 @@ function resolveToSQLOptimized(state, options) {
670
671
  outerTotals.push(sql.raw("SUM(sum_position) OVER() / NULLIF(SUM(impressions) OVER(), 0) + 1 as totalPosition"));
671
672
  break;
672
673
  }
674
+ let orderByColumnOverride;
675
+ const orderColumn = state.orderBy?.column;
676
+ if (orderColumn && orderColumn !== "date" && !metrics.includes(orderColumn)) {
677
+ orderByColumnOverride = `${ORDER_BY_HELPER_PREFIX}${orderColumn}`;
678
+ switch (orderColumn) {
679
+ case "clicks":
680
+ outerSelect.push(sql.raw(`clicks as "${orderByColumnOverride}"`));
681
+ break;
682
+ case "impressions":
683
+ outerSelect.push(sql.raw(`impressions as "${orderByColumnOverride}"`));
684
+ break;
685
+ case "ctr":
686
+ outerSelect.push(sql.raw(`CAST(clicks AS REAL) / NULLIF(impressions, 0) as "${orderByColumnOverride}"`));
687
+ break;
688
+ case "position":
689
+ outerSelect.push(sql.raw(`sum_position / NULLIF(impressions, 0) + 1 as "${orderByColumnOverride}"`));
690
+ break;
691
+ }
692
+ }
673
693
  outerSelect.push(sql.raw("COUNT(*) OVER() as totalCount"));
674
694
  for (const totalExpr of outerTotals) outerSelect.push(totalExpr);
675
695
  let cte = wherePredicates.length > 0 ? sql`SELECT ${joinComma(cteSelect)} FROM ${table} WHERE ${joinAnd(wherePredicates)}` : sql`SELECT ${joinComma(cteSelect)} FROM ${table}`;
676
696
  if (groupByExprs.length > 0) cte = sql`${cte} GROUP BY ${joinComma(groupByExprs)}`;
677
697
  if (having.length > 0) cte = sql`${cte} HAVING ${joinAnd(having)}`;
678
- return compileCollapsed(adapter, sql`WITH aggregated AS (${cte}) SELECT ${joinComma(outerSelect)} FROM aggregated ${orderByClause(state)} ${limitOffsetClause(state)}`);
698
+ return compileCollapsed(adapter, sql`WITH aggregated AS (${cte}) SELECT ${joinComma(outerSelect)} FROM aggregated ${orderByClause(state, "", orderByColumnOverride)} ${limitOffsetClause(state)}`);
679
699
  }
680
700
  function resolveToSQL(state, options) {
681
701
  const { adapter } = options;
@@ -1106,6 +1126,7 @@ async function runOptimizedQuery(runSQL, ctx, state, dateRange, options = {}) {
1106
1126
  return {
1107
1127
  rows: optRes.rows.map((r) => {
1108
1128
  const { totalCount: _tc, totalClicks: _tcl, totalImpressions: _ti, totalCtr: _tr, totalPosition: _tp, ...rest } = r;
1129
+ for (const key of Object.keys(rest)) if (key.startsWith("__order_")) delete rest[key];
1109
1130
  return rest;
1110
1131
  }),
1111
1132
  totalCount,
@@ -239,6 +239,11 @@ interface WatermarkScope {
239
239
  userId: string;
240
240
  siteId?: string;
241
241
  table: TableName;
242
+ /**
243
+ * GSC search-type this watermark covers. Omitted = `web` for legacy
244
+ * compatibility with pre-searchType watermarks.
245
+ */
246
+ searchType?: SearchType;
242
247
  }
243
248
  interface Watermark extends WatermarkScope {
244
249
  newestDateSynced: string;
@@ -249,6 +254,7 @@ interface WatermarkFilter {
249
254
  userId: string;
250
255
  siteId?: string;
251
256
  table?: TableName;
257
+ searchType?: SearchType;
252
258
  }
253
259
  type SyncStateKind = 'pending' | 'inflight' | 'done' | 'failed';
254
260
  interface SyncStateScope {
@@ -337,8 +343,9 @@ interface ManifestStore {
337
343
  * GDPR-grade tenant purge. Removes every manifest entry, watermark, and
338
344
  * sync-state record matching the filter. Does NOT touch the underlying
339
345
  * data-source bytes; callers (typically {@link StorageEngine.purgeTenant})
340
- * must sweep the tenant prefix separately before invoking this so that
341
- * mid-flight failures can't leave orphan parquet with no manifest record.
346
+ * remove manifest visibility first, then sweep tenant-prefix bytes so that
347
+ * mid-flight failures cannot leave live manifest entries pointing at deleted
348
+ * objects.
342
349
  *
343
350
  * On stores with CAS-backed sharding (R2 manifest) this may issue one
344
351
  * mutation per shard. On read-only stores (HTTP) this throws.
@@ -224,6 +224,7 @@ function createFilesystemManifestStore(opts) {
224
224
  userId: scope.userId,
225
225
  siteId: scope.siteId,
226
226
  table: scope.table,
227
+ ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {},
227
228
  newestDateSynced: date,
228
229
  oldestDateSynced: date,
229
230
  lastSyncAt: nowMs
@@ -124,12 +124,13 @@ function createR2ManifestStore(opts) {
124
124
  async function mutateShard(siteId, table, mutate) {
125
125
  return unwrapResult(await mutateShardResult(siteId, table, mutate), engineErrorToException);
126
126
  }
127
- async function listShards() {
127
+ async function listShards(siteId) {
128
128
  const shards = [];
129
129
  let cursor;
130
+ const prefix = siteId === void 0 ? `u_${userId}/manifest/` : `u_${userId}/manifest/${siteId}/`;
130
131
  do {
131
132
  const res = await bucket.list({
132
- prefix: `u_${userId}/manifest/`,
133
+ prefix,
133
134
  cursor,
134
135
  limit: 1e3
135
136
  });
@@ -149,7 +150,7 @@ function createR2ManifestStore(opts) {
149
150
  siteId: filter.siteId,
150
151
  table: filter.table
151
152
  }];
152
- return (await listShards()).filter((s) => (filter.siteId === void 0 || s.siteId === filter.siteId) && (filter.table === void 0 || s.table === filter.table));
153
+ return (await listShards(filter.siteId)).filter((s) => (filter.siteId === void 0 || s.siteId === filter.siteId) && (filter.table === void 0 || s.table === filter.table));
153
154
  }
154
155
  function assertScopedUser(got, op) {
155
156
  if (got !== userId) throw new Error(`${op}: R2 manifest store is scoped to userId=${userId}, got ${got}`);
@@ -256,13 +257,15 @@ function createR2ManifestStore(opts) {
256
257
  assertScopedUser(scope.userId, "bumpWatermark");
257
258
  if (scope.siteId === void 0) throw new Error("R2 manifest store requires watermarks to carry siteId");
258
259
  const ts = at ?? now();
260
+ const scopeSearchType = inferSearchType(scope);
259
261
  await mutateShard(scope.siteId, scope.table, (snap) => {
260
- const idx = snap.watermarks.findIndex((w) => w.userId === userId && w.siteId === scope.siteId && w.table === scope.table);
262
+ const idx = snap.watermarks.findIndex((w) => w.userId === userId && w.siteId === scope.siteId && w.table === scope.table && inferSearchType(w) === scopeSearchType);
261
263
  if (idx === -1) {
262
264
  snap.watermarks.push({
263
265
  userId,
264
266
  siteId: scope.siteId,
265
267
  table: scope.table,
268
+ ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {},
266
269
  newestDateSynced: date,
267
270
  oldestDateSynced: date,
268
271
  lastSyncAt: ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "0.35.12",
4
+ "version": "0.36.0",
5
5
  "description": "Append-only Parquet/DuckDB storage engine + planner + adapters for the gscdump pipeline. Node + edge runtimes; opt-in heavy peers.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -181,9 +181,9 @@
181
181
  "dependencies": {
182
182
  "drizzle-orm": "1.0.0-rc.3",
183
183
  "proper-lockfile": "^4.1.2",
184
- "@gscdump/lakehouse": "0.35.12",
185
- "@gscdump/contracts": "0.35.12",
186
- "gscdump": "0.35.12"
184
+ "@gscdump/contracts": "0.36.0",
185
+ "@gscdump/lakehouse": "0.36.0",
186
+ "gscdump": "0.36.0"
187
187
  },
188
188
  "devDependencies": {
189
189
  "@duckdb/duckdb-wasm": "^1.32.0",