@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.
- package/README.md +4 -6
- package/dist/_chunks/compaction.mjs +14 -10
- package/dist/_chunks/duckdb.d.mts +10 -2
- package/dist/_chunks/engine.mjs +17 -12
- package/dist/_chunks/entities.mjs +113 -85
- package/dist/_chunks/errors.d.mts +1 -3
- package/dist/_chunks/index.d.mts +2 -11
- package/dist/_chunks/index2.d.mts +1 -4
- package/dist/_chunks/pg-adapter.d.mts +0 -6
- package/dist/_chunks/resolver.mjs +44 -42
- package/dist/_chunks/schema.d.mts +1 -2
- package/dist/_chunks/schema.mjs +1 -4
- package/dist/_chunks/source.mjs +2 -16
- package/dist/adapters/filesystem.mjs +103 -34
- package/dist/adapters/hyparquet.d.mts +5 -0
- package/dist/adapters/hyparquet.mjs +22 -7
- package/dist/adapters/r2-manifest.mjs +12 -13
- package/dist/adapters/r2.mjs +13 -2
- package/dist/errors.d.mts +2 -2
- package/dist/errors.mjs +1 -4
- package/dist/iceberg/index.mjs +6 -2
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +2 -18
- package/dist/ingest.d.mts +0 -2
- package/dist/period/index.d.mts +2 -2
- package/dist/period/index.mjs +1 -20
- package/dist/resolver/index.d.mts +0 -12
- package/dist/rollups.mjs +23 -7
- package/dist/schema.d.mts +2 -2
- package/dist/schema.mjs +2 -2
- package/dist/sink-node.d.mts +2 -3
- package/dist/source/index.d.mts +2 -2
- package/dist/source/index.mjs +2 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -37,21 +37,20 @@ Optional peers (install only what your runtime needs):
|
|
|
37
37
|
| `@gscdump/engine/scope` | Multi-tenant scope predicates. |
|
|
38
38
|
| `@gscdump/engine/arrow` | Apache Arrow utilities for engine result conversion. |
|
|
39
39
|
| `@gscdump/engine/node` | Node-only DuckDB handle. |
|
|
40
|
-
| `@gscdump/engine/node-harness` | Node test harness for engine integration tests. |
|
|
41
40
|
| `@gscdump/engine/filesystem` | Node-only `DataSource` + `ManifestStore` adapters. |
|
|
42
|
-
| `@gscdump/engine/http` | Read-only HTTP `DataSource` (signed URLs, Range requests). |
|
|
43
41
|
| `@gscdump/engine/hyparquet` | Pure-JS `ParquetCodec`. |
|
|
44
42
|
| `@gscdump/engine/r2` | Cloudflare R2 `DataSource` (structurally typed against `R2Bucket`). |
|
|
45
43
|
| `@gscdump/engine/r2-manifest` | R2-backed `ManifestStore` for hosted deployments. |
|
|
46
|
-
| `@gscdump/engine/
|
|
47
|
-
| `@gscdump/engine/
|
|
44
|
+
| `@gscdump/engine/iceberg` | Edge-safe GSC Iceberg schema/catalog/append sink. |
|
|
45
|
+
| `@gscdump/engine/sink-node` | Node-only Iceberg overwrite/delete recovery writer. |
|
|
46
|
+
| `@gscdump/engine/source` | Query-source contracts and factories. |
|
|
48
47
|
|
|
49
48
|
## Stability
|
|
50
49
|
|
|
51
50
|
| Surface | Stability |
|
|
52
51
|
|---|---|
|
|
53
52
|
| `createStorageEngine` and storage contracts (`StorageEngine`, `Row`, `WriteCtx`, ...) | Public |
|
|
54
|
-
| Adapters (`/node`, `/filesystem`, `/
|
|
53
|
+
| Adapters (`/node`, `/filesystem`, `/hyparquet`, `/r2`, `/r2-manifest`) | Public |
|
|
55
54
|
| Planner (`resolveToSQL`, `enumeratePartitions`) | Public |
|
|
56
55
|
| Schema (`SCHEMAS`, `allTables`, ...) | Public |
|
|
57
56
|
| Internals reached through `@gscdump/engine/<file>` paths not listed above | Private — may break between minors |
|
|
@@ -60,7 +59,6 @@ Optional peers (install only what your runtime needs):
|
|
|
60
59
|
|
|
61
60
|
- [`gscdump`](../gscdump) — REST client + query builder (edge-safe peer dep).
|
|
62
61
|
- [`@gscdump/analysis`](../analysis) — analyzers; consumes `StorageEngine` via `createEngine` factories.
|
|
63
|
-
- [`@gscdump/engine-duckdb-node`](../engine-duckdb-node) — Node DuckDB analyzer adapter.
|
|
64
62
|
- [`@gscdump/engine-duckdb-wasm`](../engine-duckdb-wasm) — DuckDB-WASM browser adapter.
|
|
65
63
|
- [`@gscdump/engine-sqlite`](../engine-sqlite) — SQLite / D1 adapter.
|
|
66
64
|
- [`@gscdump/cli`](../cli) — CLI wrapping engine + analysis.
|
|
@@ -189,42 +189,46 @@ function splitOverlappingTiers(entries, queryRange) {
|
|
|
189
189
|
const spanned = [];
|
|
190
190
|
const kept = [];
|
|
191
191
|
const subsumed = [];
|
|
192
|
+
const clampToRange = Number.isFinite(rangeStartMs) && Number.isFinite(rangeEndMs);
|
|
192
193
|
for (const entry of entries) {
|
|
193
194
|
const span = partitionSpan(entry.partition);
|
|
194
195
|
if (!span) {
|
|
195
196
|
kept.push(entry);
|
|
196
197
|
continue;
|
|
197
198
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
days.push(t);
|
|
202
|
-
}
|
|
203
|
-
if (queryRange && days.length === 0) {
|
|
199
|
+
const startMs = clampToRange ? Math.max(span.startMs, rangeStartMs) : span.startMs;
|
|
200
|
+
const endMs = clampToRange ? Math.min(span.endMs, rangeEndMs) : span.endMs;
|
|
201
|
+
if (queryRange && startMs > endMs) {
|
|
204
202
|
subsumed.push(entry);
|
|
205
203
|
continue;
|
|
206
204
|
}
|
|
207
205
|
spanned.push({
|
|
208
206
|
entry,
|
|
209
207
|
rank: span.rank,
|
|
210
|
-
|
|
208
|
+
startMs,
|
|
209
|
+
endMs
|
|
211
210
|
});
|
|
212
211
|
}
|
|
213
212
|
spanned.sort((a, b) => a.rank - b.rank || b.entry.createdAt - a.entry.createdAt);
|
|
214
213
|
const coveredBySearchType = /* @__PURE__ */ new Map();
|
|
215
|
-
for (const { entry,
|
|
214
|
+
for (const { entry, startMs, endMs } of spanned) {
|
|
216
215
|
const slice = inferSearchType(entry);
|
|
217
216
|
let covered = coveredBySearchType.get(slice);
|
|
218
217
|
if (!covered) {
|
|
219
218
|
covered = /* @__PURE__ */ new Set();
|
|
220
219
|
coveredBySearchType.set(slice, covered);
|
|
221
220
|
}
|
|
222
|
-
|
|
221
|
+
let fullyCovered = true;
|
|
222
|
+
for (let dayMs = startMs; dayMs <= endMs; dayMs += MS_PER_DAY) if (!covered.has(dayMs)) {
|
|
223
|
+
fullyCovered = false;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
if (fullyCovered) {
|
|
223
227
|
subsumed.push(entry);
|
|
224
228
|
continue;
|
|
225
229
|
}
|
|
226
230
|
kept.push(entry);
|
|
227
|
-
for (
|
|
231
|
+
for (let dayMs = startMs; dayMs <= endMs; dayMs += MS_PER_DAY) covered.add(dayMs);
|
|
228
232
|
}
|
|
229
233
|
return {
|
|
230
234
|
kept,
|
|
@@ -14,8 +14,16 @@ interface DuckDBHandle {
|
|
|
14
14
|
interface DuckDBFactory {
|
|
15
15
|
getDuckDB: () => Promise<DuckDBHandle>;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
interface DuckDBReadOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Maximum non-URI parquet reads held in flight while registering DuckDB
|
|
20
|
+
* buffers. Defaults to 16; callers handling unusually large files can lower
|
|
21
|
+
* it to trade latency for peak JS memory.
|
|
22
|
+
*/
|
|
23
|
+
bufferReadConcurrency?: number;
|
|
24
|
+
}
|
|
25
|
+
declare function createDuckDBCodec(factory: DuckDBFactory, options?: DuckDBReadOptions): ParquetCodec;
|
|
26
|
+
declare function createDuckDBExecutor(factory: DuckDBFactory, options?: DuckDBReadOptions): QueryExecutor;
|
|
19
27
|
/**
|
|
20
28
|
* Canonical "empty-file" SELECT clause for a table. Codecs that need to
|
|
21
29
|
* emit a schema-correct empty Parquet can wrap this in:
|
package/dist/_chunks/engine.mjs
CHANGED
|
@@ -8,11 +8,16 @@ import { sqlEscape } from "../sql-bind.mjs";
|
|
|
8
8
|
import { encodeJsonBigintSafe } from "@gscdump/lakehouse";
|
|
9
9
|
import { buildLogicalPlan } from "gscdump/query/plan";
|
|
10
10
|
import { normalizeUrl } from "gscdump/normalize";
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const DEFAULT_BUFFER_READ_CONCURRENCY = 16;
|
|
12
|
+
const MAX_BUFFER_READ_CONCURRENCY = 64;
|
|
13
|
+
function bufferReadConcurrency(value, fileCount) {
|
|
14
|
+
return Math.max(1, Math.min(fileCount || 1, typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : DEFAULT_BUFFER_READ_CONCURRENCY, MAX_BUFFER_READ_CONCURRENCY));
|
|
15
|
+
}
|
|
16
|
+
async function registerBufferedFiles(db, files, dataSource, registered, signal, maxConcurrency) {
|
|
17
|
+
const batchSize = bufferReadConcurrency(maxConcurrency, files.length);
|
|
18
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
14
19
|
signal?.throwIfAborted();
|
|
15
|
-
const batch = files.slice(i, i +
|
|
20
|
+
const batch = files.slice(i, i + batchSize);
|
|
16
21
|
const buffers = await Promise.all(batch.map((file) => dataSource.read(file.key, void 0, signal)));
|
|
17
22
|
for (let j = 0; j < batch.length; j++) {
|
|
18
23
|
signal?.throwIfAborted();
|
|
@@ -47,7 +52,7 @@ async function decodeBytes(db, bytes, table) {
|
|
|
47
52
|
await db.dropFiles([name]);
|
|
48
53
|
}
|
|
49
54
|
}
|
|
50
|
-
function createDuckDBCodec(factory) {
|
|
55
|
+
function createDuckDBCodec(factory, options = {}) {
|
|
51
56
|
return {
|
|
52
57
|
async writeRows(ctx, rows, key, dataSource) {
|
|
53
58
|
const bytes = await encodeBytes(await factory.getDuckDB(), ctx.table, rows);
|
|
@@ -100,7 +105,7 @@ function createDuckDBCodec(factory) {
|
|
|
100
105
|
};
|
|
101
106
|
});
|
|
102
107
|
try {
|
|
103
|
-
await registerBufferedFiles(db, bufferedInputs, dataSource, registered);
|
|
108
|
+
await registerBufferedFiles(db, bufferedInputs, dataSource, registered, void 0, options.bufferReadConcurrency);
|
|
104
109
|
const fileList = inNames.map((n) => `'${sqlEscape(n)}'`).join(", ");
|
|
105
110
|
await db.query(`COPY (${dedupedMergeSql(ctx.table, fileList)}) TO '${sqlEscape(outName)}' (FORMAT PARQUET, COMPRESSION ZSTD)`);
|
|
106
111
|
registered.push(outName);
|
|
@@ -137,7 +142,7 @@ function rewriteEmptyFileSets(sql, placeholders, defaultTable, placeholderTables
|
|
|
137
142
|
}
|
|
138
143
|
return out;
|
|
139
144
|
}
|
|
140
|
-
function createDuckDBExecutor(factory) {
|
|
145
|
+
function createDuckDBExecutor(factory, options = {}) {
|
|
141
146
|
return { async execute({ sql, params, fileKeys, placeholderTables, dataSource, table, signal, profiler }) {
|
|
142
147
|
signal?.throwIfAborted();
|
|
143
148
|
const db = await factory.getDuckDB();
|
|
@@ -146,24 +151,24 @@ function createDuckDBExecutor(factory) {
|
|
|
146
151
|
const totalFiles = Object.values(fileKeys).reduce((n, keys) => n + keys.length, 0);
|
|
147
152
|
const endRegister = profiler?.start("files.register", { files: totalFiles });
|
|
148
153
|
try {
|
|
149
|
-
|
|
154
|
+
const bufferedByName = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const [name, keys] of Object.entries(fileKeys)) {
|
|
150
156
|
const resolved = [];
|
|
151
|
-
const buffered = [];
|
|
152
157
|
for (let i = 0; i < keys.length; i++) {
|
|
153
158
|
const key = keys[i];
|
|
154
159
|
const uri = dataSource.uri?.(key);
|
|
155
160
|
if (uri !== void 0) resolved.push(uri);
|
|
156
161
|
else {
|
|
157
|
-
|
|
162
|
+
if (!bufferedByName.has(key)) bufferedByName.set(key, {
|
|
158
163
|
key,
|
|
159
164
|
name: key
|
|
160
165
|
});
|
|
161
166
|
resolved.push(key);
|
|
162
167
|
}
|
|
163
168
|
}
|
|
164
|
-
await registerBufferedFiles(db, buffered, dataSource, registered, signal);
|
|
165
169
|
placeholders[name] = resolved;
|
|
166
|
-
}
|
|
170
|
+
}
|
|
171
|
+
await registerBufferedFiles(db, [...bufferedByName.values()], dataSource, registered, signal, options.bufferReadConcurrency);
|
|
167
172
|
endRegister?.({ buffered: registered.length });
|
|
168
173
|
signal?.throwIfAborted();
|
|
169
174
|
const finalSql = substituteNamedFiles(rewriteEmptyFileSets(sql, placeholders, table, placeholderTables), placeholders);
|
|
@@ -9,9 +9,6 @@ function isMissingKeyError(e) {
|
|
|
9
9
|
return /\bnot found\b|\bENOENT\b|\bmissing key\b/i.test(message);
|
|
10
10
|
}
|
|
11
11
|
async function readOptional(ds, key, signal) {
|
|
12
|
-
if (ds.head) {
|
|
13
|
-
if (await ds.head(key) === void 0) return void 0;
|
|
14
|
-
}
|
|
15
12
|
return await ds.read(key, void 0, signal).catch((e) => {
|
|
16
13
|
if (isMissingKeyError(e)) return void 0;
|
|
17
14
|
throw e;
|
|
@@ -72,9 +69,6 @@ function buildQueryDimRecords(queries, deps) {
|
|
|
72
69
|
return out;
|
|
73
70
|
}
|
|
74
71
|
function createQueryDimStore({ dataSource }) {
|
|
75
|
-
async function exists(key, prefix) {
|
|
76
|
-
return (await dataSource.list(prefix)).includes(key);
|
|
77
|
-
}
|
|
78
72
|
return {
|
|
79
73
|
parquetKey: queryDimParquetKey,
|
|
80
74
|
async write(ctx, records, builtAt) {
|
|
@@ -98,15 +92,14 @@ function createQueryDimStore({ dataSource }) {
|
|
|
98
92
|
};
|
|
99
93
|
},
|
|
100
94
|
async loadMeta(ctx) {
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
103
|
-
const bytes = await dataSource.read(key);
|
|
95
|
+
const bytes = await readOptional(dataSource, queryDimMetaKey(ctx));
|
|
96
|
+
if (!bytes) return null;
|
|
104
97
|
return JSON.parse(new TextDecoder().decode(bytes));
|
|
105
98
|
},
|
|
106
99
|
async loadRecords(ctx) {
|
|
107
|
-
const
|
|
108
|
-
if (!
|
|
109
|
-
return (await decodeParquetToRows(
|
|
100
|
+
const bytes = await readOptional(dataSource, queryDimParquetKey(ctx));
|
|
101
|
+
if (!bytes) return [];
|
|
102
|
+
return (await decodeParquetToRows(bytes)).map((r) => ({
|
|
110
103
|
query: String(r.query),
|
|
111
104
|
query_canonical: String(r.query_canonical),
|
|
112
105
|
intent_code: Number(r.intent_code),
|
|
@@ -117,6 +110,21 @@ function createQueryDimStore({ dataSource }) {
|
|
|
117
110
|
};
|
|
118
111
|
}
|
|
119
112
|
const YEAR_MONTH_RE = /^(\d{4})-(\d{2})-/;
|
|
113
|
+
const ENTITY_IO_CONCURRENCY = 8;
|
|
114
|
+
async function mapEntityIo(items, fn) {
|
|
115
|
+
if (items.length === 0) return [];
|
|
116
|
+
const results = Array.from({ length: items.length }, () => void 0);
|
|
117
|
+
let next = 0;
|
|
118
|
+
async function worker() {
|
|
119
|
+
while (true) {
|
|
120
|
+
const index = next++;
|
|
121
|
+
if (index >= items.length) return;
|
|
122
|
+
results[index] = await fn(items[index], index);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await Promise.all(Array.from({ length: Math.min(ENTITY_IO_CONCURRENCY, items.length) }, worker));
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
120
128
|
function inspectionIndexKey(ctx) {
|
|
121
129
|
return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/entities/inspections/index.json` : `u_${ctx.userId}/entities/inspections/index.json`;
|
|
122
130
|
}
|
|
@@ -311,33 +319,33 @@ function createInspectionStore(opts) {
|
|
|
311
319
|
if (!byMonth.has(month)) byMonth.set(month, []);
|
|
312
320
|
byMonth.get(month).push(r);
|
|
313
321
|
}
|
|
314
|
-
|
|
322
|
+
await mapEntityIo([...byMonth].map(([yearMonth, batch]) => {
|
|
315
323
|
const bytes = encodeJsonBigintSafe({
|
|
316
324
|
version: 1,
|
|
317
325
|
records: batch
|
|
318
326
|
});
|
|
319
327
|
if (bytes.byteLength > 5242880) throw new Error(`inspection history shard exceeds ${INSPECTION_HISTORY_MAX_BYTES} bytes (got ${bytes.byteLength}); split the batch`);
|
|
320
|
-
|
|
321
|
-
|
|
328
|
+
return {
|
|
329
|
+
key: inspectionHistoryShardKey(ctx, yearMonth, batchId),
|
|
330
|
+
bytes
|
|
331
|
+
};
|
|
332
|
+
}), (shard) => ds.write(shard.key, shard.bytes));
|
|
322
333
|
},
|
|
323
334
|
async loadHistory(ctx, yearMonth) {
|
|
324
335
|
const keys = await ds.list(inspectionHistoryPrefix(ctx, yearMonth));
|
|
325
336
|
if (keys.length === 0) return void 0;
|
|
326
|
-
const out = [];
|
|
327
|
-
for (const key of keys) {
|
|
328
|
-
const bytes = await readOptional(ds, key);
|
|
329
|
-
if (!bytes) continue;
|
|
330
|
-
const shard = await Promise.resolve().then(() => JSON.parse(new TextDecoder().decode(bytes))).catch((err) => {
|
|
331
|
-
console.warn("[inspection.loadHistory] failed to decode shard", {
|
|
332
|
-
key,
|
|
333
|
-
error: err.message
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
if (shard?.records) out.push(...shard.records);
|
|
337
|
-
}
|
|
338
337
|
return {
|
|
339
338
|
version: 1,
|
|
340
|
-
records:
|
|
339
|
+
records: (await mapEntityIo(keys, async (key) => {
|
|
340
|
+
const bytes = await readOptional(ds, key);
|
|
341
|
+
if (!bytes) return [];
|
|
342
|
+
return (await Promise.resolve().then(() => JSON.parse(new TextDecoder().decode(bytes))).catch((err) => {
|
|
343
|
+
console.warn("[inspection.loadHistory] failed to decode shard", {
|
|
344
|
+
key,
|
|
345
|
+
error: err.message
|
|
346
|
+
});
|
|
347
|
+
}))?.records ?? [];
|
|
348
|
+
})).flat()
|
|
341
349
|
};
|
|
342
350
|
},
|
|
343
351
|
async materialize(ctx, rowIter) {
|
|
@@ -369,18 +377,19 @@ function createInspectionStore(opts) {
|
|
|
369
377
|
bucket.push(r);
|
|
370
378
|
byMonth.set(month, bucket);
|
|
371
379
|
}
|
|
372
|
-
const
|
|
373
|
-
for (const [month, batch] of byMonth) {
|
|
380
|
+
const files = [...byMonth].map(([month, batch]) => {
|
|
374
381
|
const bytes = encodeRowsToParquetFlex(batch, {
|
|
375
382
|
columns: INSPECTION_EVENT_COLUMNS,
|
|
376
383
|
sortKey: ["urlHash"]
|
|
377
384
|
});
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
385
|
+
return {
|
|
386
|
+
key: inspectionEventKey(ctx, month, batchId),
|
|
387
|
+
bytes
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
await mapEntityIo(files, (file) => ds.write(file.key, file.bytes));
|
|
382
391
|
return {
|
|
383
|
-
keys,
|
|
392
|
+
keys: files.map((file) => file.key),
|
|
384
393
|
rowCount: rows.length
|
|
385
394
|
};
|
|
386
395
|
},
|
|
@@ -410,11 +419,18 @@ function createInspectionStore(opts) {
|
|
|
410
419
|
for (const row of baseRows) consider(row);
|
|
411
420
|
let eventsFolded = 0;
|
|
412
421
|
const consumed = [];
|
|
413
|
-
|
|
422
|
+
const eventFiles = await mapEntityIo(eventKeys.sort(), async (key) => {
|
|
414
423
|
const bytes = await readOptional(ds, key);
|
|
415
|
-
if (!bytes)
|
|
424
|
+
if (!bytes) return void 0;
|
|
425
|
+
return {
|
|
426
|
+
key,
|
|
427
|
+
rows: await decodeParquetToRows(bytes)
|
|
428
|
+
};
|
|
429
|
+
});
|
|
430
|
+
for (const file of eventFiles) {
|
|
431
|
+
if (!file) continue;
|
|
432
|
+
const { key, rows } = file;
|
|
416
433
|
consumed.push(key);
|
|
417
|
-
const rows = await decodeParquetToRows(bytes);
|
|
418
434
|
for (const row of rows) {
|
|
419
435
|
consider(row);
|
|
420
436
|
eventsFolded++;
|
|
@@ -610,16 +626,18 @@ function createSitemapStore(opts) {
|
|
|
610
626
|
records: {}
|
|
611
627
|
};
|
|
612
628
|
const stamp = now();
|
|
629
|
+
const historyDocs = /* @__PURE__ */ new Map();
|
|
613
630
|
for (const r of records) {
|
|
614
631
|
const h = hash(r.path);
|
|
615
632
|
index.records[h] = r;
|
|
616
|
-
|
|
633
|
+
historyDocs.set(sitemapHistoryKey(ctx, h, stamp), {
|
|
617
634
|
version: 1,
|
|
618
635
|
path: r.path,
|
|
619
636
|
capturedAt: r.capturedAt,
|
|
620
637
|
record: r
|
|
621
638
|
});
|
|
622
639
|
}
|
|
640
|
+
await mapEntityIo([...historyDocs], ([key, doc]) => writeJson(key, doc));
|
|
623
641
|
await writeJson(indexKey, index);
|
|
624
642
|
},
|
|
625
643
|
async loadIndex(ctx) {
|
|
@@ -702,9 +720,10 @@ function createSitemapStore(opts) {
|
|
|
702
720
|
async *loadUrls(ctx, feedpath, opts) {
|
|
703
721
|
const fpHash = hash(feedpath);
|
|
704
722
|
const includeRemoved = opts?.includeRemoved ?? false;
|
|
705
|
-
const
|
|
706
|
-
const
|
|
707
|
-
|
|
723
|
+
const [indexRows, listedDeltaKeys] = await Promise.all([readOptional(ds, sitemapUrlsIndexKey(ctx, fpHash)).then((bytes) => bytes ? decodeParquetToRows(bytes) : []), ds.list(`${sitemapUrlsPrefix(ctx)}/deltas/`)]);
|
|
724
|
+
const deltaKeys = listedDeltaKeys.filter((key) => {
|
|
725
|
+
return SITEMAP_URLS_DELTA_PREFIX_RE.exec(key)?.[2] === fpHash;
|
|
726
|
+
}).sort();
|
|
708
727
|
const live = /* @__PURE__ */ new Map();
|
|
709
728
|
const removedMap = /* @__PURE__ */ new Map();
|
|
710
729
|
for (const row of indexRows) {
|
|
@@ -712,36 +731,34 @@ function createSitemapStore(opts) {
|
|
|
712
731
|
if (rec.removedAt != null) removedMap.set(rec.urlHash, rec);
|
|
713
732
|
else live.set(rec.urlHash, rec);
|
|
714
733
|
}
|
|
715
|
-
|
|
716
|
-
const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
|
|
717
|
-
if (!m || m[2] !== fpHash) continue;
|
|
734
|
+
const deltas = await mapEntityIo(deltaKeys, async (key) => {
|
|
718
735
|
const dBytes = await readOptional(ds, key);
|
|
719
|
-
if (!dBytes)
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
736
|
+
if (!dBytes) return [];
|
|
737
|
+
return decodeParquetToRows(dBytes);
|
|
738
|
+
});
|
|
739
|
+
for (const dRows of deltas) for (const r of dRows) {
|
|
740
|
+
const op = String(r.op);
|
|
741
|
+
const urlHash = String(r.url_hash);
|
|
742
|
+
const at = Number(r.at);
|
|
743
|
+
if (op === "added") {
|
|
744
|
+
const prev = live.get(urlHash) ?? removedMap.get(urlHash);
|
|
745
|
+
removedMap.delete(urlHash);
|
|
746
|
+
live.set(urlHash, {
|
|
747
|
+
feedpath,
|
|
748
|
+
feedpathHash: fpHash,
|
|
749
|
+
urlHash,
|
|
750
|
+
loc: String(r.loc),
|
|
751
|
+
lastmod: r.lastmod == null ? void 0 : String(r.lastmod),
|
|
752
|
+
firstSeenAt: prev?.firstSeenAt ?? at,
|
|
753
|
+
lastSeenAt: at
|
|
754
|
+
});
|
|
755
|
+
} else if (op === "removed") {
|
|
756
|
+
const prev = live.get(urlHash);
|
|
757
|
+
live.delete(urlHash);
|
|
758
|
+
if (prev) removedMap.set(urlHash, {
|
|
759
|
+
...prev,
|
|
760
|
+
removedAt: at
|
|
761
|
+
});
|
|
745
762
|
}
|
|
746
763
|
}
|
|
747
764
|
for (const rec of live.values()) yield rec;
|
|
@@ -796,8 +813,14 @@ function createSitemapStore(opts) {
|
|
|
796
813
|
for (const [fpHash, feedDeltaKeys] of deltasByFeed) {
|
|
797
814
|
if (compactedFeedpaths > 0 && (compactedFeedpaths >= maxFeedpaths || now() - startedAt >= deadlineMs)) break;
|
|
798
815
|
const indexKey = sitemapUrlsIndexKey(ctx, fpHash);
|
|
799
|
-
const
|
|
800
|
-
|
|
816
|
+
const [indexRows, deltaFiles] = await Promise.all([readOptional(ds, indexKey).then((bytes) => bytes ? decodeParquetToRows(bytes) : []), mapEntityIo(feedDeltaKeys.sort(), async (key) => {
|
|
817
|
+
const bytes = await readOptional(ds, key);
|
|
818
|
+
if (!bytes) return void 0;
|
|
819
|
+
return {
|
|
820
|
+
key,
|
|
821
|
+
rows: await decodeParquetToRows(bytes)
|
|
822
|
+
};
|
|
823
|
+
})]);
|
|
801
824
|
const live = /* @__PURE__ */ new Map();
|
|
802
825
|
const removed = /* @__PURE__ */ new Map();
|
|
803
826
|
for (const row of indexRows) {
|
|
@@ -806,11 +829,10 @@ function createSitemapStore(opts) {
|
|
|
806
829
|
else live.set(rec.urlHash, rec);
|
|
807
830
|
}
|
|
808
831
|
const consumed = [];
|
|
809
|
-
for (const
|
|
810
|
-
|
|
811
|
-
|
|
832
|
+
for (const file of deltaFiles) {
|
|
833
|
+
if (!file) continue;
|
|
834
|
+
const { key, rows } = file;
|
|
812
835
|
consumed.push(key);
|
|
813
|
-
const rows = await decodeParquetToRows(bytes);
|
|
814
836
|
for (const r of rows) {
|
|
815
837
|
const urlHash = String(r.url_hash);
|
|
816
838
|
const at = Number(r.at);
|
|
@@ -856,12 +878,13 @@ function createSitemapStore(opts) {
|
|
|
856
878
|
const at = atOpt ?? now();
|
|
857
879
|
const liveHashes = new Set(liveFeedpaths.map((fp) => hash(fp)));
|
|
858
880
|
const present = /* @__PURE__ */ new Set();
|
|
859
|
-
|
|
881
|
+
const [indexKeys, deltaKeys] = await Promise.all([ds.list(`${sitemapUrlsIndexPrefix(ctx)}/`), ds.list(`${sitemapUrlsPrefix(ctx)}/deltas/`)]);
|
|
882
|
+
for (const key of indexKeys) {
|
|
860
883
|
const m = /\/by-feed\/([0-9a-f]+)\/index\.parquet$/.exec(key);
|
|
861
884
|
if (m) present.add(m[1]);
|
|
862
885
|
}
|
|
863
886
|
const deltasByFeed = /* @__PURE__ */ new Map();
|
|
864
|
-
for (const key of
|
|
887
|
+
for (const key of deltaKeys) {
|
|
865
888
|
const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
|
|
866
889
|
if (!m) continue;
|
|
867
890
|
present.add(m[2]);
|
|
@@ -874,8 +897,14 @@ function createSitemapStore(opts) {
|
|
|
874
897
|
for (const fpHash of present) {
|
|
875
898
|
if (liveHashes.has(fpHash)) continue;
|
|
876
899
|
const indexKey = sitemapUrlsIndexKey(ctx, fpHash);
|
|
877
|
-
const
|
|
878
|
-
|
|
900
|
+
const [indexRows, deltaFiles] = await Promise.all([readOptional(ds, indexKey).then((bytes) => bytes ? decodeParquetToRows(bytes) : []), mapEntityIo((deltasByFeed.get(fpHash) ?? []).sort(), async (key) => {
|
|
901
|
+
const bytes = await readOptional(ds, key);
|
|
902
|
+
if (!bytes) return void 0;
|
|
903
|
+
return {
|
|
904
|
+
key,
|
|
905
|
+
rows: await decodeParquetToRows(bytes)
|
|
906
|
+
};
|
|
907
|
+
})]);
|
|
879
908
|
const live = /* @__PURE__ */ new Map();
|
|
880
909
|
const removed = /* @__PURE__ */ new Map();
|
|
881
910
|
for (const row of indexRows) {
|
|
@@ -884,11 +913,10 @@ function createSitemapStore(opts) {
|
|
|
884
913
|
else live.set(r.urlHash, r);
|
|
885
914
|
}
|
|
886
915
|
const consumed = [];
|
|
887
|
-
for (const
|
|
888
|
-
|
|
889
|
-
|
|
916
|
+
for (const file of deltaFiles) {
|
|
917
|
+
if (!file) continue;
|
|
918
|
+
const { key, rows } = file;
|
|
890
919
|
consumed.push(key);
|
|
891
|
-
const rows = await decodeParquetToRows(bytes);
|
|
892
920
|
for (const r of rows) {
|
|
893
921
|
const urlHash = String(r.url_hash);
|
|
894
922
|
const dat = Number(r.at);
|
|
@@ -103,8 +103,6 @@ declare const engineErrors: {
|
|
|
103
103
|
readonly lockAcquireTimeout: (scope: string, timeoutMs: number) => EngineError;
|
|
104
104
|
};
|
|
105
105
|
declare function isEngineError(value: unknown): value is EngineError;
|
|
106
|
-
/** The human-readable rendering of an `EngineError`, for logs and string sinks. */
|
|
107
|
-
declare function formatEngineError(error: EngineError): string;
|
|
108
106
|
/**
|
|
109
107
|
* Re-raises an `EngineError` value as a generic `Error`, stashing the union under
|
|
110
108
|
* `.engineError` for stack-walking. Used by the throwing wrappers over the
|
|
@@ -114,4 +112,4 @@ declare function formatEngineError(error: EngineError): string;
|
|
|
114
112
|
* mapper so the class identity callers match on is preserved.
|
|
115
113
|
*/
|
|
116
114
|
declare function engineErrorToException(error: EngineError): Error;
|
|
117
|
-
export { EngineError, EngineErrorKind, engineErrorToException, engineErrors,
|
|
115
|
+
export { EngineError, EngineErrorKind, engineErrorToException, engineErrors, isEngineError };
|
package/dist/_chunks/index.d.mts
CHANGED
|
@@ -105,14 +105,5 @@ declare function createEngineQuerySource(options: EngineQuerySourceOptions): Ana
|
|
|
105
105
|
declare function runAnalyzerWithEngine(deps: {
|
|
106
106
|
engine: StorageEngine;
|
|
107
107
|
}, ctx: TenantCtx, params: AnalysisParams, registry: AnalyzerRegistry): Promise<AnalysisResult>;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
readonly __row?: TRow;
|
|
111
|
-
}
|
|
112
|
-
declare function typedQuery<TRow>(state: BuilderState): TypedQuery<TRow>;
|
|
113
|
-
declare function queryRows<TRow = QueryRow>(source: AnalysisQuerySource, query: BuilderState | TypedQuery<TRow>): Promise<TRow[]>;
|
|
114
|
-
declare function queryComparisonRows<TRow = QueryRow>(source: AnalysisQuerySource, current: BuilderState | TypedQuery<TRow>, previous: BuilderState | TypedQuery<TRow>): Promise<{
|
|
115
|
-
current: TRow[];
|
|
116
|
-
previous: TRow[];
|
|
117
|
-
}>;
|
|
118
|
-
export { AttachedTableMissingError, AttachedTableRunner, AttachedTableSourceOptions, CreateSqlQuerySourceOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, TypedQuery, createAttachedTableSource, createEngineQuerySource, createSqlQuerySource, queryComparisonRows, queryRows, rewriteForTableSource, runAnalyzerWithEngine, typedQuery };
|
|
108
|
+
declare function queryRows<TRow = QueryRow>(source: AnalysisQuerySource, state: BuilderState): Promise<TRow[]>;
|
|
109
|
+
export { AttachedTableMissingError, AttachedTableRunner, AttachedTableSourceOptions, CreateSqlQuerySourceOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, createAttachedTableSource, createEngineQuerySource, createSqlQuerySource, queryRows, rewriteForTableSource, runAnalyzerWithEngine };
|
|
@@ -30,9 +30,6 @@ declare function defaultStartDate(): string;
|
|
|
30
30
|
declare function periodOf(params: AnalysisParams): AnalysisPeriod;
|
|
31
31
|
declare function comparisonOf(params: AnalysisParams): ComparisonPeriod;
|
|
32
32
|
declare function resolveWindow(opts: ResolveWindowOptions): ResolvedWindow;
|
|
33
|
-
/** Convert a ResolvedWindow into the AnalysisPeriod / ComparisonPeriod shape. */
|
|
34
|
-
declare function windowToPeriod(w: ResolvedWindow): AnalysisPeriod;
|
|
35
|
-
declare function windowToComparisonPeriod(w: ResolvedWindow): ComparisonPeriod | undefined;
|
|
36
33
|
interface PadTimeseriesOptions<T> {
|
|
37
34
|
/** ISO date (YYYY-MM-DD), inclusive lower bound. */
|
|
38
35
|
startDate: string;
|
|
@@ -54,4 +51,4 @@ type DateRowShape = Record<string, unknown> & {
|
|
|
54
51
|
* once. Existing dates keep all their rows (grouped timeseries safe).
|
|
55
52
|
*/
|
|
56
53
|
declare function padTimeseries<T extends DateRowShape = DateRowShape>(rows: readonly T[], options: PadTimeseriesOptions<T>): T[];
|
|
57
|
-
export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, defaultStartDate, padTimeseries, periodOf, resolveWindow
|
|
54
|
+
export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, defaultStartDate, padTimeseries, periodOf, resolveWindow };
|
|
@@ -14,12 +14,6 @@ declare const pgResolverAdapter: ResolverAdapter<PgTableKey>;
|
|
|
14
14
|
* accidental adapter caching that would lock in a stale `{{FILES}}` set.
|
|
15
15
|
*/
|
|
16
16
|
interface ResolverAdapterOptions {
|
|
17
|
-
/**
|
|
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
|
-
*/
|
|
22
|
-
canonicalFallback?: boolean;
|
|
23
17
|
/**
|
|
24
18
|
* `queryDim` reads canonical from a joined query dimension. `column` is only
|
|
25
19
|
* for derived canonical rollup relations whose primary relation already
|