@gscdump/engine 0.35.11 → 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.
package/dist/_chunks/engine.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
341
|
-
* mid-flight failures
|
|
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
|
|
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.
|
|
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/contracts": "0.
|
|
185
|
-
"@gscdump/lakehouse": "0.
|
|
186
|
-
"gscdump": "0.
|
|
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",
|