@prisma-next/sql-runtime 0.5.0-dev.3 → 0.5.0-dev.30
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-Cq_9ZrU4.mjs} +649 -275
- package/dist/exports-Cq_9ZrU4.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 +11 -5
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +10 -12
- package/src/codecs/decoding.ts +256 -173
- package/src/codecs/encoding.ts +123 -39
- package/src/exports/index.ts +11 -7
- package/src/fingerprint.ts +22 -0
- package/src/guardrails/raw.ts +165 -0
- package/src/lower-sql-plan.ts +3 -3
- package/src/marker.ts +75 -0
- package/src/middleware/before-compile-chain.ts +1 -0
- package/src/middleware/budgets.ts +26 -96
- 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 +321 -111
- 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";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { AsyncIterableResult, RuntimeCore, checkAborted, checkMiddlewareCompatibility, isRuntimeError, raceAgainstAbort, runWithMiddleware, runtimeError } from "@prisma-next/framework-components/runtime";
|
|
2
|
+
import { type } from "arktype";
|
|
3
|
+
import { collectOrderedParamRefs, createCodecRegistry, isQueryAst } from "@prisma-next/sql-relational-core/ast";
|
|
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,31 +73,72 @@ 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) {
|
|
79
124
|
if (ast.groupBy !== void 0) return false;
|
|
80
125
|
return ast.projection.some((item) => item.expr.kind === "aggregate");
|
|
81
126
|
}
|
|
82
|
-
function
|
|
127
|
+
function primaryTableFromAst(ast) {
|
|
128
|
+
switch (ast.from.kind) {
|
|
129
|
+
case "table-source": return ast.from.name;
|
|
130
|
+
case "derived-table-source": return ast.from.alias;
|
|
131
|
+
default: return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function estimateRowsFromAst(ast, tableRows, defaultTableRows, hasAggregateWithoutGroup) {
|
|
83
135
|
if (hasAggregateWithoutGroup) return 1;
|
|
84
|
-
const table =
|
|
136
|
+
const table = primaryTableFromAst(ast);
|
|
85
137
|
if (!table) return null;
|
|
86
138
|
const tableEstimate = tableRows[table] ?? defaultTableRows;
|
|
87
139
|
if (ast.limit !== void 0) return Math.min(ast.limit, tableEstimate);
|
|
88
140
|
return tableEstimate;
|
|
89
141
|
}
|
|
90
|
-
function estimateRowsFromHeuristics(plan, tableRows, defaultTableRows) {
|
|
91
|
-
const table = plan.meta.refs?.tables?.[0];
|
|
92
|
-
if (!table) return null;
|
|
93
|
-
const tableEstimate = tableRows[table] ?? defaultTableRows;
|
|
94
|
-
const limit = plan.meta.annotations?.["limit"];
|
|
95
|
-
if (typeof limit === "number") return Math.min(limit, tableEstimate);
|
|
96
|
-
return tableEstimate;
|
|
97
|
-
}
|
|
98
|
-
function hasDetectableLimitFromHeuristics(plan) {
|
|
99
|
-
return typeof plan.meta.annotations?.["limit"] === "number";
|
|
100
|
-
}
|
|
101
142
|
function emitBudgetViolation(error, shouldBlock, ctx) {
|
|
102
143
|
if (shouldBlock) throw error;
|
|
103
144
|
ctx.log.warn({
|
|
@@ -118,11 +159,7 @@ function budgets(options) {
|
|
|
118
159
|
familyId: "sql",
|
|
119
160
|
async beforeExecute(plan, ctx) {
|
|
120
161
|
observedRowsByPlan.set(plan, { count: 0 });
|
|
121
|
-
if (isQueryAst(plan.ast))
|
|
122
|
-
if (plan.ast.kind === "select") return evaluateSelectAst(plan, plan.ast, ctx);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
return evaluateWithHeuristics(plan, ctx);
|
|
162
|
+
if (isQueryAst(plan.ast) && plan.ast.kind === "select") return evaluateSelectAst(plan.ast, ctx);
|
|
126
163
|
},
|
|
127
164
|
async onRow(_row, plan, _ctx) {
|
|
128
165
|
const state = observedRowsByPlan.get(plan);
|
|
@@ -145,9 +182,9 @@ function budgets(options) {
|
|
|
145
182
|
}
|
|
146
183
|
}
|
|
147
184
|
});
|
|
148
|
-
function evaluateSelectAst(
|
|
185
|
+
function evaluateSelectAst(ast, ctx) {
|
|
149
186
|
const hasAggNoGroup = hasAggregateWithoutGroupBy(ast);
|
|
150
|
-
const estimated = estimateRowsFromAst(ast, tableRows, defaultTableRows,
|
|
187
|
+
const estimated = estimateRowsFromAst(ast, tableRows, defaultTableRows, hasAggNoGroup);
|
|
151
188
|
const isUnbounded = ast.limit === void 0 && !hasAggNoGroup;
|
|
152
189
|
const shouldBlock = rowSeverity === "error" || ctx.mode === "strict";
|
|
153
190
|
if (isUnbounded) {
|
|
@@ -171,35 +208,83 @@ function budgets(options) {
|
|
|
171
208
|
maxRows
|
|
172
209
|
}), shouldBlock, ctx);
|
|
173
210
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
})
|
|
200
|
-
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/guardrails/raw.ts
|
|
215
|
+
const SELECT_STAR_REGEX = /select\s+\*/i;
|
|
216
|
+
const LIMIT_REGEX = /\blimit\b/i;
|
|
217
|
+
const MUTATION_PREFIX_REGEX = /^(insert|update|delete|create|alter|drop|truncate)\b/i;
|
|
218
|
+
const READ_ONLY_INTENTS = new Set([
|
|
219
|
+
"read",
|
|
220
|
+
"report",
|
|
221
|
+
"readonly"
|
|
222
|
+
]);
|
|
223
|
+
function evaluateRawGuardrails(plan, config) {
|
|
224
|
+
const lints$1 = [];
|
|
225
|
+
const budgets$1 = [];
|
|
226
|
+
const normalized = normalizeWhitespace(plan.sql);
|
|
227
|
+
const statementType = classifyStatement(normalized);
|
|
228
|
+
if (statementType === "select") {
|
|
229
|
+
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) }));
|
|
230
|
+
if (!LIMIT_REGEX.test(normalized)) {
|
|
231
|
+
const severity = config?.budgets?.unboundedSelectSeverity ?? "error";
|
|
232
|
+
lints$1.push(createLint("LINT.NO_LIMIT", "warn", "Raw SQL plan omits LIMIT clause", { sql: snippet(plan.sql) }));
|
|
233
|
+
budgets$1.push(createBudget("BUDGET.ROWS_EXCEEDED", severity, "Raw SQL plan is unbounded and may exceed row budget", {
|
|
234
|
+
sql: snippet(plan.sql),
|
|
235
|
+
...config?.budgets?.estimatedRows !== void 0 ? { estimatedRows: config.budgets.estimatedRows } : {}
|
|
236
|
+
}));
|
|
201
237
|
}
|
|
202
238
|
}
|
|
239
|
+
if (isMutationStatement(statementType) && isReadOnlyIntent(plan.meta)) lints$1.push(createLint("LINT.READ_ONLY_MUTATION", "error", "Raw SQL plan mutates data despite read-only intent", {
|
|
240
|
+
sql: snippet(plan.sql),
|
|
241
|
+
intent: plan.meta.annotations?.["intent"]
|
|
242
|
+
}));
|
|
243
|
+
return {
|
|
244
|
+
lints: lints$1,
|
|
245
|
+
budgets: budgets$1,
|
|
246
|
+
statement: statementType
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function classifyStatement(sql) {
|
|
250
|
+
const trimmed = sql.trim();
|
|
251
|
+
const lower = trimmed.toLowerCase();
|
|
252
|
+
if (lower.startsWith("with")) {
|
|
253
|
+
if (lower.includes("select")) return "select";
|
|
254
|
+
}
|
|
255
|
+
if (lower.startsWith("select")) return "select";
|
|
256
|
+
if (MUTATION_PREFIX_REGEX.test(trimmed)) return "mutation";
|
|
257
|
+
return "other";
|
|
258
|
+
}
|
|
259
|
+
function isMutationStatement(statement) {
|
|
260
|
+
return statement === "mutation";
|
|
261
|
+
}
|
|
262
|
+
function isReadOnlyIntent(meta) {
|
|
263
|
+
const annotations = meta.annotations;
|
|
264
|
+
const intent = typeof annotations?.intent === "string" ? annotations.intent.toLowerCase() : void 0;
|
|
265
|
+
return intent !== void 0 && READ_ONLY_INTENTS.has(intent);
|
|
266
|
+
}
|
|
267
|
+
function normalizeWhitespace(value) {
|
|
268
|
+
return value.replace(/\s+/g, " ").trim();
|
|
269
|
+
}
|
|
270
|
+
function snippet(sql) {
|
|
271
|
+
return normalizeWhitespace(sql).slice(0, 200);
|
|
272
|
+
}
|
|
273
|
+
function createLint(code, severity, message, details) {
|
|
274
|
+
return {
|
|
275
|
+
code,
|
|
276
|
+
severity,
|
|
277
|
+
message,
|
|
278
|
+
...details ? { details } : {}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function createBudget(code, severity, message, details) {
|
|
282
|
+
return {
|
|
283
|
+
code,
|
|
284
|
+
severity,
|
|
285
|
+
message,
|
|
286
|
+
...details ? { details } : {}
|
|
287
|
+
};
|
|
203
288
|
}
|
|
204
289
|
|
|
205
290
|
//#endregion
|
|
@@ -516,7 +601,8 @@ const ensureTableStatement = {
|
|
|
516
601
|
canonical_version int,
|
|
517
602
|
updated_at timestamptz not null default now(),
|
|
518
603
|
app_tag text,
|
|
519
|
-
meta jsonb not null default '{}'
|
|
604
|
+
meta jsonb not null default '{}',
|
|
605
|
+
invariants text[] not null default '{}'
|
|
520
606
|
)`,
|
|
521
607
|
params: []
|
|
522
608
|
};
|
|
@@ -529,56 +615,82 @@ function readContractMarker() {
|
|
|
529
615
|
canonical_version,
|
|
530
616
|
updated_at,
|
|
531
617
|
app_tag,
|
|
532
|
-
meta
|
|
618
|
+
meta,
|
|
619
|
+
invariants
|
|
533
620
|
from prisma_contract.marker
|
|
534
621
|
where id = $1`,
|
|
535
622
|
params: [1]
|
|
536
623
|
};
|
|
537
624
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Variable columns that participate in INSERT/UPDATE alongside the
|
|
627
|
+
* always-on `id = $1` and `updated_at = now()`. Each column declares
|
|
628
|
+
* its name, optional cast type, and parameter value; the placeholder
|
|
629
|
+
* (`$N`) is computed positionally below — adding or reordering a
|
|
630
|
+
* column doesn't desync indices. `invariants` only appears when the
|
|
631
|
+
* caller supplies it — see `WriteMarkerInput.invariants`.
|
|
632
|
+
*/
|
|
633
|
+
function markerColumns(input) {
|
|
634
|
+
return [
|
|
635
|
+
{
|
|
636
|
+
name: "core_hash",
|
|
637
|
+
param: input.storageHash
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
name: "profile_hash",
|
|
641
|
+
param: input.profileHash
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
name: "contract_json",
|
|
645
|
+
type: "jsonb",
|
|
646
|
+
param: input.contractJson ?? null
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: "canonical_version",
|
|
650
|
+
param: input.canonicalVersion ?? null
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "app_tag",
|
|
654
|
+
param: input.appTag ?? null
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
name: "meta",
|
|
658
|
+
type: "jsonb",
|
|
659
|
+
param: JSON.stringify(input.meta ?? {})
|
|
660
|
+
},
|
|
661
|
+
...input.invariants !== void 0 ? [{
|
|
662
|
+
name: "invariants",
|
|
663
|
+
type: "text[]",
|
|
664
|
+
param: input.invariants
|
|
665
|
+
}] : []
|
|
547
666
|
];
|
|
667
|
+
}
|
|
668
|
+
function writeContractMarker(input) {
|
|
669
|
+
const placed = markerColumns(input).map((c, i) => ({
|
|
670
|
+
name: c.name,
|
|
671
|
+
expr: c.type ? `$${i + 2}::${c.type}` : `$${i + 2}`,
|
|
672
|
+
param: c.param
|
|
673
|
+
}));
|
|
674
|
+
const params = [1, ...placed.map((c) => c.param)];
|
|
675
|
+
const insertColumns = [
|
|
676
|
+
"id",
|
|
677
|
+
...placed.map((c) => c.name),
|
|
678
|
+
"updated_at"
|
|
679
|
+
].join(", ");
|
|
680
|
+
const insertValues = [
|
|
681
|
+
"$1",
|
|
682
|
+
...placed.map((c) => c.expr),
|
|
683
|
+
"now()"
|
|
684
|
+
].join(", ");
|
|
685
|
+
const setClauses = [...placed.map((c) => `${c.name} = ${c.expr}`), "updated_at = now()"].join(", ");
|
|
548
686
|
return {
|
|
549
687
|
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
|
|
688
|
+
sql: `insert into prisma_contract.marker (${insertColumns}) values (${insertValues})`,
|
|
689
|
+
params
|
|
570
690
|
},
|
|
571
691
|
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
|
|
692
|
+
sql: `update prisma_contract.marker set ${setClauses} where id = $1`,
|
|
693
|
+
params
|
|
582
694
|
}
|
|
583
695
|
};
|
|
584
696
|
}
|
|
@@ -619,149 +731,279 @@ function formatErrorSummary(errors) {
|
|
|
619
731
|
|
|
620
732
|
//#endregion
|
|
621
733
|
//#region src/codecs/decoding.ts
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
734
|
+
const WIRE_PREVIEW_LIMIT = 100;
|
|
735
|
+
const EMPTY_INCLUDE_ALIASES = /* @__PURE__ */ new Set();
|
|
736
|
+
function isAstBackedPlan(plan) {
|
|
737
|
+
return plan.ast !== void 0;
|
|
738
|
+
}
|
|
739
|
+
function projectionListFromAst(ast) {
|
|
740
|
+
if (ast.kind === "select") return ast.projection;
|
|
741
|
+
return ast.returning;
|
|
742
|
+
}
|
|
743
|
+
function buildDecodeContext(plan, registry) {
|
|
744
|
+
if (!isAstBackedPlan(plan)) return {
|
|
745
|
+
aliases: void 0,
|
|
746
|
+
codecs: /* @__PURE__ */ new Map(),
|
|
747
|
+
columnRefs: /* @__PURE__ */ new Map(),
|
|
748
|
+
includeAliases: EMPTY_INCLUDE_ALIASES
|
|
749
|
+
};
|
|
750
|
+
const projection = projectionListFromAst(plan.ast);
|
|
751
|
+
if (!projection) return {
|
|
752
|
+
aliases: void 0,
|
|
753
|
+
codecs: /* @__PURE__ */ new Map(),
|
|
754
|
+
columnRefs: /* @__PURE__ */ new Map(),
|
|
755
|
+
includeAliases: EMPTY_INCLUDE_ALIASES
|
|
756
|
+
};
|
|
757
|
+
const aliases = [];
|
|
758
|
+
const codecs = /* @__PURE__ */ new Map();
|
|
759
|
+
const columnRefs = /* @__PURE__ */ new Map();
|
|
760
|
+
const includeAliases = /* @__PURE__ */ new Set();
|
|
761
|
+
for (const item of projection) {
|
|
762
|
+
aliases.push(item.alias);
|
|
763
|
+
if (item.codecId) {
|
|
764
|
+
const codec$1 = registry.get(item.codecId);
|
|
765
|
+
if (codec$1) codecs.set(item.alias, codec$1);
|
|
633
766
|
}
|
|
767
|
+
if (item.expr.kind === "column-ref") columnRefs.set(item.alias, {
|
|
768
|
+
table: item.expr.table,
|
|
769
|
+
column: item.expr.column
|
|
770
|
+
});
|
|
771
|
+
else if (item.expr.kind === "subquery" || item.expr.kind === "json-array-agg") includeAliases.add(item.alias);
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
aliases,
|
|
775
|
+
codecs,
|
|
776
|
+
columnRefs,
|
|
777
|
+
includeAliases
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function previewWireValue(wireValue) {
|
|
781
|
+
if (typeof wireValue === "string") return wireValue.length > WIRE_PREVIEW_LIMIT ? `${wireValue.substring(0, WIRE_PREVIEW_LIMIT)}...` : wireValue;
|
|
782
|
+
return String(wireValue).substring(0, WIRE_PREVIEW_LIMIT);
|
|
783
|
+
}
|
|
784
|
+
function isJsonSchemaValidationError(error) {
|
|
785
|
+
return isRuntimeError(error) && error.code === "RUNTIME.JSON_SCHEMA_VALIDATION_FAILED";
|
|
786
|
+
}
|
|
787
|
+
function wrapDecodeFailure(error, alias, ref, codec$1, wireValue) {
|
|
788
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
789
|
+
const wrapped = runtimeError("RUNTIME.DECODE_FAILED", `Failed to decode column ${ref ? `${ref.table}.${ref.column}` : alias} with codec '${codec$1.id}': ${message}`, {
|
|
790
|
+
...ref ? {
|
|
791
|
+
table: ref.table,
|
|
792
|
+
column: ref.column
|
|
793
|
+
} : { alias },
|
|
794
|
+
codec: codec$1.id,
|
|
795
|
+
wirePreview: previewWireValue(wireValue)
|
|
796
|
+
});
|
|
797
|
+
wrapped.cause = error;
|
|
798
|
+
throw wrapped;
|
|
799
|
+
}
|
|
800
|
+
function wrapIncludeAggregateFailure(error, alias, wireValue) {
|
|
801
|
+
const wrapped = runtimeError("RUNTIME.DECODE_FAILED", `Failed to parse JSON array for include alias '${alias}': ${error instanceof Error ? error.message : String(error)}`, {
|
|
802
|
+
alias,
|
|
803
|
+
wirePreview: previewWireValue(wireValue)
|
|
804
|
+
});
|
|
805
|
+
wrapped.cause = error;
|
|
806
|
+
throw wrapped;
|
|
807
|
+
}
|
|
808
|
+
function decodeIncludeAggregate(alias, wireValue) {
|
|
809
|
+
if (wireValue === null || wireValue === void 0) return [];
|
|
810
|
+
try {
|
|
811
|
+
let parsed;
|
|
812
|
+
if (typeof wireValue === "string") parsed = JSON.parse(wireValue);
|
|
813
|
+
else if (Array.isArray(wireValue)) parsed = wireValue;
|
|
814
|
+
else parsed = JSON.parse(String(wireValue));
|
|
815
|
+
if (!Array.isArray(parsed)) throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
|
|
816
|
+
return parsed;
|
|
817
|
+
} catch (error) {
|
|
818
|
+
wrapIncludeAggregateFailure(error, alias, wireValue);
|
|
634
819
|
}
|
|
635
|
-
return null;
|
|
636
820
|
}
|
|
637
821
|
/**
|
|
638
|
-
*
|
|
639
|
-
*
|
|
822
|
+
* Decodes a single field. Single-armed: every cell takes the same path —
|
|
823
|
+
* `codec.decode → await → JSON-Schema validate → return plain value` — so
|
|
824
|
+
* sync- and async-authored codecs are indistinguishable to callers.
|
|
825
|
+
*
|
|
826
|
+
* The row-level `rowCtx` is repackaged into a per-cell
|
|
827
|
+
* `SqlCodecCallContext` whose `column = { table, name }` is a structural
|
|
828
|
+
* projection of the per-cell `ColumnRef = { table, column }` resolved from
|
|
829
|
+
* the AST-backed `DecodeContext` (the same resolution `wrapDecodeFailure`
|
|
830
|
+
* uses for envelope construction — one resolution per cell, two consumers).
|
|
831
|
+
* Cells the runtime cannot resolve to a single underlying column (aggregate
|
|
832
|
+
* aliases, computed projections without a simple ref) get
|
|
833
|
+
* `column: undefined`, matching the spec contract that the runtime never
|
|
834
|
+
* silently defaults this field.
|
|
640
835
|
*/
|
|
641
|
-
function
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
table: value.slice(0, separatorIndex),
|
|
654
|
-
column: value.slice(separatorIndex + 1)
|
|
836
|
+
async function decodeField(alias, wireValue, decodeCtx, jsonValidators, rowCtx) {
|
|
837
|
+
if (wireValue === null) return null;
|
|
838
|
+
const codec$1 = decodeCtx.codecs.get(alias);
|
|
839
|
+
if (!codec$1) return wireValue;
|
|
840
|
+
const ref = decodeCtx.columnRefs.get(alias);
|
|
841
|
+
let cellCtx;
|
|
842
|
+
if (ref) cellCtx = {
|
|
843
|
+
...rowCtx,
|
|
844
|
+
column: {
|
|
845
|
+
table: ref.table,
|
|
846
|
+
name: ref.column
|
|
847
|
+
}
|
|
655
848
|
};
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
849
|
+
else {
|
|
850
|
+
const { column: _drop, ...rowCtxWithoutColumn } = rowCtx;
|
|
851
|
+
cellCtx = rowCtxWithoutColumn;
|
|
852
|
+
}
|
|
853
|
+
let decoded;
|
|
854
|
+
try {
|
|
855
|
+
decoded = await codec$1.decode(wireValue, cellCtx);
|
|
856
|
+
} catch (error) {
|
|
857
|
+
wrapDecodeFailure(error, alias, ref, codec$1, wireValue);
|
|
858
|
+
}
|
|
859
|
+
if (jsonValidators && ref) try {
|
|
860
|
+
validateJsonValue(jsonValidators, ref.table, ref.column, decoded, "decode", codec$1.id);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
if (isJsonSchemaValidationError(error)) throw error;
|
|
863
|
+
wrapDecodeFailure(error, alias, ref, codec$1, wireValue);
|
|
662
864
|
}
|
|
663
|
-
return
|
|
865
|
+
return decoded;
|
|
664
866
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
867
|
+
/**
|
|
868
|
+
* Decodes a row by dispatching all per-cell codec calls concurrently via
|
|
869
|
+
* `Promise.all`. Each cell follows the single-armed `decodeField` path.
|
|
870
|
+
* Failures are wrapped in `RUNTIME.DECODE_FAILED` with `{ table, column,
|
|
871
|
+
* codec }` (or `{ alias, codec }` when no column ref is resolvable) and the
|
|
872
|
+
* original error attached on `cause`.
|
|
873
|
+
*
|
|
874
|
+
* When `rowCtx.signal` is provided:
|
|
875
|
+
*
|
|
876
|
+
* - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED`
|
|
877
|
+
* (`{ phase: 'decode' }`) before any `codec.decode` call is made.
|
|
878
|
+
* - **Mid-flight aborts** race the per-cell `Promise.all` against the
|
|
879
|
+
* signal so the runtime returns promptly even when codec bodies ignore
|
|
880
|
+
* it. In-flight bodies that ignore the signal complete in the
|
|
881
|
+
* background (cooperative cancellation).
|
|
882
|
+
* - Existing `RUNTIME.DECODE_FAILED` envelopes from codec bodies pass
|
|
883
|
+
* through unchanged (no double wrap).
|
|
884
|
+
*/
|
|
885
|
+
async function decodeRow(row, plan, registry, jsonValidators, rowCtx) {
|
|
886
|
+
checkAborted(rowCtx, "decode");
|
|
887
|
+
const signal = rowCtx.signal;
|
|
888
|
+
const decodeCtx = buildDecodeContext(plan, registry);
|
|
889
|
+
const aliases = decodeCtx.aliases ?? Object.keys(row);
|
|
890
|
+
if (decodeCtx.aliases !== void 0) {
|
|
891
|
+
for (const alias of decodeCtx.aliases) if (!Object.hasOwn(row, alias)) throw runtimeError("RUNTIME.DECODE_FAILED", `Row missing projection alias "${alias}"`, {
|
|
892
|
+
alias,
|
|
893
|
+
expectedAliases: decodeCtx.aliases,
|
|
894
|
+
presentKeys: Object.keys(row)
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
const tasks = [];
|
|
898
|
+
const includeIndices = [];
|
|
899
|
+
for (let i = 0; i < aliases.length; i++) {
|
|
900
|
+
const alias = aliases[i];
|
|
674
901
|
const wireValue = row[alias];
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
decoded[alias] = [];
|
|
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 = {
|
|
902
|
+
if (decodeCtx.includeAliases.has(alias)) {
|
|
903
|
+
includeIndices.push({
|
|
904
|
+
index: i,
|
|
724
905
|
alias,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
906
|
+
value: wireValue
|
|
907
|
+
});
|
|
908
|
+
tasks.push(Promise.resolve(void 0));
|
|
909
|
+
continue;
|
|
729
910
|
}
|
|
911
|
+
tasks.push(decodeField(alias, wireValue, decodeCtx, jsonValidators, rowCtx));
|
|
730
912
|
}
|
|
913
|
+
const settled = await raceAgainstAbort(Promise.all(tasks), signal, "decode");
|
|
914
|
+
for (const entry of includeIndices) settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value);
|
|
915
|
+
const decoded = {};
|
|
916
|
+
for (let i = 0; i < aliases.length; i++) decoded[aliases[i]] = settled[i];
|
|
731
917
|
return decoded;
|
|
732
918
|
}
|
|
733
919
|
|
|
734
920
|
//#endregion
|
|
735
921
|
//#region src/codecs/encoding.ts
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
return
|
|
922
|
+
const NO_METADATA = Object.freeze({
|
|
923
|
+
codecId: void 0,
|
|
924
|
+
name: void 0
|
|
925
|
+
});
|
|
926
|
+
function paramLabel(metadata, paramIndex) {
|
|
927
|
+
return metadata.name ?? `param[${paramIndex}]`;
|
|
928
|
+
}
|
|
929
|
+
function wrapEncodeFailure(error, metadata, paramIndex, codecId) {
|
|
930
|
+
const label = paramLabel(metadata, paramIndex);
|
|
931
|
+
const wrapped = runtimeError("RUNTIME.ENCODE_FAILED", `Failed to encode parameter ${label} with codec '${codecId}': ${error instanceof Error ? error.message : String(error)}`, {
|
|
932
|
+
label,
|
|
933
|
+
codec: codecId,
|
|
934
|
+
paramIndex
|
|
935
|
+
});
|
|
936
|
+
wrapped.cause = error;
|
|
937
|
+
throw wrapped;
|
|
742
938
|
}
|
|
743
|
-
function
|
|
939
|
+
async function encodeParamValue(value, metadata, paramIndex, registry, ctx) {
|
|
744
940
|
if (value === null || value === void 0) return null;
|
|
745
|
-
|
|
941
|
+
if (!metadata.codecId) return value;
|
|
942
|
+
const codec$1 = registry.get(metadata.codecId);
|
|
746
943
|
if (!codec$1) return value;
|
|
747
|
-
|
|
748
|
-
return codec$1.encode(value);
|
|
944
|
+
try {
|
|
945
|
+
return await codec$1.encode(value, ctx);
|
|
749
946
|
} catch (error) {
|
|
750
|
-
|
|
751
|
-
throw new Error(`Failed to encode parameter ${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
947
|
+
wrapEncodeFailure(error, metadata, paramIndex, codec$1.id);
|
|
752
948
|
}
|
|
753
|
-
return value;
|
|
754
949
|
}
|
|
755
|
-
|
|
950
|
+
/**
|
|
951
|
+
* Encodes all parameters concurrently via `Promise.all`. Per parameter, sync-
|
|
952
|
+
* and async-authored codecs share the same path: `codec.encode → await →
|
|
953
|
+
* return`. Param-level failures are wrapped in `RUNTIME.ENCODE_FAILED`.
|
|
954
|
+
*
|
|
955
|
+
* When `ctx.signal` is provided:
|
|
956
|
+
*
|
|
957
|
+
* - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED`
|
|
958
|
+
* (`{ phase: 'encode' }`) before any `codec.encode` call is made — codecs
|
|
959
|
+
* can pin this with a per-call counter that stays at zero.
|
|
960
|
+
* - **Mid-flight abort** races the per-param `Promise.all` against
|
|
961
|
+
* `abortable(ctx.signal)`. The runtime returns `RUNTIME.ABORTED` promptly
|
|
962
|
+
* even if codec bodies ignore the signal; the in-flight bodies are
|
|
963
|
+
* abandoned and run to completion in the background (cooperative
|
|
964
|
+
* cancellation, see ADR 204).
|
|
965
|
+
* - Existing `RUNTIME.ENCODE_FAILED` envelopes that surface from a codec
|
|
966
|
+
* body before the runtime observes the abort pass through unchanged
|
|
967
|
+
* (no double wrap).
|
|
968
|
+
*/
|
|
969
|
+
async function encodeParams(plan, registry, ctx) {
|
|
970
|
+
checkAborted(ctx, "encode");
|
|
971
|
+
const signal = ctx.signal;
|
|
756
972
|
if (plan.params.length === 0) return plan.params;
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
973
|
+
const paramCount = plan.params.length;
|
|
974
|
+
const metadata = new Array(paramCount).fill(NO_METADATA);
|
|
975
|
+
if (plan.ast) {
|
|
976
|
+
const refs = collectOrderedParamRefs(plan.ast);
|
|
977
|
+
for (let i = 0; i < paramCount && i < refs.length; i++) {
|
|
978
|
+
const ref = refs[i];
|
|
979
|
+
if (ref) metadata[i] = {
|
|
980
|
+
codecId: ref.codecId,
|
|
981
|
+
name: ref.name
|
|
982
|
+
};
|
|
983
|
+
}
|
|
763
984
|
}
|
|
764
|
-
|
|
985
|
+
const tasks = new Array(paramCount);
|
|
986
|
+
for (let i = 0; i < paramCount; i++) tasks[i] = encodeParamValue(plan.params[i], metadata[i] ?? NO_METADATA, i, registry, ctx);
|
|
987
|
+
const settled = await raceAgainstAbort(Promise.all(tasks), signal, "encode");
|
|
988
|
+
return Object.freeze(settled);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region src/fingerprint.ts
|
|
993
|
+
const STRING_LITERAL_REGEX = /'(?:''|[^'])*'/g;
|
|
994
|
+
const NUMERIC_LITERAL_REGEX = /\b\d+(?:\.\d+)?\b/g;
|
|
995
|
+
const WHITESPACE_REGEX = /\s+/g;
|
|
996
|
+
/**
|
|
997
|
+
* Computes a literal-stripped, normalized fingerprint of a SQL statement.
|
|
998
|
+
*
|
|
999
|
+
* The function strips string and numeric literals, collapses whitespace, and
|
|
1000
|
+
* lowercases the result before hashing — so two structurally equivalent
|
|
1001
|
+
* statements (with different parameter values) produce the same fingerprint.
|
|
1002
|
+
* Used by SQL telemetry to group queries.
|
|
1003
|
+
*/
|
|
1004
|
+
function computeSqlFingerprint(sql) {
|
|
1005
|
+
const normalized = sql.replace(STRING_LITERAL_REGEX, "?").replace(NUMERIC_LITERAL_REGEX, "?").replace(WHITESPACE_REGEX, " ").trim().toLowerCase();
|
|
1006
|
+
return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
|
|
765
1007
|
}
|
|
766
1008
|
|
|
767
1009
|
//#endregion
|
|
@@ -806,102 +1048,234 @@ var SqlFamilyAdapter = class {
|
|
|
806
1048
|
|
|
807
1049
|
//#endregion
|
|
808
1050
|
//#region src/sql-runtime.ts
|
|
809
|
-
|
|
810
|
-
|
|
1051
|
+
function isExecutionPlan(plan) {
|
|
1052
|
+
return "sql" in plan;
|
|
1053
|
+
}
|
|
1054
|
+
var SqlRuntimeImpl = class extends RuntimeCore {
|
|
811
1055
|
contract;
|
|
812
1056
|
adapter;
|
|
1057
|
+
driver;
|
|
1058
|
+
familyAdapter;
|
|
813
1059
|
codecRegistry;
|
|
814
1060
|
jsonSchemaValidators;
|
|
1061
|
+
sqlCtx;
|
|
1062
|
+
verify;
|
|
815
1063
|
codecRegistryValidated;
|
|
1064
|
+
verified;
|
|
1065
|
+
startupVerified;
|
|
1066
|
+
_telemetry;
|
|
816
1067
|
constructor(options) {
|
|
817
1068
|
const { context, adapter, driver, verify, middleware, mode, log } = options;
|
|
1069
|
+
if (middleware) for (const mw of middleware) checkMiddlewareCompatibility(mw, "sql", context.contract.target);
|
|
1070
|
+
const sqlCtx = {
|
|
1071
|
+
contract: context.contract,
|
|
1072
|
+
mode: mode ?? "strict",
|
|
1073
|
+
now: () => Date.now(),
|
|
1074
|
+
log: log ?? {
|
|
1075
|
+
info: () => {},
|
|
1076
|
+
warn: () => {},
|
|
1077
|
+
error: () => {}
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
super({
|
|
1081
|
+
middleware: middleware ?? [],
|
|
1082
|
+
ctx: sqlCtx
|
|
1083
|
+
});
|
|
818
1084
|
this.contract = context.contract;
|
|
819
1085
|
this.adapter = adapter;
|
|
1086
|
+
this.driver = driver;
|
|
1087
|
+
this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
820
1088
|
this.codecRegistry = context.codecs;
|
|
821
1089
|
this.jsonSchemaValidators = context.jsonSchemaValidators;
|
|
1090
|
+
this.sqlCtx = sqlCtx;
|
|
1091
|
+
this.verify = verify;
|
|
822
1092
|
this.codecRegistryValidated = false;
|
|
823
|
-
|
|
824
|
-
this.
|
|
825
|
-
|
|
826
|
-
driver,
|
|
827
|
-
verify,
|
|
828
|
-
...ifDefined("middleware", middleware),
|
|
829
|
-
...ifDefined("mode", mode),
|
|
830
|
-
...ifDefined("log", log)
|
|
831
|
-
});
|
|
1093
|
+
this.verified = verify.mode === "startup" ? false : verify.mode === "always";
|
|
1094
|
+
this.startupVerified = false;
|
|
1095
|
+
this._telemetry = null;
|
|
832
1096
|
if (verify.mode === "startup") {
|
|
833
1097
|
validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
|
|
834
1098
|
this.codecRegistryValidated = true;
|
|
835
1099
|
}
|
|
836
1100
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1101
|
+
/**
|
|
1102
|
+
* Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with
|
|
1103
|
+
* encoded parameters ready for the driver. This is the single point at
|
|
1104
|
+
* which params transition from app-layer values to driver wire-format.
|
|
1105
|
+
*
|
|
1106
|
+
* `ctx: SqlCodecCallContext` is forwarded to `encodeParams` so per-query
|
|
1107
|
+
* cancellation reaches every codec body during parameter encoding. The
|
|
1108
|
+
* framework abstract typed this as `CodecCallContext`; the SQL family
|
|
1109
|
+
* narrows it to the SQL-specific extension. SQL params do not populate
|
|
1110
|
+
* `ctx.column` — encode-side column metadata is the middleware's domain.
|
|
1111
|
+
*/
|
|
1112
|
+
async lower(plan, ctx) {
|
|
1113
|
+
const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
|
|
1114
|
+
return Object.freeze({
|
|
1115
|
+
...lowered,
|
|
1116
|
+
params: await encodeParams(lowered, this.codecRegistry, ctx)
|
|
1117
|
+
});
|
|
842
1118
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1119
|
+
/**
|
|
1120
|
+
* Default driver invocation. Production execution paths override the
|
|
1121
|
+
* queryable target (e.g. transaction or connection) by going through
|
|
1122
|
+
* `executeAgainstQueryable`; this implementation supports any caller of
|
|
1123
|
+
* `super.execute(plan)` and the abstract-base contract.
|
|
1124
|
+
*/
|
|
1125
|
+
runDriver(exec) {
|
|
1126
|
+
return this.driver.execute({
|
|
1127
|
+
sql: exec.sql,
|
|
1128
|
+
params: exec.params
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* SQL pre-compile hook. Runs the registered middleware `beforeCompile`
|
|
1133
|
+
* chain over the plan's draft (AST + meta). Returns the original plan
|
|
1134
|
+
* unchanged when no middleware rewrote the AST; otherwise returns a new
|
|
1135
|
+
* plan carrying the rewritten AST and meta. The AST is the authoritative
|
|
1136
|
+
* source of execution metadata, so a rewrite needs no sidecar
|
|
1137
|
+
* reconciliation here — the lowering adapter and the encoder both walk
|
|
1138
|
+
* the rewritten AST directly.
|
|
1139
|
+
*/
|
|
1140
|
+
async runBeforeCompile(plan) {
|
|
1141
|
+
const rewrittenDraft = await runBeforeCompileChain(this.middleware, {
|
|
849
1142
|
ast: plan.ast,
|
|
850
1143
|
meta: plan.meta
|
|
851
|
-
}, this.
|
|
852
|
-
|
|
1144
|
+
}, this.sqlCtx);
|
|
1145
|
+
return rewrittenDraft.ast === plan.ast ? plan : {
|
|
853
1146
|
...plan,
|
|
854
|
-
ast: rewrittenDraft.ast
|
|
855
|
-
|
|
856
|
-
return lowerSqlPlan(this.adapter, this.contract, planToLower);
|
|
857
|
-
}
|
|
858
|
-
executeAgainstQueryable(plan, queryable) {
|
|
859
|
-
this.ensureCodecRegistryValidated(this.contract);
|
|
860
|
-
const iterator = async function* (self) {
|
|
861
|
-
const executablePlan = await self.toExecutionPlan(plan);
|
|
862
|
-
const encodedParams = encodeParams(executablePlan, self.codecRegistry);
|
|
863
|
-
const planWithEncodedParams = {
|
|
864
|
-
...executablePlan,
|
|
865
|
-
params: encodedParams
|
|
866
|
-
};
|
|
867
|
-
const coreIterator = queryable.execute(planWithEncodedParams);
|
|
868
|
-
for await (const rawRow of coreIterator) yield decodeRow(rawRow, executablePlan, self.codecRegistry, self.jsonSchemaValidators);
|
|
1147
|
+
ast: rewrittenDraft.ast,
|
|
1148
|
+
meta: rewrittenDraft.meta
|
|
869
1149
|
};
|
|
870
|
-
return new AsyncIterableResult(iterator(this));
|
|
871
1150
|
}
|
|
872
|
-
execute(plan) {
|
|
873
|
-
return this.executeAgainstQueryable(plan, this.
|
|
1151
|
+
execute(plan, options) {
|
|
1152
|
+
return this.executeAgainstQueryable(plan, this.driver, options);
|
|
1153
|
+
}
|
|
1154
|
+
executeAgainstQueryable(plan, queryable, options) {
|
|
1155
|
+
this.ensureCodecRegistryValidated();
|
|
1156
|
+
const self = this;
|
|
1157
|
+
const signal = options?.signal;
|
|
1158
|
+
const codecCtx = signal === void 0 ? {} : { signal };
|
|
1159
|
+
const generator = async function* () {
|
|
1160
|
+
checkAborted(codecCtx, "stream");
|
|
1161
|
+
const exec = isExecutionPlan(plan) ? Object.freeze({
|
|
1162
|
+
...plan,
|
|
1163
|
+
params: await encodeParams(plan, self.codecRegistry, codecCtx)
|
|
1164
|
+
}) : await self.lower(await self.runBeforeCompile(plan), codecCtx);
|
|
1165
|
+
self.familyAdapter.validatePlan(exec, self.contract);
|
|
1166
|
+
self._telemetry = null;
|
|
1167
|
+
if (!self.startupVerified && self.verify.mode === "startup") await self.verifyMarker();
|
|
1168
|
+
if (!self.verified && self.verify.mode === "onFirstUse") await self.verifyMarker();
|
|
1169
|
+
const startedAt = Date.now();
|
|
1170
|
+
let outcome = null;
|
|
1171
|
+
try {
|
|
1172
|
+
if (self.verify.mode === "always") await self.verifyMarker();
|
|
1173
|
+
const iterator = runWithMiddleware(exec, self.middleware, self.ctx, () => queryable.execute({
|
|
1174
|
+
sql: exec.sql,
|
|
1175
|
+
params: exec.params
|
|
1176
|
+
}))[Symbol.asyncIterator]();
|
|
1177
|
+
try {
|
|
1178
|
+
while (true) {
|
|
1179
|
+
checkAborted(codecCtx, "stream");
|
|
1180
|
+
const next = await iterator.next();
|
|
1181
|
+
if (next.done) break;
|
|
1182
|
+
yield await decodeRow(next.value, exec, self.codecRegistry, self.jsonSchemaValidators, codecCtx);
|
|
1183
|
+
}
|
|
1184
|
+
} finally {
|
|
1185
|
+
await iterator.return?.();
|
|
1186
|
+
}
|
|
1187
|
+
outcome = "success";
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
outcome = "runtime-error";
|
|
1190
|
+
throw error;
|
|
1191
|
+
} finally {
|
|
1192
|
+
if (outcome !== null) self.recordTelemetry(exec, outcome, Date.now() - startedAt);
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
return new AsyncIterableResult(generator());
|
|
874
1196
|
}
|
|
875
1197
|
async connection() {
|
|
876
|
-
const
|
|
1198
|
+
const driverConn = await this.driver.acquireConnection();
|
|
877
1199
|
const self = this;
|
|
878
1200
|
return {
|
|
879
1201
|
async transaction() {
|
|
880
|
-
const
|
|
881
|
-
return
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1202
|
+
const driverTx = await driverConn.beginTransaction();
|
|
1203
|
+
return self.wrapTransaction(driverTx);
|
|
1204
|
+
},
|
|
1205
|
+
async release() {
|
|
1206
|
+
await driverConn.release();
|
|
1207
|
+
},
|
|
1208
|
+
async destroy(reason) {
|
|
1209
|
+
await driverConn.destroy(reason);
|
|
1210
|
+
},
|
|
1211
|
+
execute(plan, options) {
|
|
1212
|
+
return self.executeAgainstQueryable(plan, driverConn, options);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
wrapTransaction(driverTx) {
|
|
1217
|
+
const self = this;
|
|
1218
|
+
return {
|
|
1219
|
+
async commit() {
|
|
1220
|
+
await driverTx.commit();
|
|
1221
|
+
},
|
|
1222
|
+
async rollback() {
|
|
1223
|
+
await driverTx.rollback();
|
|
888
1224
|
},
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
execute(plan) {
|
|
892
|
-
return self.executeAgainstQueryable(plan, coreConn);
|
|
1225
|
+
execute(plan, options) {
|
|
1226
|
+
return self.executeAgainstQueryable(plan, driverTx, options);
|
|
893
1227
|
}
|
|
894
1228
|
};
|
|
895
1229
|
}
|
|
896
1230
|
telemetry() {
|
|
897
|
-
return this.
|
|
1231
|
+
return this._telemetry;
|
|
898
1232
|
}
|
|
899
|
-
close() {
|
|
900
|
-
|
|
1233
|
+
async close() {
|
|
1234
|
+
await this.driver.close();
|
|
1235
|
+
}
|
|
1236
|
+
ensureCodecRegistryValidated() {
|
|
1237
|
+
if (!this.codecRegistryValidated) {
|
|
1238
|
+
validateCodecRegistryCompleteness(this.codecRegistry, this.contract);
|
|
1239
|
+
this.codecRegistryValidated = true;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
async verifyMarker() {
|
|
1243
|
+
if (this.verify.mode === "always") this.verified = false;
|
|
1244
|
+
if (this.verified) return;
|
|
1245
|
+
const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
|
|
1246
|
+
const result = await this.driver.query(readStatement.sql, readStatement.params);
|
|
1247
|
+
if (result.rows.length === 0) {
|
|
1248
|
+
if (this.verify.requireMarker) throw runtimeError("CONTRACT.MARKER_MISSING", "Contract marker not found in database");
|
|
1249
|
+
this.verified = true;
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const marker = this.familyAdapter.markerReader.parseMarkerRow(result.rows[0]);
|
|
1253
|
+
const contract = this.contract;
|
|
1254
|
+
if (marker.storageHash !== contract.storage.storageHash) throw runtimeError("CONTRACT.MARKER_MISMATCH", "Database storage hash does not match contract", {
|
|
1255
|
+
expected: contract.storage.storageHash,
|
|
1256
|
+
actual: marker.storageHash
|
|
1257
|
+
});
|
|
1258
|
+
const expectedProfile = contract.profileHash ?? null;
|
|
1259
|
+
if (expectedProfile !== null && marker.profileHash !== expectedProfile) throw runtimeError("CONTRACT.MARKER_MISMATCH", "Database profile hash does not match contract", {
|
|
1260
|
+
expectedProfile,
|
|
1261
|
+
actualProfile: marker.profileHash
|
|
1262
|
+
});
|
|
1263
|
+
this.verified = true;
|
|
1264
|
+
this.startupVerified = true;
|
|
1265
|
+
}
|
|
1266
|
+
recordTelemetry(plan, outcome, durationMs) {
|
|
1267
|
+
const contract = this.contract;
|
|
1268
|
+
this._telemetry = Object.freeze({
|
|
1269
|
+
lane: plan.meta.lane,
|
|
1270
|
+
target: contract.target,
|
|
1271
|
+
fingerprint: computeSqlFingerprint(plan.sql),
|
|
1272
|
+
outcome,
|
|
1273
|
+
...durationMs !== void 0 ? { durationMs } : {}
|
|
1274
|
+
});
|
|
901
1275
|
}
|
|
902
1276
|
};
|
|
903
1277
|
function transactionClosedError() {
|
|
904
|
-
return runtimeError
|
|
1278
|
+
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
1279
|
}
|
|
906
1280
|
async function withTransaction(runtime, fn) {
|
|
907
1281
|
const connection = await runtime.connection();
|
|
@@ -911,9 +1285,9 @@ async function withTransaction(runtime, fn) {
|
|
|
911
1285
|
get invalidated() {
|
|
912
1286
|
return invalidated;
|
|
913
1287
|
},
|
|
914
|
-
execute(plan) {
|
|
1288
|
+
execute(plan, options) {
|
|
915
1289
|
if (invalidated) throw transactionClosedError();
|
|
916
|
-
const inner = transaction.execute(plan);
|
|
1290
|
+
const inner = transaction.execute(plan, options);
|
|
917
1291
|
const guarded = async function* () {
|
|
918
1292
|
for await (const row of inner) {
|
|
919
1293
|
if (invalidated) throw transactionClosedError();
|
|
@@ -938,7 +1312,7 @@ async function withTransaction(runtime, fn) {
|
|
|
938
1312
|
await transaction.rollback();
|
|
939
1313
|
} catch (rollbackError) {
|
|
940
1314
|
await destroyConnection(rollbackError);
|
|
941
|
-
const wrapped = runtimeError
|
|
1315
|
+
const wrapped = runtimeError("RUNTIME.TRANSACTION_ROLLBACK_FAILED", "Transaction rollback failed after callback error", { rollbackError });
|
|
942
1316
|
wrapped.cause = error;
|
|
943
1317
|
throw wrapped;
|
|
944
1318
|
}
|
|
@@ -954,7 +1328,7 @@ async function withTransaction(runtime, fn) {
|
|
|
954
1328
|
} catch {
|
|
955
1329
|
await destroyConnection(commitError);
|
|
956
1330
|
}
|
|
957
|
-
const wrapped = runtimeError
|
|
1331
|
+
const wrapped = runtimeError("RUNTIME.TRANSACTION_COMMIT_FAILED", "Transaction commit failed", { commitError });
|
|
958
1332
|
wrapped.cause = commitError;
|
|
959
1333
|
throw wrapped;
|
|
960
1334
|
}
|
|
@@ -977,5 +1351,5 @@ function createRuntime(options) {
|
|
|
977
1351
|
}
|
|
978
1352
|
|
|
979
1353
|
//#endregion
|
|
980
|
-
export { readContractMarker as a, createSqlExecutionStack as c,
|
|
981
|
-
//# sourceMappingURL=exports-
|
|
1354
|
+
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 };
|
|
1355
|
+
//# sourceMappingURL=exports-Cq_9ZrU4.mjs.map
|