@gscdump/engine 1.0.1 → 1.0.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.
- package/README.md +4 -2
- package/dist/_chunks/coerce.mjs +1 -1
- package/dist/_chunks/engine.mjs +1 -1
- package/dist/_chunks/entities.mjs +3 -91
- package/dist/_chunks/index.d.mts +44 -95
- package/dist/_chunks/resolver.mjs +105 -105
- package/dist/_chunks/schema2.mjs +2 -1
- package/dist/_chunks/sink.d.mts +2 -1
- package/dist/_chunks/snapshot.d.mts +27 -1
- package/dist/adapters/node.d.mts +6 -2
- package/dist/adapters/node.mjs +93 -2
- package/dist/entities.d.mts +3 -36
- package/dist/entities.mjs +4 -2
- package/dist/entity-keys.d.mts +27 -0
- package/dist/entity-keys.mjs +93 -0
- package/dist/iceberg/index.mjs +7 -7
- package/dist/index.d.mts +6 -107
- package/dist/index.mjs +101 -132
- package/dist/ingest-accumulator.d.mts +97 -0
- package/dist/ingest-accumulator.mjs +123 -0
- package/dist/period/index.d.mts +1 -1
- package/dist/planner.d.mts +14 -2
- package/dist/report/index.d.mts +1 -1
- package/dist/resolver/index.d.mts +53 -1
- package/dist/rollups.mjs +3 -2
- package/dist/source/index.d.mts +103 -2
- package/dist/source/index.mjs +142 -1
- package/package.json +14 -4
- package/dist/_chunks/duckdb.d.mts +0 -27
- package/dist/_chunks/index2.d.mts +0 -53
- package/dist/_chunks/pg-adapter.d.mts +0 -56
- package/dist/_chunks/planner.d.mts +0 -15
- package/dist/_chunks/source.mjs +0 -143
- package/dist/adapters/duckdb-node.d.mts +0 -7
- package/dist/adapters/duckdb-node.mjs +0 -94
- package/dist/schedule.mjs +0 -100
- /package/dist/{schedule.d.mts → _chunks/schedule.d.mts} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import { engineErrorToException, engineErrors
|
|
1
|
+
import { engineErrorToException, engineErrors } from "./errors.mjs";
|
|
2
2
|
import { DEFAULT_SEARCH_TYPE, dayPartition, hourPartition, inferLegacyTier, inferSearchType, objectKey } from "./_chunks/layout.mjs";
|
|
3
3
|
import { matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter } from "./_chunks/manifest-store-utils.mjs";
|
|
4
4
|
import { coerceRow, coerceRows } from "./_chunks/coerce.mjs";
|
|
5
|
-
import { SCHEMAS, TABLE_METADATA, allTables, countries, currentSchemaVersion, dates, dimensionToColumn, drizzleSchema, hourly_pages, inferTable, page_queries, pages, queries } from "./_chunks/schema.mjs";
|
|
6
|
-
import { FILES_PLACEHOLDER, enumeratePartitions, resolveParquetSQL, substituteNamedFiles } from "./_chunks/parquet-plan.mjs";
|
|
7
|
-
import { bindLiterals, formatLiteral } from "./sql-bind.mjs";
|
|
8
5
|
import { MAX_DAY_BYTES, createDuckDBCodec, createDuckDBExecutor, createStorageEngine } from "./_chunks/engine.mjs";
|
|
9
|
-
import { assembleDatesRow, createRowAccumulator, toPath, toSumPosition, transformGscRow } from "./ingest.mjs";
|
|
10
|
-
import "./planner.mjs";
|
|
11
|
-
import { createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter } from "./_chunks/resolver.mjs";
|
|
12
|
-
import { rebuildDailyFromHourly } from "./rollups.mjs";
|
|
13
|
-
import { fixedPolicy, inspectionPolicy, sitemapPolicy } from "./schedule.mjs";
|
|
14
|
-
import { ENGINE_QUERY_CAPABILITIES, createSqlQuerySource } from "./_chunks/source.mjs";
|
|
15
6
|
import { err, ok, unwrapResult } from "gscdump/result";
|
|
16
7
|
const SHARD_RE = /^u_[^/]+\/manifest\/(?<siteId>[^/]+)\/(?<table>[^/]+)\/HEAD$/;
|
|
17
8
|
const CAS_BACKOFF_BASE_MS = 5;
|
|
@@ -372,127 +363,6 @@ function createR2ManifestStore(opts) {
|
|
|
372
363
|
}
|
|
373
364
|
};
|
|
374
365
|
}
|
|
375
|
-
function scopeOf(ctx, table, date) {
|
|
376
|
-
return {
|
|
377
|
-
userId: String(ctx.userId),
|
|
378
|
-
siteId: ctx.siteId,
|
|
379
|
-
table,
|
|
380
|
-
date,
|
|
381
|
-
...ctx.searchType !== void 0 ? { searchType: ctx.searchType } : {}
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
function createIngestAccumulator(opts) {
|
|
385
|
-
const { engine, ctx, hooks, ...accOpts } = opts;
|
|
386
|
-
const acc = createRowAccumulator(accOpts);
|
|
387
|
-
function reportHookFailure(hook, error) {
|
|
388
|
-
console.warn(`[gscdump/engine] ${hook} hook failed`, error);
|
|
389
|
-
}
|
|
390
|
-
async function notifyWriteError(info) {
|
|
391
|
-
try {
|
|
392
|
-
await hooks.onWriteError(info);
|
|
393
|
-
} catch (hookError) {
|
|
394
|
-
reportHookFailure("onWriteError", hookError);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
async function writeOne(table, date, rows) {
|
|
398
|
-
const scope = scopeOf(ctx, table, date);
|
|
399
|
-
return (ctx.grain === "hour" ? engine.writeHour ?? (() => Promise.reject(/* @__PURE__ */ new Error("ingest accumulator: grain=hour requires engine.writeHour"))) : engine.writeDay)(scope, rows).then(() => engine.setSyncState(scope, "done")).then(async () => {
|
|
400
|
-
await hooks.onWritten?.({
|
|
401
|
-
table,
|
|
402
|
-
date,
|
|
403
|
-
rowCount: rows.length
|
|
404
|
-
});
|
|
405
|
-
return {
|
|
406
|
-
ok: true,
|
|
407
|
-
rows: rows.length
|
|
408
|
-
};
|
|
409
|
-
}).catch(async (err) => {
|
|
410
|
-
await notifyWriteError({
|
|
411
|
-
table,
|
|
412
|
-
date,
|
|
413
|
-
error: err
|
|
414
|
-
});
|
|
415
|
-
return { ok: false };
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
async function recover(table, date) {
|
|
419
|
-
const scope = scopeOf(ctx, table, date);
|
|
420
|
-
try {
|
|
421
|
-
await engine.setSyncState(scope, "failed", { error: "mid-continuation-skip" });
|
|
422
|
-
} catch (stateError) {
|
|
423
|
-
await notifyWriteError({
|
|
424
|
-
table,
|
|
425
|
-
date,
|
|
426
|
-
error: stateError
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
return hooks.onRecover(table, date).catch(async (err) => {
|
|
430
|
-
await notifyWriteError({
|
|
431
|
-
table,
|
|
432
|
-
date,
|
|
433
|
-
error: err
|
|
434
|
-
});
|
|
435
|
-
return false;
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
return {
|
|
439
|
-
push(table, rows) {
|
|
440
|
-
return acc.push(table, rows);
|
|
441
|
-
},
|
|
442
|
-
async finalize({ hasMore }) {
|
|
443
|
-
const overflowed = acc.overflowed;
|
|
444
|
-
const totalRows = acc.totalRows;
|
|
445
|
-
const buckets = acc.drain();
|
|
446
|
-
if (overflowed || hasMore) {
|
|
447
|
-
const tasks = [];
|
|
448
|
-
for (const [table, byDate] of buckets) for (const date of byDate.keys()) tasks.push(recover(table, date));
|
|
449
|
-
const results = await Promise.all(tasks).catch(async (err) => {
|
|
450
|
-
await notifyWriteError({
|
|
451
|
-
table: null,
|
|
452
|
-
date: null,
|
|
453
|
-
error: err
|
|
454
|
-
});
|
|
455
|
-
return [];
|
|
456
|
-
});
|
|
457
|
-
if (overflowed) await notifyWriteError({
|
|
458
|
-
table: null,
|
|
459
|
-
date: null,
|
|
460
|
-
error: /* @__PURE__ */ new Error(`ingest accumulator overflow at ${totalRows} rows; recovering via forced re-sync`)
|
|
461
|
-
});
|
|
462
|
-
return {
|
|
463
|
-
flushed: 0,
|
|
464
|
-
recovered: results.filter(Boolean).length,
|
|
465
|
-
failed: 0,
|
|
466
|
-
rowsWritten: 0
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
const writes = [];
|
|
470
|
-
for (const [table, byDate] of buckets) for (const [date, rows] of byDate) writes.push(writeOne(table, date, rows));
|
|
471
|
-
const outcomes = await Promise.all(writes);
|
|
472
|
-
let flushed = 0;
|
|
473
|
-
let failed = 0;
|
|
474
|
-
let rowsWritten = 0;
|
|
475
|
-
for (const o of outcomes) if (o.ok) {
|
|
476
|
-
flushed++;
|
|
477
|
-
rowsWritten += o.rows;
|
|
478
|
-
} else failed++;
|
|
479
|
-
if (flushed > 0 && hooks.onJobComplete) try {
|
|
480
|
-
await hooks.onJobComplete({
|
|
481
|
-
flushed,
|
|
482
|
-
rowsWritten
|
|
483
|
-
});
|
|
484
|
-
} catch (hookError) {
|
|
485
|
-
reportHookFailure("onJobComplete", hookError);
|
|
486
|
-
}
|
|
487
|
-
return {
|
|
488
|
-
flushed,
|
|
489
|
-
recovered: 0,
|
|
490
|
-
failed,
|
|
491
|
-
rowsWritten
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
366
|
function createQueryProfiler(sink, now = () => Date.now()) {
|
|
497
367
|
return { start(name, meta) {
|
|
498
368
|
const t0 = now();
|
|
@@ -519,6 +389,105 @@ function collectSpans(now) {
|
|
|
519
389
|
spans
|
|
520
390
|
};
|
|
521
391
|
}
|
|
392
|
+
const DAY = 24 * (3600 * 1e3);
|
|
393
|
+
function isDue(state, now) {
|
|
394
|
+
return now >= state.nextAt;
|
|
395
|
+
}
|
|
396
|
+
function sitemapCadenceMs(consecutiveUnchanged) {
|
|
397
|
+
if (consecutiveUnchanged >= 7) return 30 * DAY;
|
|
398
|
+
if (consecutiveUnchanged >= 3) return 7 * DAY;
|
|
399
|
+
return DAY;
|
|
400
|
+
}
|
|
401
|
+
const SITEMAP_VERSION = 1;
|
|
402
|
+
const sitemapPolicy = {
|
|
403
|
+
version: SITEMAP_VERSION,
|
|
404
|
+
initial(now) {
|
|
405
|
+
return {
|
|
406
|
+
nextAt: now + DAY,
|
|
407
|
+
consecutiveUnchanged: 0,
|
|
408
|
+
policyVersion: SITEMAP_VERSION
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
observe(prev, evt) {
|
|
412
|
+
if (prev.policyVersion !== SITEMAP_VERSION) return {
|
|
413
|
+
nextAt: evt.at + sitemapCadenceMs(0),
|
|
414
|
+
consecutiveUnchanged: 0,
|
|
415
|
+
policyVersion: SITEMAP_VERSION
|
|
416
|
+
};
|
|
417
|
+
if (evt.changed) return {
|
|
418
|
+
nextAt: evt.at + DAY,
|
|
419
|
+
consecutiveUnchanged: 0,
|
|
420
|
+
policyVersion: SITEMAP_VERSION
|
|
421
|
+
};
|
|
422
|
+
const next = prev.consecutiveUnchanged + 1;
|
|
423
|
+
return {
|
|
424
|
+
nextAt: evt.at + sitemapCadenceMs(next),
|
|
425
|
+
consecutiveUnchanged: next,
|
|
426
|
+
policyVersion: SITEMAP_VERSION
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
isDue
|
|
430
|
+
};
|
|
431
|
+
const INSPECTION_VERSION = 1;
|
|
432
|
+
function inspectionCadenceMs(verdict) {
|
|
433
|
+
if (verdict === "PASS") return 30 * DAY;
|
|
434
|
+
if (verdict === "FAIL") return 7 * DAY;
|
|
435
|
+
return 14 * DAY;
|
|
436
|
+
}
|
|
437
|
+
function inspectionPolicy(verdict) {
|
|
438
|
+
const cadence = inspectionCadenceMs(verdict);
|
|
439
|
+
return {
|
|
440
|
+
version: INSPECTION_VERSION,
|
|
441
|
+
initial(now) {
|
|
442
|
+
return {
|
|
443
|
+
nextAt: now + cadence,
|
|
444
|
+
consecutiveUnchanged: 0,
|
|
445
|
+
policyVersion: INSPECTION_VERSION
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
observe(prev, evt) {
|
|
449
|
+
if (prev.policyVersion !== INSPECTION_VERSION) return {
|
|
450
|
+
nextAt: evt.at + cadence,
|
|
451
|
+
consecutiveUnchanged: 0,
|
|
452
|
+
policyVersion: INSPECTION_VERSION
|
|
453
|
+
};
|
|
454
|
+
const next = evt.changed ? 0 : prev.consecutiveUnchanged + 1;
|
|
455
|
+
return {
|
|
456
|
+
nextAt: evt.at + cadence,
|
|
457
|
+
consecutiveUnchanged: next,
|
|
458
|
+
policyVersion: INSPECTION_VERSION
|
|
459
|
+
};
|
|
460
|
+
},
|
|
461
|
+
isDue
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const FIXED_VERSION = 1;
|
|
465
|
+
function fixedPolicy(intervalMs) {
|
|
466
|
+
return {
|
|
467
|
+
version: FIXED_VERSION,
|
|
468
|
+
initial(now) {
|
|
469
|
+
return {
|
|
470
|
+
nextAt: now + intervalMs,
|
|
471
|
+
consecutiveUnchanged: 0,
|
|
472
|
+
policyVersion: FIXED_VERSION
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
observe(prev, evt) {
|
|
476
|
+
if (prev.policyVersion !== FIXED_VERSION) return {
|
|
477
|
+
nextAt: evt.at + intervalMs,
|
|
478
|
+
consecutiveUnchanged: 0,
|
|
479
|
+
policyVersion: FIXED_VERSION
|
|
480
|
+
};
|
|
481
|
+
const next = evt.changed ? 0 : prev.consecutiveUnchanged + 1;
|
|
482
|
+
return {
|
|
483
|
+
nextAt: evt.at + intervalMs,
|
|
484
|
+
consecutiveUnchanged: next,
|
|
485
|
+
policyVersion: FIXED_VERSION
|
|
486
|
+
};
|
|
487
|
+
},
|
|
488
|
+
isDue
|
|
489
|
+
};
|
|
490
|
+
}
|
|
522
491
|
const KEY_SEP = "\0";
|
|
523
492
|
function partitionKey(slice) {
|
|
524
493
|
return [
|
|
@@ -674,4 +643,4 @@ const MIN_SYNC_IMPRESSIONS = 1;
|
|
|
674
643
|
const MIN_COUNTRY_IMPRESSIONS = 10;
|
|
675
644
|
const MAX_SITEMAP_URLS_PER_SITE = 5e4;
|
|
676
645
|
const MAX_TRACKED_URLS_PER_SITE = 2e5;
|
|
677
|
-
export { DEFAULT_SEARCH_TYPE,
|
|
646
|
+
export { DEFAULT_SEARCH_TYPE, MAX_DAY_BYTES, MAX_GSC_PAGES_R2, MAX_SITEMAP_URLS_PER_SITE, MAX_TRACKED_URLS_PER_SITE, MIN_COUNTRY_IMPRESSIONS, MIN_SYNC_IMPRESSIONS, ROW_LIMIT_R2, TABLES_BY_SEARCH_TYPE, TABLE_TIERS, TIER_PRIORITY, WEIGHT_PRIORITY, coerceRow, coerceRows, collectSpans, createDuckDBCodec, createDuckDBExecutor, createInMemorySink, createQueryProfiler, createR2ManifestStore, createStorageEngine, dayPartition, fixedPolicy, getDateWeight, getTableTier, getTablesForTier, hourPartition, inferLegacyTier, inferSearchType, inspectionPolicy, objectKey, parseEnabledSearchTypes, sitemapPolicy, validateEnabledSearchTypes, validateEnabledSearchTypesResult };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { SearchType, TenantCtx as TenantCtx$1 } from "./_chunks/storage.mjs";
|
|
2
|
+
import { GscApiRow, RowAccumulatorOptions } from "./ingest.mjs";
|
|
3
|
+
import { Grain, Row, TableName } from "@gscdump/contracts";
|
|
4
|
+
interface IngestAccumulatorEngine {
|
|
5
|
+
writeDay: (scope: TenantCtx$1 & {
|
|
6
|
+
table: TableName;
|
|
7
|
+
date: string;
|
|
8
|
+
searchType?: SearchType;
|
|
9
|
+
}, rows: Row[]) => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Routed when the accumulator's `ctx.grain === 'hour'`. Same scope shape as
|
|
12
|
+
* `writeDay`; `date` is the PT calendar day, rows carry `hour` + `date`.
|
|
13
|
+
* Optional so hosts that never opt into hourly need not implement it.
|
|
14
|
+
*/
|
|
15
|
+
writeHour?: (scope: TenantCtx$1 & {
|
|
16
|
+
table: TableName;
|
|
17
|
+
date: string;
|
|
18
|
+
searchType?: SearchType;
|
|
19
|
+
}, rows: Row[]) => Promise<void>;
|
|
20
|
+
setSyncState: (scope: TenantCtx$1 & {
|
|
21
|
+
table: TableName;
|
|
22
|
+
date: string;
|
|
23
|
+
searchType?: SearchType;
|
|
24
|
+
}, state: 'done' | 'failed', info?: {
|
|
25
|
+
error?: string;
|
|
26
|
+
}) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
interface IngestAccumulatorCtx {
|
|
29
|
+
userId: string | number;
|
|
30
|
+
siteId: string;
|
|
31
|
+
searchType?: SearchType;
|
|
32
|
+
/**
|
|
33
|
+
* Temporal granularity for this accumulator. `'day'` (default) routes
|
|
34
|
+
* flushed buckets to `engine.writeDay`. `'hour'` routes to
|
|
35
|
+
* `engine.writeHour` and requires the engine implementation to be set.
|
|
36
|
+
*/
|
|
37
|
+
grain?: Grain;
|
|
38
|
+
}
|
|
39
|
+
interface IngestAccumulatorHooks {
|
|
40
|
+
/**
|
|
41
|
+
* Called once per (table, date) when the job must abandon in-memory rows
|
|
42
|
+
* (overflow or `hasMore` continuation). Host queues a forced re-sync from
|
|
43
|
+
* the source. Return true iff a recovery job was actually queued.
|
|
44
|
+
*/
|
|
45
|
+
onRecover: (table: TableName, date: string) => Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Called when an engine.writeDay fails or recovery itself errors. Host
|
|
48
|
+
* logs to its error sink (e.g. `r2_write_errors` D1 table).
|
|
49
|
+
*/
|
|
50
|
+
onWriteError: (info: {
|
|
51
|
+
table: TableName | null;
|
|
52
|
+
date: string | null;
|
|
53
|
+
error: unknown;
|
|
54
|
+
}) => Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Called after a successful writeDay for a (table, date). Host typically
|
|
57
|
+
* busts the manifest cache here so the next read sees the new parquet.
|
|
58
|
+
*/
|
|
59
|
+
onWritten?: (info: {
|
|
60
|
+
table: TableName;
|
|
61
|
+
date: string;
|
|
62
|
+
rowCount: number;
|
|
63
|
+
}) => void | Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Called once at end of `finalize`, only when at least one (table, date)
|
|
66
|
+
* actually landed. Host queues rollup rebuild + compaction.
|
|
67
|
+
*/
|
|
68
|
+
onJobComplete?: (info: {
|
|
69
|
+
flushed: number;
|
|
70
|
+
rowsWritten: number;
|
|
71
|
+
}) => Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
interface FinalizeOptions {
|
|
74
|
+
/**
|
|
75
|
+
* The GSC `hasMore` flag for the whole job. When true, in-memory buckets
|
|
76
|
+
* only reflect this job's slice; we re-queue forced single-day re-syncs
|
|
77
|
+
* via `onRecover` so R2 stays authoritative.
|
|
78
|
+
*/
|
|
79
|
+
hasMore: boolean;
|
|
80
|
+
}
|
|
81
|
+
interface FinalizeResult {
|
|
82
|
+
flushed: number;
|
|
83
|
+
recovered: number;
|
|
84
|
+
failed: number;
|
|
85
|
+
rowsWritten: number;
|
|
86
|
+
}
|
|
87
|
+
interface IngestAccumulator {
|
|
88
|
+
push: (table: TableName, rows: readonly GscApiRow[]) => boolean;
|
|
89
|
+
finalize: (opts: FinalizeOptions) => Promise<FinalizeResult>;
|
|
90
|
+
}
|
|
91
|
+
interface CreateIngestAccumulatorOptions extends RowAccumulatorOptions {
|
|
92
|
+
engine: IngestAccumulatorEngine;
|
|
93
|
+
ctx: IngestAccumulatorCtx;
|
|
94
|
+
hooks: IngestAccumulatorHooks;
|
|
95
|
+
}
|
|
96
|
+
declare function createIngestAccumulator(opts: CreateIngestAccumulatorOptions): IngestAccumulator;
|
|
97
|
+
export { CreateIngestAccumulatorOptions, FinalizeOptions, FinalizeResult, IngestAccumulator, IngestAccumulatorCtx, IngestAccumulatorEngine, IngestAccumulatorHooks, createIngestAccumulator };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createRowAccumulator } from "./ingest.mjs";
|
|
2
|
+
function scopeOf(ctx, table, date) {
|
|
3
|
+
return {
|
|
4
|
+
userId: String(ctx.userId),
|
|
5
|
+
siteId: ctx.siteId,
|
|
6
|
+
table,
|
|
7
|
+
date,
|
|
8
|
+
...ctx.searchType !== void 0 ? { searchType: ctx.searchType } : {}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function createIngestAccumulator(opts) {
|
|
12
|
+
const { engine, ctx, hooks, ...accOpts } = opts;
|
|
13
|
+
const acc = createRowAccumulator(accOpts);
|
|
14
|
+
function reportHookFailure(hook, error) {
|
|
15
|
+
console.warn(`[gscdump/engine] ${hook} hook failed`, error);
|
|
16
|
+
}
|
|
17
|
+
async function notifyWriteError(info) {
|
|
18
|
+
try {
|
|
19
|
+
await hooks.onWriteError(info);
|
|
20
|
+
} catch (hookError) {
|
|
21
|
+
reportHookFailure("onWriteError", hookError);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function writeOne(table, date, rows) {
|
|
25
|
+
const scope = scopeOf(ctx, table, date);
|
|
26
|
+
return (ctx.grain === "hour" ? engine.writeHour ?? (() => Promise.reject(/* @__PURE__ */ new Error("ingest accumulator: grain=hour requires engine.writeHour"))) : engine.writeDay)(scope, rows).then(() => engine.setSyncState(scope, "done")).then(async () => {
|
|
27
|
+
await hooks.onWritten?.({
|
|
28
|
+
table,
|
|
29
|
+
date,
|
|
30
|
+
rowCount: rows.length
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
rows: rows.length
|
|
35
|
+
};
|
|
36
|
+
}).catch(async (err) => {
|
|
37
|
+
await notifyWriteError({
|
|
38
|
+
table,
|
|
39
|
+
date,
|
|
40
|
+
error: err
|
|
41
|
+
});
|
|
42
|
+
return { ok: false };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function recover(table, date) {
|
|
46
|
+
const scope = scopeOf(ctx, table, date);
|
|
47
|
+
try {
|
|
48
|
+
await engine.setSyncState(scope, "failed", { error: "mid-continuation-skip" });
|
|
49
|
+
} catch (stateError) {
|
|
50
|
+
await notifyWriteError({
|
|
51
|
+
table,
|
|
52
|
+
date,
|
|
53
|
+
error: stateError
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return hooks.onRecover(table, date).catch(async (err) => {
|
|
57
|
+
await notifyWriteError({
|
|
58
|
+
table,
|
|
59
|
+
date,
|
|
60
|
+
error: err
|
|
61
|
+
});
|
|
62
|
+
return false;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
push(table, rows) {
|
|
67
|
+
return acc.push(table, rows);
|
|
68
|
+
},
|
|
69
|
+
async finalize({ hasMore }) {
|
|
70
|
+
const overflowed = acc.overflowed;
|
|
71
|
+
const totalRows = acc.totalRows;
|
|
72
|
+
const buckets = acc.drain();
|
|
73
|
+
if (overflowed || hasMore) {
|
|
74
|
+
const tasks = [];
|
|
75
|
+
for (const [table, byDate] of buckets) for (const date of byDate.keys()) tasks.push(recover(table, date));
|
|
76
|
+
const results = await Promise.all(tasks).catch(async (err) => {
|
|
77
|
+
await notifyWriteError({
|
|
78
|
+
table: null,
|
|
79
|
+
date: null,
|
|
80
|
+
error: err
|
|
81
|
+
});
|
|
82
|
+
return [];
|
|
83
|
+
});
|
|
84
|
+
if (overflowed) await notifyWriteError({
|
|
85
|
+
table: null,
|
|
86
|
+
date: null,
|
|
87
|
+
error: /* @__PURE__ */ new Error(`ingest accumulator overflow at ${totalRows} rows; recovering via forced re-sync`)
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
flushed: 0,
|
|
91
|
+
recovered: results.filter(Boolean).length,
|
|
92
|
+
failed: 0,
|
|
93
|
+
rowsWritten: 0
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const writes = [];
|
|
97
|
+
for (const [table, byDate] of buckets) for (const [date, rows] of byDate) writes.push(writeOne(table, date, rows));
|
|
98
|
+
const outcomes = await Promise.all(writes);
|
|
99
|
+
let flushed = 0;
|
|
100
|
+
let failed = 0;
|
|
101
|
+
let rowsWritten = 0;
|
|
102
|
+
for (const o of outcomes) if (o.ok) {
|
|
103
|
+
flushed++;
|
|
104
|
+
rowsWritten += o.rows;
|
|
105
|
+
} else failed++;
|
|
106
|
+
if (flushed > 0 && hooks.onJobComplete) try {
|
|
107
|
+
await hooks.onJobComplete({
|
|
108
|
+
flushed,
|
|
109
|
+
rowsWritten
|
|
110
|
+
});
|
|
111
|
+
} catch (hookError) {
|
|
112
|
+
reportHookFailure("onJobComplete", hookError);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
flushed,
|
|
116
|
+
recovered: 0,
|
|
117
|
+
failed,
|
|
118
|
+
rowsWritten
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export { createIngestAccumulator };
|
package/dist/period/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, padTimeseries, periodOf, resolveWindow } from "../_chunks/
|
|
1
|
+
import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, padTimeseries, periodOf, resolveWindow } from "../_chunks/index.mjs";
|
|
2
2
|
export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, padTimeseries, periodOf, resolveWindow };
|
package/dist/planner.d.mts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
import { enumeratePartitions } from "./_chunks/storage.mjs";
|
|
2
|
-
import {
|
|
1
|
+
import { TableName, enumeratePartitions } from "./_chunks/storage.mjs";
|
|
2
|
+
import { LogicalQueryPlan } from "gscdump/query/plan";
|
|
3
|
+
import { BuilderState } from "gscdump/query";
|
|
4
|
+
interface ResolvedQuery {
|
|
5
|
+
sql: string;
|
|
6
|
+
params: unknown[];
|
|
7
|
+
partitions: string[];
|
|
8
|
+
table: TableName;
|
|
9
|
+
filesPlaceholder: string;
|
|
10
|
+
}
|
|
11
|
+
declare const FILES_PLACEHOLDER = "{{FILES}}";
|
|
12
|
+
declare function compileLogicalQueryPlan(plan: LogicalQueryPlan, table?: TableName): ResolvedQuery;
|
|
13
|
+
declare function resolveParquetSQL(state: BuilderState, table?: TableName): ResolvedQuery;
|
|
14
|
+
declare function substituteNamedFiles(sql: string, sets: Record<string, string[]>): string;
|
|
3
15
|
export { FILES_PLACEHOLDER, type ResolvedQuery, compileLogicalQueryPlan, enumeratePartitions, resolveParquetSQL, substituteNamedFiles };
|
package/dist/report/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnalysisParams, AnalysisResult } from "../_chunks/analysis-types.mjs";
|
|
2
|
-
import { ComparisonMode, ResolvedWindow, WindowPreset } from "../_chunks/
|
|
2
|
+
import { ComparisonMode, ResolvedWindow, WindowPreset } from "../_chunks/index.mjs";
|
|
3
3
|
/** Status vocabulary mirrors `ActionPrioritySourceStatus`. */
|
|
4
4
|
type ReportStepStatus = 'pending' | 'running' | 'done' | 'skipped' | 'error';
|
|
5
5
|
type ReportSeverity = 'info' | 'low' | 'medium' | 'high';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { SearchType as SearchType$1, TableName as TableName$1 } from "../_chunks/storage.mjs";
|
|
2
2
|
import { ComparisonFilter, ExtraQuery, ResolvedComparisonSQL, ResolvedSQL, ResolvedSQLOptimized, ResolverAdapter, ResolverOptions } from "../_chunks/types.mjs";
|
|
3
|
-
import { PgTableKey, R2SqlResolverAdapterOptions, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter } from "../_chunks/pg-adapter.mjs";
|
|
4
3
|
import { LogicalDataset, LogicalDataset as LogicalDataset$1, PlannerCapabilities, UnresolvableDatasetError, inferDataset as inferLogicalDataset, isDatasetResolvable } from "gscdump/query/plan";
|
|
5
4
|
import { SQL } from "drizzle-orm";
|
|
6
5
|
import { BuilderState, Dimension, FilterInput, InternalFilter, Metric } from "gscdump/query";
|
|
@@ -304,6 +303,59 @@ declare function dimensionValue(row: Record<string, unknown>, dimension: string)
|
|
|
304
303
|
declare function matchesDimensionFilter(row: Record<string, unknown>, filter: InternalFilter): boolean;
|
|
305
304
|
declare function matchesMetricFilter(row: Record<string, unknown>, filter: InternalFilter): boolean;
|
|
306
305
|
declare function matchesTopLevelPage(row: Record<string, unknown>): boolean;
|
|
306
|
+
type PgTableKey = TableName;
|
|
307
|
+
declare const pgResolverAdapter: ResolverAdapter<PgTableKey>;
|
|
308
|
+
/**
|
|
309
|
+
* Parquet-aware variant of {@link pgResolverAdapter}. Identical SQL output
|
|
310
|
+
* except FROM clauses emit `read_parquet({{FILES}}, union_by_name = true) AS
|
|
311
|
+
* "${tk}"`. The runSQL pipeline substitutes `{{FILES}}` with R2 object keys
|
|
312
|
+
* resolved from the manifest. The `AS "${tk}"` alias is mandatory — drizzle
|
|
313
|
+
* compiles `colRef` to table-qualified `"pages"."url"`, which would not
|
|
314
|
+
* resolve against an unaliased FROM.
|
|
315
|
+
*
|
|
316
|
+
* Single-use: build a fresh adapter per query. Cheap (no I/O) and avoids
|
|
317
|
+
* accidental adapter caching that would lock in a stale `{{FILES}}` set.
|
|
318
|
+
*/
|
|
319
|
+
interface ResolverAdapterOptions {
|
|
320
|
+
/**
|
|
321
|
+
* `queryDim` reads canonical from a joined query dimension. `column` is only
|
|
322
|
+
* for derived canonical rollup relations whose primary relation already
|
|
323
|
+
* carries a null-free `query_canonical` output column.
|
|
324
|
+
*/
|
|
325
|
+
queryCanonicalSource?: 'queryDim' | 'column';
|
|
326
|
+
}
|
|
327
|
+
interface R2SqlResolverAdapterOptions extends ResolverAdapterOptions {
|
|
328
|
+
/**
|
|
329
|
+
* R2 SQL string partition equality can undercount on identity partitions;
|
|
330
|
+
* string-encoded catalogs use CONCAT(col, '') in partition predicates. Int
|
|
331
|
+
* catalogs do not need the workaround and keep bare equality for pruning.
|
|
332
|
+
*/
|
|
333
|
+
partitionKeyEncoding?: 'string' | 'int';
|
|
334
|
+
}
|
|
335
|
+
declare function createParquetResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
|
|
336
|
+
/**
|
|
337
|
+
* Multi-tenant pg-flavored adapter for the Iceberg / R2 SQL read path.
|
|
338
|
+
* Identical SQL output to `pgResolverAdapter` except WHERE clauses inject
|
|
339
|
+
* `site_id = ?` AND `search_type = ?` automatically when those scopes are
|
|
340
|
+
* passed to `resolveToSQL`. Required for the Iceberg fact tables which are
|
|
341
|
+
* shared across tenants — querying without these predicates would leak
|
|
342
|
+
* cross-tenant data. Single-use: the adapter has no `tableRef` override,
|
|
343
|
+
* so callers must rewrite bare table names to their qualified form (e.g.
|
|
344
|
+
* `${namespace}.pages`) before sending to R2 SQL.
|
|
345
|
+
*/
|
|
346
|
+
declare function createIcebergResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
|
|
347
|
+
/**
|
|
348
|
+
* R2 SQL adapter for the Iceberg fact tables.
|
|
349
|
+
*
|
|
350
|
+
* It shares the multi-tenant Iceberg schema with `createIcebergResolverAdapter`
|
|
351
|
+
* but models R2 SQL's narrower execution surface: no window-total plans and no
|
|
352
|
+
* comparison joins. Int-partition catalogs are the default and emit bare
|
|
353
|
+
* equality predicates for pruning. Legacy string-partition catalogs must pass
|
|
354
|
+
* `partitionKeyEncoding: 'string'` to emit `CONCAT(partition_col, '') = ?`,
|
|
355
|
+
* working around R2 SQL's partition-string equality undercount while preserving
|
|
356
|
+
* bound params.
|
|
357
|
+
*/
|
|
358
|
+
declare function createR2SqlResolverAdapter(options?: R2SqlResolverAdapterOptions): ResolverAdapter<PgTableKey>;
|
|
307
359
|
interface AssertSchemaInSyncOptions {
|
|
308
360
|
/** Label used in the thrown error (e.g. 'browser', 'sqlite'). */
|
|
309
361
|
label: string;
|
package/dist/rollups.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { engineErrors } from "./errors.mjs";
|
|
2
2
|
import "./_chunks/layout.mjs";
|
|
3
|
+
import { inspectionParquetKey, sitemapUrlsIndexPrefix } from "./entity-keys.mjs";
|
|
3
4
|
import { encodeRowsToParquetFlex } from "./adapters/hyparquet.mjs";
|
|
4
|
-
import { createIndexingMetadataStore, createQueryDimStore, createSitemapStore
|
|
5
|
+
import { createIndexingMetadataStore, createQueryDimStore, createSitemapStore } from "./_chunks/entities.mjs";
|
|
5
6
|
import { MS_PER_DAY } from "gscdump/dates";
|
|
6
|
-
import { encodeJsonBigintSafe } from "@gscdump/lakehouse";
|
|
7
|
+
import { encodeJsonBigintSafe } from "@gscdump/lakehouse/bigint";
|
|
7
8
|
function rollupPrefix(ctx, searchType) {
|
|
8
9
|
const base = ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/rollups` : `u_${ctx.userId}/rollups`;
|
|
9
10
|
return searchType !== void 0 && searchType !== "web" ? `${base}/${searchType}` : base;
|