@gscdump/engine 0.25.14 → 0.26.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/dispatch.mjs +12 -2
- package/dist/_chunks/entities.mjs +664 -0
- package/dist/_chunks/errors.d.mts +117 -0
- package/dist/_chunks/index.d.mts +2 -0
- package/dist/_chunks/registry.d.mts +1 -1
- package/dist/_chunks/sink.d.mts +23 -7
- package/dist/_chunks/source.mjs +5 -1
- package/dist/_chunks/storage.d.mts +1 -1
- package/dist/adapters/filesystem.mjs +3 -1
- package/dist/adapters/node.d.mts +12 -2
- package/dist/adapters/node.mjs +25 -9
- package/dist/adapters/r2-manifest.mjs +12 -7
- package/dist/analyzer/index.d.mts +2 -0
- package/dist/entities.mjs +1 -640
- package/dist/errors.d.mts +2 -0
- package/dist/errors.mjs +202 -0
- package/dist/iceberg/index.mjs +14 -14
- package/dist/index.d.mts +10 -2
- package/dist/index.mjs +11 -6
- package/dist/resolver/index.d.mts +1 -1
- package/dist/rollups.d.mts +5 -3
- package/dist/rollups.mjs +5 -5
- package/dist/sql-bind.d.mts +15 -1
- package/dist/sql-bind.mjs +32 -20
- package/package.json +8 -3
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
type EngineErrorKind = 'analyzer-not-found' | 'analyzer-capability-missing' | 'invalid-sql-literal' | 'placeholder-arity-mismatch' | 'invalid-search-types' | 'attached-table-missing' | 'manifest-cas-exhausted' | 'invalid-snapshot-filename' | 'unsupported-snapshot-index-version' | 'invalid-schema-identifier' | 'invalid-year-month' | 'missing-attach-url' | 'manifest-cas-round-lost' | 'iceberg-table-op-failed' | 'sink-table-flush-failed' | 'rollup-build-failed' | 'lock-acquire-timeout';
|
|
2
|
+
type EngineError = {
|
|
3
|
+
kind: 'analyzer-not-found';
|
|
4
|
+
tool: string;
|
|
5
|
+
message: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'analyzer-capability-missing';
|
|
8
|
+
tool: string;
|
|
9
|
+
missing: readonly string[];
|
|
10
|
+
message: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'invalid-sql-literal';
|
|
13
|
+
message: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'placeholder-arity-mismatch';
|
|
16
|
+
message: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'invalid-search-types';
|
|
19
|
+
message: string;
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'attached-table-missing';
|
|
23
|
+
missing: readonly string[];
|
|
24
|
+
message: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'manifest-cas-exhausted';
|
|
27
|
+
message: string;
|
|
28
|
+
siteId: string;
|
|
29
|
+
table: string;
|
|
30
|
+
attempts: number;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'invalid-snapshot-filename';
|
|
33
|
+
message: string;
|
|
34
|
+
fileName: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'unsupported-snapshot-index-version';
|
|
37
|
+
message: string;
|
|
38
|
+
version: unknown;
|
|
39
|
+
} | {
|
|
40
|
+
kind: 'invalid-schema-identifier';
|
|
41
|
+
message: string;
|
|
42
|
+
schema: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'invalid-year-month';
|
|
45
|
+
message: string;
|
|
46
|
+
value: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'missing-attach-url';
|
|
49
|
+
message: string;
|
|
50
|
+
fileName: string;
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'manifest-cas-round-lost';
|
|
53
|
+
message: string;
|
|
54
|
+
siteId: string;
|
|
55
|
+
table: string;
|
|
56
|
+
attempt: number;
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'iceberg-table-op-failed';
|
|
59
|
+
message: string;
|
|
60
|
+
op: 'create' | 'drop';
|
|
61
|
+
table: string;
|
|
62
|
+
cause?: unknown;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'sink-table-flush-failed';
|
|
65
|
+
message: string;
|
|
66
|
+
table: string;
|
|
67
|
+
cause?: unknown;
|
|
68
|
+
} | {
|
|
69
|
+
kind: 'rollup-build-failed';
|
|
70
|
+
message: string;
|
|
71
|
+
id: string;
|
|
72
|
+
cause?: unknown;
|
|
73
|
+
} | {
|
|
74
|
+
kind: 'lock-acquire-timeout';
|
|
75
|
+
message: string;
|
|
76
|
+
scope: string;
|
|
77
|
+
timeoutMs: number;
|
|
78
|
+
};
|
|
79
|
+
declare const engineErrors: {
|
|
80
|
+
readonly analyzerNotFound: (tool: string) => EngineError;
|
|
81
|
+
readonly analyzerCapabilityMissing: (tool: string, missing: readonly string[]) => EngineError;
|
|
82
|
+
readonly nonFiniteNumberLiteral: (value: number) => EngineError;
|
|
83
|
+
readonly controlCharsInLiteral: () => EngineError;
|
|
84
|
+
readonly uninlinableLiteralType: (type: string) => EngineError;
|
|
85
|
+
readonly morePlaceholdersThanParams: (have: number) => EngineError;
|
|
86
|
+
readonly dollarPlaceholderOutOfRange: (n: number, have: number) => EngineError;
|
|
87
|
+
readonly mixedPlaceholderStyles: () => EngineError;
|
|
88
|
+
readonly unusedParams: (unused: number) => EngineError;
|
|
89
|
+
readonly searchTypesNotArray: () => EngineError;
|
|
90
|
+
readonly unknownSearchType: (value: unknown) => EngineError;
|
|
91
|
+
readonly searchTypesMissingWeb: () => EngineError;
|
|
92
|
+
readonly attachedTableMissing: (missing: readonly string[]) => EngineError;
|
|
93
|
+
readonly manifestCasExhausted: (siteId: string, table: string, attempts: number) => EngineError;
|
|
94
|
+
readonly invalidSnapshotFilename: (fileName: string) => EngineError;
|
|
95
|
+
readonly unsupportedSnapshotIndexVersion: (version: unknown) => EngineError;
|
|
96
|
+
readonly invalidSchemaIdentifier: (schema: string) => EngineError;
|
|
97
|
+
readonly invalidYearMonth: (value: string) => EngineError;
|
|
98
|
+
readonly missingAttachUrl: (fileName: string) => EngineError;
|
|
99
|
+
readonly manifestCasRoundLost: (siteId: string, table: string, attempt: number) => EngineError;
|
|
100
|
+
readonly icebergTableOpFailed: (op: "create" | "drop", table: string, cause: unknown) => EngineError;
|
|
101
|
+
readonly sinkTableFlushFailed: (table: string, cause: unknown) => EngineError;
|
|
102
|
+
readonly rollupBuildFailed: (id: string, cause: unknown) => EngineError;
|
|
103
|
+
readonly lockAcquireTimeout: (scope: string, timeoutMs: number) => EngineError;
|
|
104
|
+
};
|
|
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
|
+
/**
|
|
109
|
+
* Re-raises an `EngineError` value as a generic `Error`, stashing the union under
|
|
110
|
+
* `.engineError` for stack-walking. Used by the throwing wrappers over the
|
|
111
|
+
* `Result`-returning cores whose original throws were bare `Error`s (the SQL
|
|
112
|
+
* binder, sync-config). Modules that historically threw a *named* class
|
|
113
|
+
* (`AnalyzerCapabilityError`, `AttachedTableMissingError`) provide their own
|
|
114
|
+
* mapper so the class identity callers match on is preserved.
|
|
115
|
+
*/
|
|
116
|
+
declare function engineErrorToException(error: EngineError): Error;
|
|
117
|
+
export { EngineError, EngineErrorKind, engineErrorToException, engineErrors, formatEngineError, isEngineError };
|
package/dist/_chunks/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Row, SearchType as SearchType$1, StorageEngine, TenantCtx } from "./storage.mjs";
|
|
2
|
+
import { EngineError } from "./errors.mjs";
|
|
2
3
|
import { AnalysisParams, AnalysisResult } from "./analysis-types.mjs";
|
|
3
4
|
import { ResolverAdapter } from "./types.mjs";
|
|
4
5
|
import { AnalysisQuerySource, AnalysisSourceKind, AnalyzerRegistry, FileSet, QueryRow, SourceCapabilities } from "./registry.mjs";
|
|
@@ -39,6 +40,7 @@ interface AttachedTableSourceOptions {
|
|
|
39
40
|
}
|
|
40
41
|
declare class AttachedTableMissingError extends Error {
|
|
41
42
|
readonly missing: readonly string[];
|
|
43
|
+
readonly engineError: EngineError;
|
|
42
44
|
constructor(missing: readonly string[]);
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
@@ -2,8 +2,8 @@ import { Row as Row$1 } from "./storage.mjs";
|
|
|
2
2
|
import { AnalysisParams } from "./analysis-types.mjs";
|
|
3
3
|
import { ResolverAdapter } from "./types.mjs";
|
|
4
4
|
import { PlannerCapabilities } from "gscdump/query/plan";
|
|
5
|
-
import { TableName } from "@gscdump/contracts";
|
|
6
5
|
import { BuilderState } from "gscdump/query";
|
|
6
|
+
import { TableName } from "@gscdump/contracts";
|
|
7
7
|
type QueryRow = Record<string, unknown>;
|
|
8
8
|
interface FileSet {
|
|
9
9
|
table: TableName;
|
package/dist/_chunks/sink.d.mts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Row as Row$1, SearchType, TenantCtx as TenantCtx$1 } from "./storage.mjs";
|
|
2
|
+
import { EngineError } from "./errors.mjs";
|
|
3
|
+
import { Result } from "gscdump/result";
|
|
2
4
|
import { icebergAppend, restCatalogConnect, s3SignedResolver } from "icebird";
|
|
3
5
|
import { TableName } from "@gscdump/contracts";
|
|
4
6
|
/**
|
|
@@ -209,12 +211,16 @@ declare function isCommitRateLimited(err: unknown): boolean;
|
|
|
209
211
|
* coalescing) is assessed in the Phase-1.5 report.
|
|
210
212
|
*/
|
|
211
213
|
declare function icebergAppendRetrying(args: Parameters<typeof icebergAppend>[0], options?: CommitRetryOptions): Promise<void>;
|
|
212
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Outcome of a single table create/drop: the table name plus a `Result` —
|
|
216
|
+
* `Ok(void)` on success, `Err(iceberg-table-op-failed)` carrying the failure
|
|
217
|
+
* message (and original `cause`) when the catalog rejects the op (e.g. "table
|
|
218
|
+
* already exists", a 5xx). Per-table so a partial provisioning run is fully
|
|
219
|
+
* observable; the human-readable string lives on `error.message`.
|
|
220
|
+
*/
|
|
213
221
|
interface IcebergTableOpResult {
|
|
214
222
|
table: string;
|
|
215
|
-
|
|
216
|
-
/** Present when `ok` is false. */
|
|
217
|
-
error?: string;
|
|
223
|
+
outcome: Result<void, EngineError>;
|
|
218
224
|
}
|
|
219
225
|
/**
|
|
220
226
|
* Ensure the catalog namespace exists. Idempotent — an "already exists"
|
|
@@ -229,7 +235,13 @@ declare function ensureIcebergNamespace(conn: IcebergConnection): Promise<void>;
|
|
|
229
235
|
* as a failed result. Used by the app's one-off provisioning script.
|
|
230
236
|
*/
|
|
231
237
|
declare function createIcebergTables(conn: IcebergConnection, tables?: readonly IcebergTableName[]): Promise<IcebergTableOpResult[]>;
|
|
232
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* List the table names currently in the catalog namespace.
|
|
240
|
+
*
|
|
241
|
+
* A genuinely-empty namespace resolves to `[]`. A LIST *failure* (catalog
|
|
242
|
+
* unreachable, 401/403, 5xx) propagates rather than being masked as an empty
|
|
243
|
+
* list — callers must be able to tell "no tables" from "couldn't ask".
|
|
244
|
+
*/
|
|
233
245
|
declare function listIcebergTables(conn: IcebergConnection): Promise<string[]>;
|
|
234
246
|
/** A data file in the current snapshot's manifest, scoped to one partition. */
|
|
235
247
|
interface IcebergListedDataFile {
|
|
@@ -321,10 +333,14 @@ interface SinkCapabilities {
|
|
|
321
333
|
interface SinkCloseResult {
|
|
322
334
|
/** Tables whose buffered rows committed durably. */
|
|
323
335
|
flushed: IcebergTableName[];
|
|
324
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* Tables whose flush failed — their slices must NOT be ledger-recorded. Each
|
|
338
|
+
* carries a typed `sink-table-flush-failed` `EngineError`; the human-readable
|
|
339
|
+
* cause is on `error.message`.
|
|
340
|
+
*/
|
|
325
341
|
failed: {
|
|
326
342
|
table: IcebergTableName;
|
|
327
|
-
error:
|
|
343
|
+
error: EngineError;
|
|
328
344
|
}[];
|
|
329
345
|
}
|
|
330
346
|
interface Sink {
|
package/dist/_chunks/source.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "./layout.mjs";
|
|
2
|
+
import { engineErrors } from "../errors.mjs";
|
|
2
3
|
import { assertDimensionsSupported, getFilterDimensions, pgResolverAdapter, resolveToSQL } from "./resolver.mjs";
|
|
3
4
|
import { runAnalyzerFromSource } from "./dispatch.mjs";
|
|
4
5
|
function coerceRow(row) {
|
|
@@ -16,10 +17,13 @@ function coerceRows(rows) {
|
|
|
16
17
|
}
|
|
17
18
|
var AttachedTableMissingError = class extends Error {
|
|
18
19
|
missing;
|
|
20
|
+
engineError;
|
|
19
21
|
constructor(missing) {
|
|
20
|
-
|
|
22
|
+
const engineError = engineErrors.attachedTableMissing(missing);
|
|
23
|
+
super(engineError.message);
|
|
21
24
|
this.missing = missing;
|
|
22
25
|
this.name = "AttachedTableMissingError";
|
|
26
|
+
this.engineError = engineError;
|
|
23
27
|
}
|
|
24
28
|
};
|
|
25
29
|
const ATTACHED_TABLE_CAPABILITIES = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Grain, Grain as Grain$1, Row, Row as Row$1, TableName, TableName as TableName$1, TenantCtx, TenantCtx as TenantCtx$1 } from "@gscdump/contracts";
|
|
2
1
|
import { BuilderState, SearchType, SearchType as SearchType$1 } from "gscdump/query";
|
|
2
|
+
import { Grain, Grain as Grain$1, Row, Row as Row$1, TableName, TableName as TableName$1, TenantCtx, TenantCtx as TenantCtx$1 } from "@gscdump/contracts";
|
|
3
3
|
/**
|
|
4
4
|
* Per-tier age threshold in days. Default ladder collapses on these gates:
|
|
5
5
|
* - raw → d7 once a daily file is older than `raw` days (default 7).
|
|
@@ -238,7 +238,9 @@ function createFilesystemManifestStore(opts) {
|
|
|
238
238
|
factor: 1.5
|
|
239
239
|
}
|
|
240
240
|
});
|
|
241
|
-
return await fn().finally(() => release().catch(() => {
|
|
241
|
+
return await fn().finally(() => release().catch((releaseErr) => {
|
|
242
|
+
console.warn(`[gscdump/engine] failed to release lock ${path}; it will go stale after 30000ms`, releaseErr);
|
|
243
|
+
}));
|
|
242
244
|
},
|
|
243
245
|
async purgeTenant(filter) {
|
|
244
246
|
return enqueue(async () => {
|
package/dist/adapters/node.d.mts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { DataSource, StorageEngine } from "../_chunks/storage.mjs";
|
|
2
2
|
import { NodeDuckDBOptions, createNodeDuckDBHandle, resetNodeDuckDB } from "./duckdb-node.mjs";
|
|
3
|
+
import { EngineError } from "../_chunks/errors.mjs";
|
|
3
4
|
import { SnapshotIndex } from "../_chunks/snapshot.mjs";
|
|
4
|
-
import {
|
|
5
|
+
import { Result } from "gscdump/result";
|
|
5
6
|
import { SearchType } from "gscdump/query";
|
|
7
|
+
import { Row, TableName } from "@gscdump/contracts";
|
|
6
8
|
interface NodeHarnessOptions {
|
|
7
9
|
dataDir: string;
|
|
8
10
|
/** Tenant user id. Defaults to `'local'` for single-user CLI installs. */
|
|
@@ -72,6 +74,14 @@ interface AttachSnapshotResult {
|
|
|
72
74
|
* `cold_2024_09`. `hot.duckdb` → `hot`.
|
|
73
75
|
*/
|
|
74
76
|
declare function snapshotAlias(fileName: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Errors-as-values core: validates the snapshot index and presigned-URL map,
|
|
79
|
+
* returning a typed `EngineError` for every modelled failure (bad index version,
|
|
80
|
+
* unsafe schema/YYYY-MM identifier, missing attach URL). Underlying DuckDB
|
|
81
|
+
* `runner` IO failures stay defects and propagate. `attachSnapshotIndex` is the
|
|
82
|
+
* throwing wrapper.
|
|
83
|
+
*/
|
|
84
|
+
declare function attachSnapshotIndexResult(runner: SnapshotQueryRunner, opts: AttachSnapshotOptions): Promise<Result<AttachSnapshotResult, EngineError>>;
|
|
75
85
|
declare function attachSnapshotIndex(runner: SnapshotQueryRunner, opts: AttachSnapshotOptions): Promise<AttachSnapshotResult>;
|
|
76
86
|
interface AttachParquetIndexOptions {
|
|
77
87
|
/**
|
|
@@ -94,4 +104,4 @@ interface AttachParquetIndexResult {
|
|
|
94
104
|
tables: string[];
|
|
95
105
|
}
|
|
96
106
|
declare function attachParquetIndex(runner: SnapshotQueryRunner, opts: AttachParquetIndexOptions): Promise<AttachParquetIndexResult>;
|
|
97
|
-
export { type AttachParquetIndexOptions, type AttachParquetIndexResult, type AttachSnapshotOptions, type AttachSnapshotResult, type NodeDuckDBOptions, type NodeHarness, type NodeHarnessOptions, type SnapshotQueryRunner, attachParquetIndex, attachSnapshotIndex, createNodeDuckDBHandle, createNodeHarness, resetNodeDuckDB, snapshotAlias };
|
|
107
|
+
export { type AttachParquetIndexOptions, type AttachParquetIndexResult, type AttachSnapshotOptions, type AttachSnapshotResult, type NodeDuckDBOptions, type NodeHarness, type NodeHarnessOptions, type SnapshotQueryRunner, attachParquetIndex, attachSnapshotIndex, attachSnapshotIndexResult, createNodeDuckDBHandle, createNodeHarness, resetNodeDuckDB, snapshotAlias };
|
package/dist/adapters/node.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { engineErrors } from "../errors.mjs";
|
|
1
2
|
import { createDuckDBCodec, createDuckDBExecutor, createStorageEngine } from "../_chunks/engine.mjs";
|
|
2
3
|
import { createNodeDuckDBHandle, resetNodeDuckDB } from "./duckdb-node.mjs";
|
|
3
4
|
import { createFilesystemDataSource, createFilesystemManifestStore } from "./filesystem.mjs";
|
|
4
5
|
import { encodeSiteId } from "gscdump";
|
|
6
|
+
import { err, ok, unwrapResult } from "gscdump/result";
|
|
5
7
|
import path from "node:path";
|
|
6
8
|
function createNodeHarness(opts) {
|
|
7
9
|
const dataDir = opts.dataDir;
|
|
@@ -72,20 +74,31 @@ function snapshotAlias(fileName) {
|
|
|
72
74
|
if (!m?.[1]) throw new TypeError(`snapshotAlias: unrecognised filename ${JSON.stringify(fileName)}`);
|
|
73
75
|
return `cold_${m[1].replace("-", "_")}`;
|
|
74
76
|
}
|
|
75
|
-
|
|
77
|
+
const SNAPSHOT_TYPE_ERROR_KINDS = new Set([
|
|
78
|
+
"invalid-snapshot-filename",
|
|
79
|
+
"unsupported-snapshot-index-version",
|
|
80
|
+
"invalid-schema-identifier",
|
|
81
|
+
"invalid-year-month"
|
|
82
|
+
]);
|
|
83
|
+
function snapshotAttachErrorToException(error) {
|
|
84
|
+
const exception = SNAPSHOT_TYPE_ERROR_KINDS.has(error.kind) ? new TypeError(error.message) : new Error(error.message);
|
|
85
|
+
exception.engineError = error;
|
|
86
|
+
return exception;
|
|
87
|
+
}
|
|
88
|
+
async function attachSnapshotIndexResult(runner, opts) {
|
|
76
89
|
const { index, attachUrls } = opts;
|
|
77
90
|
const schema = opts.schema ?? "main";
|
|
78
91
|
const forceDownload = opts.forceDownload !== false;
|
|
79
|
-
if (index?.version !== 1)
|
|
80
|
-
if (!SCHEMA_IDENT_RE.test(schema))
|
|
81
|
-
for (const ym of index.cold) if (!YEAR_MONTH_RE.test(ym))
|
|
92
|
+
if (index?.version !== 1) return err(engineErrors.unsupportedSnapshotIndexVersion(index?.version));
|
|
93
|
+
if (!SCHEMA_IDENT_RE.test(schema)) return err(engineErrors.invalidSchemaIdentifier(schema));
|
|
94
|
+
for (const ym of index.cold) if (!YEAR_MONTH_RE.test(ym)) return err(engineErrors.invalidYearMonth(ym));
|
|
82
95
|
await runner("LOAD httpfs").catch(() => void 0);
|
|
83
96
|
if (forceDownload) await runner("SET force_download=true");
|
|
84
97
|
const plan = [];
|
|
85
98
|
for (const ym of index.cold) {
|
|
86
99
|
const fileName = `cold-${ym}.duckdb`;
|
|
87
100
|
const url = attachUrls[fileName];
|
|
88
|
-
if (!url)
|
|
101
|
+
if (!url) return err(engineErrors.missingAttachUrl(fileName));
|
|
89
102
|
plan.push({
|
|
90
103
|
fileName,
|
|
91
104
|
alias: snapshotAlias(fileName),
|
|
@@ -95,7 +108,7 @@ async function attachSnapshotIndex(runner, opts) {
|
|
|
95
108
|
if (index.hot) {
|
|
96
109
|
const fileName = "hot.duckdb";
|
|
97
110
|
const url = attachUrls[fileName];
|
|
98
|
-
if (!url)
|
|
111
|
+
if (!url) return err(engineErrors.missingAttachUrl(fileName));
|
|
99
112
|
plan.push({
|
|
100
113
|
fileName,
|
|
101
114
|
alias: snapshotAlias(fileName),
|
|
@@ -125,10 +138,13 @@ async function attachSnapshotIndex(runner, opts) {
|
|
|
125
138
|
await runner(`CREATE OR REPLACE VIEW ${schema}.${table} AS ${aliases.filter((a) => dbsSet.has(a)).map((db) => `SELECT * FROM ${db}.${table}`).join(" UNION ALL BY NAME ")}`);
|
|
126
139
|
tables.push(table);
|
|
127
140
|
}
|
|
128
|
-
return {
|
|
141
|
+
return ok({
|
|
129
142
|
schema,
|
|
130
143
|
aliases,
|
|
131
144
|
tables
|
|
132
|
-
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async function attachSnapshotIndex(runner, opts) {
|
|
148
|
+
return unwrapResult(await attachSnapshotIndexResult(runner, opts), snapshotAttachErrorToException);
|
|
133
149
|
}
|
|
134
|
-
export { attachParquetIndex, attachSnapshotIndex, createNodeDuckDBHandle, createNodeHarness, resetNodeDuckDB, snapshotAlias };
|
|
150
|
+
export { attachParquetIndex, attachSnapshotIndex, attachSnapshotIndexResult, createNodeDuckDBHandle, createNodeHarness, resetNodeDuckDB, snapshotAlias };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { inferLegacyTier, inferSearchType } from "../_chunks/layout.mjs";
|
|
2
|
+
import { engineErrorToException, engineErrors } from "../errors.mjs";
|
|
3
|
+
import { err, ok, unwrapResult } from "gscdump/result";
|
|
2
4
|
const SHARD_RE = /^u_[^/]+\/manifest\/(?<siteId>[^/]+)\/(?<table>[^/]+)\/HEAD$/;
|
|
3
5
|
const CAS_BACKOFF_BASE_MS = 5;
|
|
4
6
|
const CAS_BACKOFF_CAP_MS = 250;
|
|
@@ -83,14 +85,14 @@ function createR2ManifestStore(opts) {
|
|
|
83
85
|
headEtag: head.etag
|
|
84
86
|
};
|
|
85
87
|
}
|
|
86
|
-
async function
|
|
88
|
+
async function writeShardResult(siteId, table, snapshot, headEtag, attempt) {
|
|
87
89
|
const id = newSnapshotId();
|
|
88
90
|
const snapKey = snapshotKey(userId, siteId, table, id);
|
|
89
91
|
await bucket.put(snapKey, JSON.stringify(snapshot));
|
|
90
92
|
const conditional = headEtag ? { onlyIf: { etagMatches: headEtag } } : { onlyIf: { etagDoesNotMatch: "*" } };
|
|
91
|
-
return
|
|
93
|
+
return await bucket.put(headKey(userId, siteId, table), id, conditional) !== null ? ok(void 0) : err(engineErrors.manifestCasRoundLost(siteId, table, attempt));
|
|
92
94
|
}
|
|
93
|
-
async function
|
|
95
|
+
async function mutateShardResult(siteId, table, mutate) {
|
|
94
96
|
let attempt = 0;
|
|
95
97
|
while (attempt < maxRetries) {
|
|
96
98
|
onEvent?.({
|
|
@@ -101,15 +103,15 @@ function createR2ManifestStore(opts) {
|
|
|
101
103
|
});
|
|
102
104
|
const { snapshot, headEtag } = await readShard(siteId, table);
|
|
103
105
|
await mutate(snapshot);
|
|
104
|
-
const
|
|
105
|
-
if (ok) {
|
|
106
|
+
const round = await writeShardResult(siteId, table, snapshot, headEtag, attempt);
|
|
107
|
+
if (round.ok) {
|
|
106
108
|
onEvent?.({
|
|
107
109
|
kind: "cas-committed",
|
|
108
110
|
siteId,
|
|
109
111
|
table,
|
|
110
112
|
attempts: attempt + 1
|
|
111
113
|
});
|
|
112
|
-
return;
|
|
114
|
+
return round;
|
|
113
115
|
}
|
|
114
116
|
onEvent?.({
|
|
115
117
|
kind: "cas-rejected",
|
|
@@ -120,7 +122,10 @@ function createR2ManifestStore(opts) {
|
|
|
120
122
|
attempt++;
|
|
121
123
|
if (attempt < maxRetries) await casBackoff(attempt);
|
|
122
124
|
}
|
|
123
|
-
|
|
125
|
+
return err(engineErrors.manifestCasExhausted(siteId, table, maxRetries));
|
|
126
|
+
}
|
|
127
|
+
async function mutateShard(siteId, table, mutate) {
|
|
128
|
+
return unwrapResult(await mutateShardResult(siteId, table, mutate), engineErrorToException);
|
|
124
129
|
}
|
|
125
130
|
async function listShards() {
|
|
126
131
|
const shards = [];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { EngineError } from "../_chunks/errors.mjs";
|
|
1
2
|
import { AnalysisParams, AnalysisResult } from "../_chunks/analysis-types.mjs";
|
|
2
3
|
import { AnalysisQuerySource, Analyzer, AnalyzerRegistry, AnalyzerRegistryInit, AnalyzerVariants, BuildContext, DefineAnalyzerOptions, DefinedAnalyzer, Plan, ReduceContext, ReduceCtx, Reducer, RequiredCapability, RowQueriesPlan, SqlExtraQuery, SqlPlan, SqlPlanSpec, TypedRowQuery, createAnalyzerRegistry, defineAnalyzer, requireAdapter } from "../_chunks/registry.mjs";
|
|
3
4
|
declare class AnalyzerCapabilityError extends Error {
|
|
4
5
|
readonly tool: string;
|
|
5
6
|
readonly missing: readonly RequiredCapability[];
|
|
7
|
+
readonly engineError: EngineError;
|
|
6
8
|
constructor(tool: string, missing: readonly RequiredCapability[]);
|
|
7
9
|
}
|
|
8
10
|
/**
|