@prisma-next/sql-runtime 0.5.0-dev.2 → 0.5.0-dev.20
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 +29 -21
- package/dist/{exports-BQZSVXXt.mjs → exports-BET5HxxT.mjs} +552 -171
- package/dist/exports-BET5HxxT.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-Df2GsLSH.d.mts} +65 -16
- package/dist/index-Df2GsLSH.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/test/utils.d.mts +6 -5
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +10 -4
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +12 -14
- package/src/codecs/decoding.ts +172 -116
- package/src/codecs/encoding.ts +59 -21
- package/src/exports/index.ts +11 -7
- package/src/fingerprint.ts +22 -0
- package/src/guardrails/raw.ts +214 -0
- package/src/lower-sql-plan.ts +3 -3
- package/src/marker.ts +75 -0
- package/src/middleware/before-compile-chain.ts +32 -1
- package/src/middleware/budgets.ts +14 -11
- package/src/middleware/lints.ts +3 -3
- package/src/middleware/sql-middleware.ts +6 -5
- package/src/runtime-spi.ts +44 -0
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +62 -47
- package/src/sql-runtime.ts +271 -110
- package/dist/exports-BQZSVXXt.mjs.map +0 -1
- package/dist/index-yb51L_1h.d.mts.map +0 -1
- package/test/async-iterable-result.test.ts +0 -141
- package/test/before-compile-chain.test.ts +0 -223
- package/test/budgets.test.ts +0 -431
- package/test/context.types.test-d.ts +0 -68
- package/test/execution-stack.test.ts +0 -161
- package/test/json-schema-validation.test.ts +0 -571
- package/test/lints.test.ts +0 -160
- package/test/mutation-default-generators.test.ts +0 -254
- package/test/parameterized-types.test.ts +0 -529
- package/test/sql-context.test.ts +0 -384
- package/test/sql-family-adapter.test.ts +0 -103
- package/test/sql-runtime.test.ts +0 -792
- package/test/utils.ts +0 -297
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { checkMiddlewareCompatibility, runtimeError } from "@prisma-next/framework-components/runtime";
|
|
1
|
+
import { AsyncIterableResult, RuntimeCore, checkMiddlewareCompatibility, isRuntimeError, runWithMiddleware, runtimeError } from "@prisma-next/framework-components/runtime";
|
|
2
|
+
import { type } from "arktype";
|
|
2
3
|
import { createCodecRegistry, isQueryAst } from "@prisma-next/sql-relational-core/ast";
|
|
3
|
-
import { AsyncIterableResult, createRuntimeCore, evaluateRawGuardrails, runtimeError as runtimeError$1 } from "@prisma-next/runtime-executor";
|
|
4
4
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
5
5
|
import { checkContractComponentRequirements } from "@prisma-next/framework-components/components";
|
|
6
6
|
import { createExecutionStack } from "@prisma-next/framework-components/execution";
|
|
7
7
|
import { createSqlOperationRegistry } from "@prisma-next/sql-operations";
|
|
8
|
-
import {
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
9
|
|
|
10
10
|
//#region src/codecs/validation.ts
|
|
11
11
|
function extractCodecIds(contract) {
|
|
@@ -73,6 +73,51 @@ function lowerSqlPlan(adapter, contract, queryPlan) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/marker.ts
|
|
78
|
+
const MetaSchema = type({ "[string]": "unknown" });
|
|
79
|
+
function parseMeta(meta) {
|
|
80
|
+
if (meta === null || meta === void 0) return {};
|
|
81
|
+
let parsed;
|
|
82
|
+
if (typeof meta === "string") try {
|
|
83
|
+
parsed = JSON.parse(meta);
|
|
84
|
+
} catch {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
else parsed = meta;
|
|
88
|
+
const result = MetaSchema(parsed);
|
|
89
|
+
if (result instanceof type.errors) return {};
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
const ContractMarkerRowSchema = type({
|
|
93
|
+
core_hash: "string",
|
|
94
|
+
profile_hash: "string",
|
|
95
|
+
"contract_json?": "unknown | null",
|
|
96
|
+
"canonical_version?": "number | null",
|
|
97
|
+
"updated_at?": "Date | string",
|
|
98
|
+
"app_tag?": "string | null",
|
|
99
|
+
"meta?": "unknown | null",
|
|
100
|
+
invariants: type("string").array()
|
|
101
|
+
});
|
|
102
|
+
function parseContractMarkerRow(row) {
|
|
103
|
+
const result = ContractMarkerRowSchema(row);
|
|
104
|
+
if (result instanceof type.errors) {
|
|
105
|
+
const messages = result.map((p) => p.message).join("; ");
|
|
106
|
+
throw new Error(`Invalid contract marker row: ${messages}`);
|
|
107
|
+
}
|
|
108
|
+
const updatedAt = result.updated_at ? result.updated_at instanceof Date ? result.updated_at : new Date(result.updated_at) : /* @__PURE__ */ new Date();
|
|
109
|
+
return {
|
|
110
|
+
storageHash: result.core_hash,
|
|
111
|
+
profileHash: result.profile_hash,
|
|
112
|
+
contractJson: result.contract_json ?? null,
|
|
113
|
+
canonicalVersion: result.canonical_version ?? null,
|
|
114
|
+
updatedAt,
|
|
115
|
+
appTag: result.app_tag ?? null,
|
|
116
|
+
meta: parseMeta(result.meta),
|
|
117
|
+
invariants: result.invariants
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
76
121
|
//#endregion
|
|
77
122
|
//#region src/middleware/budgets.ts
|
|
78
123
|
function hasAggregateWithoutGroupBy(ast) {
|
|
@@ -202,6 +247,95 @@ function budgets(options) {
|
|
|
202
247
|
}
|
|
203
248
|
}
|
|
204
249
|
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/guardrails/raw.ts
|
|
252
|
+
const SELECT_STAR_REGEX = /select\s+\*/i;
|
|
253
|
+
const LIMIT_REGEX = /\blimit\b/i;
|
|
254
|
+
const MUTATION_PREFIX_REGEX = /^(insert|update|delete|create|alter|drop|truncate)\b/i;
|
|
255
|
+
const READ_ONLY_INTENTS = new Set([
|
|
256
|
+
"read",
|
|
257
|
+
"report",
|
|
258
|
+
"readonly"
|
|
259
|
+
]);
|
|
260
|
+
function evaluateRawGuardrails(plan, config) {
|
|
261
|
+
const lints$1 = [];
|
|
262
|
+
const budgets$1 = [];
|
|
263
|
+
const normalized = normalizeWhitespace(plan.sql);
|
|
264
|
+
const statementType = classifyStatement(normalized);
|
|
265
|
+
if (statementType === "select") {
|
|
266
|
+
if (SELECT_STAR_REGEX.test(normalized)) lints$1.push(createLint("LINT.SELECT_STAR", "error", "Raw SQL plan selects all columns via *", { sql: snippet(plan.sql) }));
|
|
267
|
+
if (!LIMIT_REGEX.test(normalized)) {
|
|
268
|
+
const severity = config?.budgets?.unboundedSelectSeverity ?? "error";
|
|
269
|
+
lints$1.push(createLint("LINT.NO_LIMIT", "warn", "Raw SQL plan omits LIMIT clause", { sql: snippet(plan.sql) }));
|
|
270
|
+
budgets$1.push(createBudget("BUDGET.ROWS_EXCEEDED", severity, "Raw SQL plan is unbounded and may exceed row budget", {
|
|
271
|
+
sql: snippet(plan.sql),
|
|
272
|
+
...config?.budgets?.estimatedRows !== void 0 ? { estimatedRows: config.budgets.estimatedRows } : {}
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (isMutationStatement(statementType) && isReadOnlyIntent(plan.meta)) lints$1.push(createLint("LINT.READ_ONLY_MUTATION", "error", "Raw SQL plan mutates data despite read-only intent", {
|
|
277
|
+
sql: snippet(plan.sql),
|
|
278
|
+
intent: plan.meta.annotations?.["intent"]
|
|
279
|
+
}));
|
|
280
|
+
const refs = plan.meta.refs;
|
|
281
|
+
if (refs) evaluateIndexCoverage(refs, lints$1);
|
|
282
|
+
return {
|
|
283
|
+
lints: lints$1,
|
|
284
|
+
budgets: budgets$1,
|
|
285
|
+
statement: statementType
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function evaluateIndexCoverage(refs, lints$1) {
|
|
289
|
+
const predicateColumns = refs.columns ?? [];
|
|
290
|
+
if (predicateColumns.length === 0) return;
|
|
291
|
+
const indexes = refs.indexes ?? [];
|
|
292
|
+
if (indexes.length === 0) {
|
|
293
|
+
lints$1.push(createLint("LINT.UNINDEXED_PREDICATE", "warn", "Raw SQL plan predicates lack supporting indexes", { predicates: predicateColumns }));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (!predicateColumns.every((column) => indexes.some((index) => index.table === column.table && index.columns.some((col) => col.toLowerCase() === column.column.toLowerCase())))) lints$1.push(createLint("LINT.UNINDEXED_PREDICATE", "warn", "Raw SQL plan predicates lack supporting indexes", { predicates: predicateColumns }));
|
|
297
|
+
}
|
|
298
|
+
function classifyStatement(sql) {
|
|
299
|
+
const trimmed = sql.trim();
|
|
300
|
+
const lower = trimmed.toLowerCase();
|
|
301
|
+
if (lower.startsWith("with")) {
|
|
302
|
+
if (lower.includes("select")) return "select";
|
|
303
|
+
}
|
|
304
|
+
if (lower.startsWith("select")) return "select";
|
|
305
|
+
if (MUTATION_PREFIX_REGEX.test(trimmed)) return "mutation";
|
|
306
|
+
return "other";
|
|
307
|
+
}
|
|
308
|
+
function isMutationStatement(statement) {
|
|
309
|
+
return statement === "mutation";
|
|
310
|
+
}
|
|
311
|
+
function isReadOnlyIntent(meta) {
|
|
312
|
+
const annotations = meta.annotations;
|
|
313
|
+
const intent = typeof annotations?.intent === "string" ? annotations.intent.toLowerCase() : void 0;
|
|
314
|
+
return intent !== void 0 && READ_ONLY_INTENTS.has(intent);
|
|
315
|
+
}
|
|
316
|
+
function normalizeWhitespace(value) {
|
|
317
|
+
return value.replace(/\s+/g, " ").trim();
|
|
318
|
+
}
|
|
319
|
+
function snippet(sql) {
|
|
320
|
+
return normalizeWhitespace(sql).slice(0, 200);
|
|
321
|
+
}
|
|
322
|
+
function createLint(code, severity, message, details) {
|
|
323
|
+
return {
|
|
324
|
+
code,
|
|
325
|
+
severity,
|
|
326
|
+
message,
|
|
327
|
+
...details ? { details } : {}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function createBudget(code, severity, message, details) {
|
|
331
|
+
return {
|
|
332
|
+
code,
|
|
333
|
+
severity,
|
|
334
|
+
message,
|
|
335
|
+
...details ? { details } : {}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
205
339
|
//#endregion
|
|
206
340
|
//#region src/middleware/lints.ts
|
|
207
341
|
function getFromSourceTableDetail(source) {
|
|
@@ -516,7 +650,8 @@ const ensureTableStatement = {
|
|
|
516
650
|
canonical_version int,
|
|
517
651
|
updated_at timestamptz not null default now(),
|
|
518
652
|
app_tag text,
|
|
519
|
-
meta jsonb not null default '{}'
|
|
653
|
+
meta jsonb not null default '{}',
|
|
654
|
+
invariants text[] not null default '{}'
|
|
520
655
|
)`,
|
|
521
656
|
params: []
|
|
522
657
|
};
|
|
@@ -529,56 +664,82 @@ function readContractMarker() {
|
|
|
529
664
|
canonical_version,
|
|
530
665
|
updated_at,
|
|
531
666
|
app_tag,
|
|
532
|
-
meta
|
|
667
|
+
meta,
|
|
668
|
+
invariants
|
|
533
669
|
from prisma_contract.marker
|
|
534
670
|
where id = $1`,
|
|
535
671
|
params: [1]
|
|
536
672
|
};
|
|
537
673
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
674
|
+
/**
|
|
675
|
+
* Variable columns that participate in INSERT/UPDATE alongside the
|
|
676
|
+
* always-on `id = $1` and `updated_at = now()`. Each column declares
|
|
677
|
+
* its name, optional cast type, and parameter value; the placeholder
|
|
678
|
+
* (`$N`) is computed positionally below — adding or reordering a
|
|
679
|
+
* column doesn't desync indices. `invariants` only appears when the
|
|
680
|
+
* caller supplies it — see `WriteMarkerInput.invariants`.
|
|
681
|
+
*/
|
|
682
|
+
function markerColumns(input) {
|
|
683
|
+
return [
|
|
684
|
+
{
|
|
685
|
+
name: "core_hash",
|
|
686
|
+
param: input.storageHash
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
name: "profile_hash",
|
|
690
|
+
param: input.profileHash
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: "contract_json",
|
|
694
|
+
type: "jsonb",
|
|
695
|
+
param: input.contractJson ?? null
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
name: "canonical_version",
|
|
699
|
+
param: input.canonicalVersion ?? null
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: "app_tag",
|
|
703
|
+
param: input.appTag ?? null
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: "meta",
|
|
707
|
+
type: "jsonb",
|
|
708
|
+
param: JSON.stringify(input.meta ?? {})
|
|
709
|
+
},
|
|
710
|
+
...input.invariants !== void 0 ? [{
|
|
711
|
+
name: "invariants",
|
|
712
|
+
type: "text[]",
|
|
713
|
+
param: input.invariants
|
|
714
|
+
}] : []
|
|
547
715
|
];
|
|
716
|
+
}
|
|
717
|
+
function writeContractMarker(input) {
|
|
718
|
+
const placed = markerColumns(input).map((c, i) => ({
|
|
719
|
+
name: c.name,
|
|
720
|
+
expr: c.type ? `$${i + 2}::${c.type}` : `$${i + 2}`,
|
|
721
|
+
param: c.param
|
|
722
|
+
}));
|
|
723
|
+
const params = [1, ...placed.map((c) => c.param)];
|
|
724
|
+
const insertColumns = [
|
|
725
|
+
"id",
|
|
726
|
+
...placed.map((c) => c.name),
|
|
727
|
+
"updated_at"
|
|
728
|
+
].join(", ");
|
|
729
|
+
const insertValues = [
|
|
730
|
+
"$1",
|
|
731
|
+
...placed.map((c) => c.expr),
|
|
732
|
+
"now()"
|
|
733
|
+
].join(", ");
|
|
734
|
+
const setClauses = [...placed.map((c) => `${c.name} = ${c.expr}`), "updated_at = now()"].join(", ");
|
|
548
735
|
return {
|
|
549
736
|
insert: {
|
|
550
|
-
sql: `insert into prisma_contract.marker (
|
|
551
|
-
|
|
552
|
-
core_hash,
|
|
553
|
-
profile_hash,
|
|
554
|
-
contract_json,
|
|
555
|
-
canonical_version,
|
|
556
|
-
updated_at,
|
|
557
|
-
app_tag,
|
|
558
|
-
meta
|
|
559
|
-
) values (
|
|
560
|
-
$1,
|
|
561
|
-
$2,
|
|
562
|
-
$3,
|
|
563
|
-
$4::jsonb,
|
|
564
|
-
$5,
|
|
565
|
-
now(),
|
|
566
|
-
$6,
|
|
567
|
-
$7::jsonb
|
|
568
|
-
)`,
|
|
569
|
-
params: baseParams
|
|
737
|
+
sql: `insert into prisma_contract.marker (${insertColumns}) values (${insertValues})`,
|
|
738
|
+
params
|
|
570
739
|
},
|
|
571
740
|
update: {
|
|
572
|
-
sql: `update prisma_contract.marker set
|
|
573
|
-
|
|
574
|
-
profile_hash = $3,
|
|
575
|
-
contract_json = $4::jsonb,
|
|
576
|
-
canonical_version = $5,
|
|
577
|
-
updated_at = now(),
|
|
578
|
-
app_tag = $6,
|
|
579
|
-
meta = $7::jsonb
|
|
580
|
-
where id = $1`,
|
|
581
|
-
params: baseParams
|
|
741
|
+
sql: `update prisma_contract.marker set ${setClauses} where id = $1`,
|
|
742
|
+
params
|
|
582
743
|
}
|
|
583
744
|
};
|
|
584
745
|
}
|
|
@@ -619,6 +780,7 @@ function formatErrorSummary(errors) {
|
|
|
619
780
|
|
|
620
781
|
//#endregion
|
|
621
782
|
//#region src/codecs/decoding.ts
|
|
783
|
+
const WIRE_PREVIEW_LIMIT = 100;
|
|
622
784
|
function resolveRowCodec(alias, plan, registry) {
|
|
623
785
|
const planCodecId = plan.meta.annotations?.codecs?.[alias];
|
|
624
786
|
if (planCodecId) {
|
|
@@ -662,72 +824,106 @@ function resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex) {
|
|
|
662
824
|
}
|
|
663
825
|
return fallbackColumnRefIndex?.get(alias);
|
|
664
826
|
}
|
|
665
|
-
function
|
|
666
|
-
|
|
827
|
+
function previewWireValue(wireValue) {
|
|
828
|
+
if (typeof wireValue === "string") return wireValue.length > WIRE_PREVIEW_LIMIT ? `${wireValue.substring(0, WIRE_PREVIEW_LIMIT)}...` : wireValue;
|
|
829
|
+
return String(wireValue).substring(0, WIRE_PREVIEW_LIMIT);
|
|
830
|
+
}
|
|
831
|
+
function isJsonSchemaValidationError(error) {
|
|
832
|
+
return isRuntimeError(error) && error.code === "RUNTIME.JSON_SCHEMA_VALIDATION_FAILED";
|
|
833
|
+
}
|
|
834
|
+
function wrapDecodeFailure(error, alias, ref, codec$1, wireValue) {
|
|
835
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
836
|
+
const wrapped = runtimeError("RUNTIME.DECODE_FAILED", `Failed to decode column ${ref ? `${ref.table}.${ref.column}` : alias} with codec '${codec$1.id}': ${message}`, {
|
|
837
|
+
...ref ? {
|
|
838
|
+
table: ref.table,
|
|
839
|
+
column: ref.column
|
|
840
|
+
} : { alias },
|
|
841
|
+
codec: codec$1.id,
|
|
842
|
+
wirePreview: previewWireValue(wireValue)
|
|
843
|
+
});
|
|
844
|
+
wrapped.cause = error;
|
|
845
|
+
throw wrapped;
|
|
846
|
+
}
|
|
847
|
+
function wrapIncludeAggregateFailure(error, alias, wireValue) {
|
|
848
|
+
const wrapped = runtimeError("RUNTIME.DECODE_FAILED", `Failed to parse JSON array for include alias '${alias}': ${error instanceof Error ? error.message : String(error)}`, {
|
|
849
|
+
alias,
|
|
850
|
+
wirePreview: previewWireValue(wireValue)
|
|
851
|
+
});
|
|
852
|
+
wrapped.cause = error;
|
|
853
|
+
throw wrapped;
|
|
854
|
+
}
|
|
855
|
+
function decodeIncludeAggregate(alias, wireValue) {
|
|
856
|
+
if (wireValue === null || wireValue === void 0) return [];
|
|
857
|
+
try {
|
|
858
|
+
let parsed;
|
|
859
|
+
if (typeof wireValue === "string") parsed = JSON.parse(wireValue);
|
|
860
|
+
else if (Array.isArray(wireValue)) parsed = wireValue;
|
|
861
|
+
else parsed = JSON.parse(String(wireValue));
|
|
862
|
+
if (!Array.isArray(parsed)) throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
|
|
863
|
+
return parsed;
|
|
864
|
+
} catch (error) {
|
|
865
|
+
wrapIncludeAggregateFailure(error, alias, wireValue);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Decodes a single field. Single-armed: every cell takes the same path —
|
|
870
|
+
* `codec.decode → await → JSON-Schema validate → return plain value` — so
|
|
871
|
+
* sync- and async-authored codecs are indistinguishable to callers.
|
|
872
|
+
*/
|
|
873
|
+
async function decodeField(alias, wireValue, plan, registry, jsonValidators, projection, fallbackColumnRefIndex) {
|
|
874
|
+
if (wireValue === null || wireValue === void 0) return wireValue;
|
|
875
|
+
const codec$1 = resolveRowCodec(alias, plan, registry);
|
|
876
|
+
if (!codec$1) return wireValue;
|
|
877
|
+
const ref = resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex);
|
|
878
|
+
let decoded;
|
|
879
|
+
try {
|
|
880
|
+
decoded = await codec$1.decode(wireValue);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
wrapDecodeFailure(error, alias, ref, codec$1, wireValue);
|
|
883
|
+
}
|
|
884
|
+
if (jsonValidators && ref) try {
|
|
885
|
+
validateJsonValue(jsonValidators, ref.table, ref.column, decoded, "decode", codec$1.id);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if (isJsonSchemaValidationError(error)) throw error;
|
|
888
|
+
wrapDecodeFailure(error, alias, ref, codec$1, wireValue);
|
|
889
|
+
}
|
|
890
|
+
return decoded;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Decodes a row by dispatching all per-cell codec calls concurrently via
|
|
894
|
+
* `Promise.all`. Each cell follows the single-armed `decodeField` path.
|
|
895
|
+
* Failures are wrapped in `RUNTIME.DECODE_FAILED` with `{ table, column,
|
|
896
|
+
* codec }` (or `{ alias, codec }` when no column ref is resolvable) and the
|
|
897
|
+
* original error attached on `cause`.
|
|
898
|
+
*/
|
|
899
|
+
async function decodeRow(row, plan, registry, jsonValidators) {
|
|
667
900
|
const projection = plan.meta.projection;
|
|
668
|
-
const fallbackColumnRefIndex =
|
|
901
|
+
const fallbackColumnRefIndex = !projection || Array.isArray(projection) ? buildColumnRefIndex(plan) : null;
|
|
669
902
|
let aliases;
|
|
670
903
|
if (projection && !Array.isArray(projection)) aliases = Object.keys(projection);
|
|
671
904
|
else if (projection && Array.isArray(projection)) aliases = projection;
|
|
672
905
|
else aliases = Object.keys(row);
|
|
673
|
-
|
|
906
|
+
const tasks = [];
|
|
907
|
+
const includeIndices = [];
|
|
908
|
+
for (let i = 0; i < aliases.length; i++) {
|
|
909
|
+
const alias = aliases[i];
|
|
674
910
|
const wireValue = row[alias];
|
|
675
911
|
const projectionValue = projection && typeof projection === "object" && !Array.isArray(projection) ? projection[alias] : void 0;
|
|
676
912
|
if (typeof projectionValue === "string" && projectionValue.startsWith("include:")) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
try {
|
|
682
|
-
let parsed;
|
|
683
|
-
if (typeof wireValue === "string") parsed = JSON.parse(wireValue);
|
|
684
|
-
else if (Array.isArray(wireValue)) parsed = wireValue;
|
|
685
|
-
else parsed = JSON.parse(String(wireValue));
|
|
686
|
-
if (!Array.isArray(parsed)) throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
|
|
687
|
-
decoded[alias] = parsed;
|
|
688
|
-
} catch (error) {
|
|
689
|
-
const decodeError = /* @__PURE__ */ new Error(`Failed to parse JSON array for include alias '${alias}': ${error instanceof Error ? error.message : String(error)}`);
|
|
690
|
-
decodeError.code = "RUNTIME.DECODE_FAILED";
|
|
691
|
-
decodeError.category = "RUNTIME";
|
|
692
|
-
decodeError.severity = "error";
|
|
693
|
-
decodeError.details = {
|
|
694
|
-
alias,
|
|
695
|
-
wirePreview: typeof wireValue === "string" && wireValue.length > 100 ? `${wireValue.substring(0, 100)}...` : String(wireValue).substring(0, 100)
|
|
696
|
-
};
|
|
697
|
-
throw decodeError;
|
|
698
|
-
}
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
if (wireValue === null || wireValue === void 0) {
|
|
702
|
-
decoded[alias] = wireValue;
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
const codec$1 = resolveRowCodec(alias, plan, registry);
|
|
706
|
-
if (!codec$1) {
|
|
707
|
-
decoded[alias] = wireValue;
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
try {
|
|
711
|
-
const decodedValue = codec$1.decode(wireValue);
|
|
712
|
-
if (jsonValidators) {
|
|
713
|
-
const ref = resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex);
|
|
714
|
-
if (ref) validateJsonValue(jsonValidators, ref.table, ref.column, decodedValue, "decode", codec$1.id);
|
|
715
|
-
}
|
|
716
|
-
decoded[alias] = decodedValue;
|
|
717
|
-
} catch (error) {
|
|
718
|
-
if (error instanceof Error && "code" in error && error.code === "RUNTIME.JSON_SCHEMA_VALIDATION_FAILED") throw error;
|
|
719
|
-
const decodeError = /* @__PURE__ */ new Error(`Failed to decode row alias '${alias}' with codec '${codec$1.id}': ${error instanceof Error ? error.message : String(error)}`);
|
|
720
|
-
decodeError.code = "RUNTIME.DECODE_FAILED";
|
|
721
|
-
decodeError.category = "RUNTIME";
|
|
722
|
-
decodeError.severity = "error";
|
|
723
|
-
decodeError.details = {
|
|
913
|
+
includeIndices.push({
|
|
914
|
+
index: i,
|
|
724
915
|
alias,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
916
|
+
value: wireValue
|
|
917
|
+
});
|
|
918
|
+
tasks.push(Promise.resolve(void 0));
|
|
919
|
+
continue;
|
|
729
920
|
}
|
|
921
|
+
tasks.push(decodeField(alias, wireValue, plan, registry, jsonValidators, projection, fallbackColumnRefIndex));
|
|
730
922
|
}
|
|
923
|
+
const settled = await Promise.all(tasks);
|
|
924
|
+
for (const entry of includeIndices) settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value);
|
|
925
|
+
const decoded = {};
|
|
926
|
+
for (let i = 0; i < aliases.length; i++) decoded[aliases[i]] = settled[i];
|
|
731
927
|
return decoded;
|
|
732
928
|
}
|
|
733
929
|
|
|
@@ -740,30 +936,78 @@ function resolveParamCodec(paramDescriptor, registry) {
|
|
|
740
936
|
}
|
|
741
937
|
return null;
|
|
742
938
|
}
|
|
743
|
-
function
|
|
939
|
+
function paramLabel(paramDescriptor, paramIndex) {
|
|
940
|
+
return paramDescriptor.name ?? `param[${paramIndex}]`;
|
|
941
|
+
}
|
|
942
|
+
function wrapEncodeFailure(error, paramDescriptor, paramIndex, codecId) {
|
|
943
|
+
const label = paramLabel(paramDescriptor, paramIndex);
|
|
944
|
+
const wrapped = runtimeError("RUNTIME.ENCODE_FAILED", `Failed to encode parameter ${label} with codec '${codecId}': ${error instanceof Error ? error.message : String(error)}`, {
|
|
945
|
+
label,
|
|
946
|
+
codec: codecId,
|
|
947
|
+
paramIndex
|
|
948
|
+
});
|
|
949
|
+
wrapped.cause = error;
|
|
950
|
+
throw wrapped;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Encodes a single parameter through its codec. Always awaits codec.encode so
|
|
954
|
+
* a Promise can never leak into the driver, even if a sync-authored codec is
|
|
955
|
+
* lifted to async by the codec() factory. Failures are wrapped in
|
|
956
|
+
* `RUNTIME.ENCODE_FAILED` with `{ label, codec, paramIndex }` and the original
|
|
957
|
+
* error attached on `cause`.
|
|
958
|
+
*/
|
|
959
|
+
async function encodeParam(value, paramDescriptor, paramIndex, registry) {
|
|
744
960
|
if (value === null || value === void 0) return null;
|
|
745
961
|
const codec$1 = resolveParamCodec(paramDescriptor, registry);
|
|
746
962
|
if (!codec$1) return value;
|
|
747
|
-
|
|
748
|
-
return codec$1.encode(value);
|
|
963
|
+
try {
|
|
964
|
+
return await codec$1.encode(value);
|
|
749
965
|
} catch (error) {
|
|
750
|
-
|
|
751
|
-
throw new Error(`Failed to encode parameter ${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
966
|
+
wrapEncodeFailure(error, paramDescriptor, paramIndex, codec$1.id);
|
|
752
967
|
}
|
|
753
|
-
return value;
|
|
754
968
|
}
|
|
755
|
-
|
|
969
|
+
/**
|
|
970
|
+
* Encodes all parameters concurrently via `Promise.all`. Per parameter, sync-
|
|
971
|
+
* and async-authored codecs share the same path: `codec.encode → await →
|
|
972
|
+
* return`. Param-level failures are wrapped in `RUNTIME.ENCODE_FAILED`.
|
|
973
|
+
*/
|
|
974
|
+
async function encodeParams(plan, registry) {
|
|
756
975
|
if (plan.params.length === 0) return plan.params;
|
|
757
|
-
const
|
|
758
|
-
|
|
976
|
+
const descriptorCount = plan.meta.paramDescriptors.length;
|
|
977
|
+
const paramCount = plan.params.length;
|
|
978
|
+
const tasks = new Array(paramCount);
|
|
979
|
+
for (let i = 0; i < paramCount; i++) {
|
|
759
980
|
const paramValue = plan.params[i];
|
|
760
981
|
const paramDescriptor = plan.meta.paramDescriptors[i];
|
|
761
|
-
if (paramDescriptor)
|
|
762
|
-
|
|
982
|
+
if (!paramDescriptor) throw runtimeError("RUNTIME.MISSING_PARAM_DESCRIPTOR", `Missing paramDescriptor for parameter at index ${i} (plan has ${paramCount} params, ${descriptorCount} descriptors). The planner must emit one descriptor per param; this is a contract violation.`, {
|
|
983
|
+
paramIndex: i,
|
|
984
|
+
paramCount,
|
|
985
|
+
descriptorCount
|
|
986
|
+
});
|
|
987
|
+
tasks[i] = encodeParam(paramValue, paramDescriptor, i, registry);
|
|
763
988
|
}
|
|
989
|
+
const encoded = await Promise.all(tasks);
|
|
764
990
|
return Object.freeze(encoded);
|
|
765
991
|
}
|
|
766
992
|
|
|
993
|
+
//#endregion
|
|
994
|
+
//#region src/fingerprint.ts
|
|
995
|
+
const STRING_LITERAL_REGEX = /'(?:''|[^'])*'/g;
|
|
996
|
+
const NUMERIC_LITERAL_REGEX = /\b\d+(?:\.\d+)?\b/g;
|
|
997
|
+
const WHITESPACE_REGEX = /\s+/g;
|
|
998
|
+
/**
|
|
999
|
+
* Computes a literal-stripped, normalized fingerprint of a SQL statement.
|
|
1000
|
+
*
|
|
1001
|
+
* The function strips string and numeric literals, collapses whitespace, and
|
|
1002
|
+
* lowercases the result before hashing — so two structurally equivalent
|
|
1003
|
+
* statements (with different parameter values) produce the same fingerprint.
|
|
1004
|
+
* Used by SQL telemetry to group queries.
|
|
1005
|
+
*/
|
|
1006
|
+
function computeSqlFingerprint(sql) {
|
|
1007
|
+
const normalized = sql.replace(STRING_LITERAL_REGEX, "?").replace(NUMERIC_LITERAL_REGEX, "?").replace(WHITESPACE_REGEX, " ").trim().toLowerCase();
|
|
1008
|
+
return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
767
1011
|
//#endregion
|
|
768
1012
|
//#region src/middleware/before-compile-chain.ts
|
|
769
1013
|
async function runBeforeCompileChain(middleware, initial, ctx) {
|
|
@@ -780,7 +1024,32 @@ async function runBeforeCompileChain(middleware, initial, ctx) {
|
|
|
780
1024
|
});
|
|
781
1025
|
current = result;
|
|
782
1026
|
}
|
|
783
|
-
return current;
|
|
1027
|
+
if (current.ast === initial.ast) return current;
|
|
1028
|
+
const paramDescriptors = deriveParamDescriptorsFromAst(current.ast);
|
|
1029
|
+
const meta = {
|
|
1030
|
+
...current.meta,
|
|
1031
|
+
paramDescriptors
|
|
1032
|
+
};
|
|
1033
|
+
return {
|
|
1034
|
+
ast: current.ast,
|
|
1035
|
+
meta
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function deriveParamDescriptorsFromAst(ast) {
|
|
1039
|
+
const refs = ast.collectParamRefs();
|
|
1040
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1041
|
+
const descriptors = [];
|
|
1042
|
+
for (const ref of refs) {
|
|
1043
|
+
if (seen.has(ref)) continue;
|
|
1044
|
+
seen.add(ref);
|
|
1045
|
+
descriptors.push({
|
|
1046
|
+
index: descriptors.length + 1,
|
|
1047
|
+
...ref.name !== void 0 ? { name: ref.name } : {},
|
|
1048
|
+
source: "dsl",
|
|
1049
|
+
...ref.codecId !== void 0 ? { codecId: ref.codecId } : {}
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return descriptors;
|
|
784
1053
|
}
|
|
785
1054
|
|
|
786
1055
|
//#endregion
|
|
@@ -806,102 +1075,214 @@ var SqlFamilyAdapter = class {
|
|
|
806
1075
|
|
|
807
1076
|
//#endregion
|
|
808
1077
|
//#region src/sql-runtime.ts
|
|
809
|
-
|
|
810
|
-
|
|
1078
|
+
function isExecutionPlan(plan) {
|
|
1079
|
+
return "sql" in plan;
|
|
1080
|
+
}
|
|
1081
|
+
var SqlRuntimeImpl = class extends RuntimeCore {
|
|
811
1082
|
contract;
|
|
812
1083
|
adapter;
|
|
1084
|
+
driver;
|
|
1085
|
+
familyAdapter;
|
|
813
1086
|
codecRegistry;
|
|
814
1087
|
jsonSchemaValidators;
|
|
1088
|
+
sqlCtx;
|
|
1089
|
+
verify;
|
|
815
1090
|
codecRegistryValidated;
|
|
1091
|
+
verified;
|
|
1092
|
+
startupVerified;
|
|
1093
|
+
_telemetry;
|
|
816
1094
|
constructor(options) {
|
|
817
1095
|
const { context, adapter, driver, verify, middleware, mode, log } = options;
|
|
1096
|
+
if (middleware) for (const mw of middleware) checkMiddlewareCompatibility(mw, "sql", context.contract.target);
|
|
1097
|
+
const sqlCtx = {
|
|
1098
|
+
contract: context.contract,
|
|
1099
|
+
mode: mode ?? "strict",
|
|
1100
|
+
now: () => Date.now(),
|
|
1101
|
+
log: log ?? {
|
|
1102
|
+
info: () => {},
|
|
1103
|
+
warn: () => {},
|
|
1104
|
+
error: () => {}
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
super({
|
|
1108
|
+
middleware: middleware ?? [],
|
|
1109
|
+
ctx: sqlCtx
|
|
1110
|
+
});
|
|
818
1111
|
this.contract = context.contract;
|
|
819
1112
|
this.adapter = adapter;
|
|
1113
|
+
this.driver = driver;
|
|
1114
|
+
this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
820
1115
|
this.codecRegistry = context.codecs;
|
|
821
1116
|
this.jsonSchemaValidators = context.jsonSchemaValidators;
|
|
1117
|
+
this.sqlCtx = sqlCtx;
|
|
1118
|
+
this.verify = verify;
|
|
822
1119
|
this.codecRegistryValidated = false;
|
|
823
|
-
|
|
824
|
-
this.
|
|
825
|
-
|
|
826
|
-
driver,
|
|
827
|
-
verify,
|
|
828
|
-
...ifDefined("middleware", middleware),
|
|
829
|
-
...ifDefined("mode", mode),
|
|
830
|
-
...ifDefined("log", log)
|
|
831
|
-
});
|
|
1120
|
+
this.verified = verify.mode === "startup" ? false : verify.mode === "always";
|
|
1121
|
+
this.startupVerified = false;
|
|
1122
|
+
this._telemetry = null;
|
|
832
1123
|
if (verify.mode === "startup") {
|
|
833
1124
|
validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
|
|
834
1125
|
this.codecRegistryValidated = true;
|
|
835
1126
|
}
|
|
836
1127
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1128
|
+
/**
|
|
1129
|
+
* Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with
|
|
1130
|
+
* encoded parameters ready for the driver. This is the single point at
|
|
1131
|
+
* which params transition from app-layer values to driver wire-format.
|
|
1132
|
+
*/
|
|
1133
|
+
async lower(plan) {
|
|
1134
|
+
const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
|
|
1135
|
+
return Object.freeze({
|
|
1136
|
+
...lowered,
|
|
1137
|
+
params: await encodeParams(lowered, this.codecRegistry)
|
|
1138
|
+
});
|
|
842
1139
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1140
|
+
/**
|
|
1141
|
+
* Default driver invocation. Production execution paths override the
|
|
1142
|
+
* queryable target (e.g. transaction or connection) by going through
|
|
1143
|
+
* `executeAgainstQueryable`; this implementation supports any caller of
|
|
1144
|
+
* `super.execute(plan)` and the abstract-base contract.
|
|
1145
|
+
*/
|
|
1146
|
+
runDriver(exec) {
|
|
1147
|
+
return this.driver.execute({
|
|
1148
|
+
sql: exec.sql,
|
|
1149
|
+
params: exec.params
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* SQL pre-compile hook. Runs the registered middleware `beforeCompile`
|
|
1154
|
+
* chain over the plan's draft (AST + meta) and returns a `SqlQueryPlan`
|
|
1155
|
+
* with the rewritten AST and meta when the chain mutates them. The chain
|
|
1156
|
+
* re-derives `meta.paramDescriptors` from the rewritten AST so descriptors
|
|
1157
|
+
* stay in lockstep with the params the adapter will emit during lowering.
|
|
1158
|
+
*/
|
|
1159
|
+
async runBeforeCompile(plan) {
|
|
1160
|
+
const rewrittenDraft = await runBeforeCompileChain(this.middleware, {
|
|
849
1161
|
ast: plan.ast,
|
|
850
1162
|
meta: plan.meta
|
|
851
|
-
}, this.
|
|
852
|
-
|
|
1163
|
+
}, this.sqlCtx);
|
|
1164
|
+
return rewrittenDraft.ast === plan.ast ? plan : {
|
|
853
1165
|
...plan,
|
|
854
|
-
ast: rewrittenDraft.ast
|
|
1166
|
+
ast: rewrittenDraft.ast,
|
|
1167
|
+
meta: rewrittenDraft.meta
|
|
855
1168
|
};
|
|
856
|
-
|
|
1169
|
+
}
|
|
1170
|
+
execute(plan) {
|
|
1171
|
+
return this.executeAgainstQueryable(plan, this.driver);
|
|
857
1172
|
}
|
|
858
1173
|
executeAgainstQueryable(plan, queryable) {
|
|
859
|
-
this.ensureCodecRegistryValidated(
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1174
|
+
this.ensureCodecRegistryValidated();
|
|
1175
|
+
const self = this;
|
|
1176
|
+
const generator = async function* () {
|
|
1177
|
+
const exec = isExecutionPlan(plan) ? Object.freeze({
|
|
1178
|
+
...plan,
|
|
1179
|
+
params: await encodeParams(plan, self.codecRegistry)
|
|
1180
|
+
}) : await self.lower(await self.runBeforeCompile(plan));
|
|
1181
|
+
self.familyAdapter.validatePlan(exec, self.contract);
|
|
1182
|
+
self._telemetry = null;
|
|
1183
|
+
if (!self.startupVerified && self.verify.mode === "startup") await self.verifyMarker();
|
|
1184
|
+
if (!self.verified && self.verify.mode === "onFirstUse") await self.verifyMarker();
|
|
1185
|
+
const startedAt = Date.now();
|
|
1186
|
+
let outcome = null;
|
|
1187
|
+
try {
|
|
1188
|
+
if (self.verify.mode === "always") await self.verifyMarker();
|
|
1189
|
+
const stream = runWithMiddleware(exec, self.middleware, self.ctx, () => queryable.execute({
|
|
1190
|
+
sql: exec.sql,
|
|
1191
|
+
params: exec.params
|
|
1192
|
+
}));
|
|
1193
|
+
for await (const rawRow of stream) yield await decodeRow(rawRow, exec, self.codecRegistry, self.jsonSchemaValidators);
|
|
1194
|
+
outcome = "success";
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
outcome = "runtime-error";
|
|
1197
|
+
throw error;
|
|
1198
|
+
} finally {
|
|
1199
|
+
if (outcome !== null) self.recordTelemetry(exec, outcome, Date.now() - startedAt);
|
|
1200
|
+
}
|
|
869
1201
|
};
|
|
870
|
-
return new AsyncIterableResult(
|
|
871
|
-
}
|
|
872
|
-
execute(plan) {
|
|
873
|
-
return this.executeAgainstQueryable(plan, this.core);
|
|
1202
|
+
return new AsyncIterableResult(generator());
|
|
874
1203
|
}
|
|
875
1204
|
async connection() {
|
|
876
|
-
const
|
|
1205
|
+
const driverConn = await this.driver.acquireConnection();
|
|
877
1206
|
const self = this;
|
|
878
1207
|
return {
|
|
879
1208
|
async transaction() {
|
|
880
|
-
const
|
|
881
|
-
return
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1209
|
+
const driverTx = await driverConn.beginTransaction();
|
|
1210
|
+
return self.wrapTransaction(driverTx);
|
|
1211
|
+
},
|
|
1212
|
+
async release() {
|
|
1213
|
+
await driverConn.release();
|
|
1214
|
+
},
|
|
1215
|
+
async destroy(reason) {
|
|
1216
|
+
await driverConn.destroy(reason);
|
|
888
1217
|
},
|
|
889
|
-
release: coreConn.release.bind(coreConn),
|
|
890
|
-
destroy: coreConn.destroy.bind(coreConn),
|
|
891
1218
|
execute(plan) {
|
|
892
|
-
return self.executeAgainstQueryable(plan,
|
|
1219
|
+
return self.executeAgainstQueryable(plan, driverConn);
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
wrapTransaction(driverTx) {
|
|
1224
|
+
const self = this;
|
|
1225
|
+
return {
|
|
1226
|
+
async commit() {
|
|
1227
|
+
await driverTx.commit();
|
|
1228
|
+
},
|
|
1229
|
+
async rollback() {
|
|
1230
|
+
await driverTx.rollback();
|
|
1231
|
+
},
|
|
1232
|
+
execute(plan) {
|
|
1233
|
+
return self.executeAgainstQueryable(plan, driverTx);
|
|
893
1234
|
}
|
|
894
1235
|
};
|
|
895
1236
|
}
|
|
896
1237
|
telemetry() {
|
|
897
|
-
return this.
|
|
1238
|
+
return this._telemetry;
|
|
1239
|
+
}
|
|
1240
|
+
async close() {
|
|
1241
|
+
await this.driver.close();
|
|
1242
|
+
}
|
|
1243
|
+
ensureCodecRegistryValidated() {
|
|
1244
|
+
if (!this.codecRegistryValidated) {
|
|
1245
|
+
validateCodecRegistryCompleteness(this.codecRegistry, this.contract);
|
|
1246
|
+
this.codecRegistryValidated = true;
|
|
1247
|
+
}
|
|
898
1248
|
}
|
|
899
|
-
|
|
900
|
-
|
|
1249
|
+
async verifyMarker() {
|
|
1250
|
+
if (this.verify.mode === "always") this.verified = false;
|
|
1251
|
+
if (this.verified) return;
|
|
1252
|
+
const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
|
|
1253
|
+
const result = await this.driver.query(readStatement.sql, readStatement.params);
|
|
1254
|
+
if (result.rows.length === 0) {
|
|
1255
|
+
if (this.verify.requireMarker) throw runtimeError("CONTRACT.MARKER_MISSING", "Contract marker not found in database");
|
|
1256
|
+
this.verified = true;
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
const marker = this.familyAdapter.markerReader.parseMarkerRow(result.rows[0]);
|
|
1260
|
+
const contract = this.contract;
|
|
1261
|
+
if (marker.storageHash !== contract.storage.storageHash) throw runtimeError("CONTRACT.MARKER_MISMATCH", "Database storage hash does not match contract", {
|
|
1262
|
+
expected: contract.storage.storageHash,
|
|
1263
|
+
actual: marker.storageHash
|
|
1264
|
+
});
|
|
1265
|
+
const expectedProfile = contract.profileHash ?? null;
|
|
1266
|
+
if (expectedProfile !== null && marker.profileHash !== expectedProfile) throw runtimeError("CONTRACT.MARKER_MISMATCH", "Database profile hash does not match contract", {
|
|
1267
|
+
expectedProfile,
|
|
1268
|
+
actualProfile: marker.profileHash
|
|
1269
|
+
});
|
|
1270
|
+
this.verified = true;
|
|
1271
|
+
this.startupVerified = true;
|
|
1272
|
+
}
|
|
1273
|
+
recordTelemetry(plan, outcome, durationMs) {
|
|
1274
|
+
const contract = this.contract;
|
|
1275
|
+
this._telemetry = Object.freeze({
|
|
1276
|
+
lane: plan.meta.lane,
|
|
1277
|
+
target: contract.target,
|
|
1278
|
+
fingerprint: computeSqlFingerprint(plan.sql),
|
|
1279
|
+
outcome,
|
|
1280
|
+
...durationMs !== void 0 ? { durationMs } : {}
|
|
1281
|
+
});
|
|
901
1282
|
}
|
|
902
1283
|
};
|
|
903
1284
|
function transactionClosedError() {
|
|
904
|
-
return runtimeError
|
|
1285
|
+
return runtimeError("RUNTIME.TRANSACTION_CLOSED", "Cannot read from a query result after the transaction has ended. Await the result or call .toArray() inside the transaction callback.", {});
|
|
905
1286
|
}
|
|
906
1287
|
async function withTransaction(runtime, fn) {
|
|
907
1288
|
const connection = await runtime.connection();
|
|
@@ -938,7 +1319,7 @@ async function withTransaction(runtime, fn) {
|
|
|
938
1319
|
await transaction.rollback();
|
|
939
1320
|
} catch (rollbackError) {
|
|
940
1321
|
await destroyConnection(rollbackError);
|
|
941
|
-
const wrapped = runtimeError
|
|
1322
|
+
const wrapped = runtimeError("RUNTIME.TRANSACTION_ROLLBACK_FAILED", "Transaction rollback failed after callback error", { rollbackError });
|
|
942
1323
|
wrapped.cause = error;
|
|
943
1324
|
throw wrapped;
|
|
944
1325
|
}
|
|
@@ -954,7 +1335,7 @@ async function withTransaction(runtime, fn) {
|
|
|
954
1335
|
} catch {
|
|
955
1336
|
await destroyConnection(commitError);
|
|
956
1337
|
}
|
|
957
|
-
const wrapped = runtimeError
|
|
1338
|
+
const wrapped = runtimeError("RUNTIME.TRANSACTION_COMMIT_FAILED", "Transaction commit failed", { commitError });
|
|
958
1339
|
wrapped.cause = commitError;
|
|
959
1340
|
throw wrapped;
|
|
960
1341
|
}
|
|
@@ -977,5 +1358,5 @@ function createRuntime(options) {
|
|
|
977
1358
|
}
|
|
978
1359
|
|
|
979
1360
|
//#endregion
|
|
980
|
-
export { readContractMarker as a, createSqlExecutionStack as c,
|
|
981
|
-
//# sourceMappingURL=exports-
|
|
1361
|
+
export { readContractMarker as a, createSqlExecutionStack as c, parseContractMarkerRow as d, lowerSqlPlan as f, validateContractCodecMappings as h, ensureTableStatement as i, lints as l, validateCodecRegistryCompleteness as m, withTransaction as n, writeContractMarker as o, extractCodecIds as p, ensureSchemaStatement as r, createExecutionContext as s, createRuntime as t, budgets as u };
|
|
1362
|
+
//# sourceMappingURL=exports-BET5HxxT.mjs.map
|