@prisma-next/sql-runtime 0.5.0-dev.9 → 0.6.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/exports-BSTHn_rH.mjs +1516 -0
- package/dist/exports-BSTHn_rH.mjs.map +1 -0
- package/dist/{index-CZmC2kD3.d.mts → index-CTCvZOWI.d.mts} +87 -44
- package/dist/index-CTCvZOWI.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -3
- package/dist/test/utils.d.mts +33 -29
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +104 -64
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +15 -14
- package/src/codecs/alias-resolver.ts +37 -0
- package/src/codecs/decoding.ts +163 -129
- package/src/codecs/encoding.ts +121 -46
- package/src/codecs/validation.ts +4 -4
- package/src/content-hash.ts +44 -0
- package/src/exports/index.ts +4 -1
- package/src/guardrails/raw.ts +1 -50
- package/src/marker.ts +13 -20
- package/src/middleware/before-compile-chain.ts +1 -31
- package/src/middleware/budgets.ts +26 -113
- package/src/middleware/lints.ts +17 -23
- package/src/middleware/sql-middleware.ts +21 -2
- package/src/runtime-spi.ts +3 -8
- package/src/sql-context.ts +320 -109
- package/src/sql-marker.ts +88 -50
- package/src/sql-runtime.ts +108 -90
- package/dist/exports-BOHa3Emo.mjs +0 -1334
- package/dist/exports-BOHa3Emo.mjs.map +0 -1
- package/dist/index-CZmC2kD3.d.mts.map +0 -1
- package/src/codecs/json-schema-validation.ts +0 -61
|
@@ -1,1334 +0,0 @@
|
|
|
1
|
-
import { AsyncIterableResult, RuntimeCore, checkMiddlewareCompatibility, isRuntimeError, runWithMiddleware, runtimeError } from "@prisma-next/framework-components/runtime";
|
|
2
|
-
import { createCodecRegistry, isQueryAst } from "@prisma-next/sql-relational-core/ast";
|
|
3
|
-
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
|
-
import { checkContractComponentRequirements } from "@prisma-next/framework-components/components";
|
|
5
|
-
import { createExecutionStack } from "@prisma-next/framework-components/execution";
|
|
6
|
-
import { createSqlOperationRegistry } from "@prisma-next/sql-operations";
|
|
7
|
-
import { type } from "arktype";
|
|
8
|
-
import { createHash } from "node:crypto";
|
|
9
|
-
|
|
10
|
-
//#region src/codecs/validation.ts
|
|
11
|
-
function extractCodecIds(contract) {
|
|
12
|
-
const codecIds = /* @__PURE__ */ new Set();
|
|
13
|
-
for (const table of Object.values(contract.storage.tables)) for (const column of Object.values(table.columns)) {
|
|
14
|
-
const codecId = column.codecId;
|
|
15
|
-
codecIds.add(codecId);
|
|
16
|
-
}
|
|
17
|
-
return codecIds;
|
|
18
|
-
}
|
|
19
|
-
function extractCodecIdsFromColumns(contract) {
|
|
20
|
-
const codecIds = /* @__PURE__ */ new Map();
|
|
21
|
-
for (const [tableName, table] of Object.entries(contract.storage.tables)) for (const [columnName, column] of Object.entries(table.columns)) {
|
|
22
|
-
const codecId = column.codecId;
|
|
23
|
-
const key = `${tableName}.${columnName}`;
|
|
24
|
-
codecIds.set(key, codecId);
|
|
25
|
-
}
|
|
26
|
-
return codecIds;
|
|
27
|
-
}
|
|
28
|
-
function validateContractCodecMappings(registry, contract) {
|
|
29
|
-
const codecIds = extractCodecIdsFromColumns(contract);
|
|
30
|
-
const invalidCodecs = [];
|
|
31
|
-
for (const [key, codecId] of codecIds.entries()) if (!registry.has(codecId)) {
|
|
32
|
-
const parts = key.split(".");
|
|
33
|
-
const table = parts[0] ?? "";
|
|
34
|
-
const column = parts[1] ?? "";
|
|
35
|
-
invalidCodecs.push({
|
|
36
|
-
table,
|
|
37
|
-
column,
|
|
38
|
-
codecId
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
if (invalidCodecs.length > 0) {
|
|
42
|
-
const details = {
|
|
43
|
-
contractTarget: contract.target,
|
|
44
|
-
invalidCodecs
|
|
45
|
-
};
|
|
46
|
-
throw runtimeError("RUNTIME.CODEC_MISSING", `Missing codec implementations for column codecIds: ${invalidCodecs.map((c) => `${c.table}.${c.column} (${c.codecId})`).join(", ")}`, details);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function validateCodecRegistryCompleteness(registry, contract) {
|
|
50
|
-
validateContractCodecMappings(registry, contract);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
//#endregion
|
|
54
|
-
//#region src/lower-sql-plan.ts
|
|
55
|
-
/**
|
|
56
|
-
* Lowers a SQL query plan to an executable Plan by calling the adapter's lower method.
|
|
57
|
-
*
|
|
58
|
-
* @param adapter - Adapter to lower AST to SQL
|
|
59
|
-
* @param contract - Contract for lowering context
|
|
60
|
-
* @param queryPlan - SQL query plan from a lane (contains AST, params, meta, but no SQL)
|
|
61
|
-
* @returns Fully executable Plan with SQL string
|
|
62
|
-
*/
|
|
63
|
-
function lowerSqlPlan(adapter, contract, queryPlan) {
|
|
64
|
-
const lowered = adapter.lower(queryPlan.ast, {
|
|
65
|
-
contract,
|
|
66
|
-
params: queryPlan.params
|
|
67
|
-
});
|
|
68
|
-
return Object.freeze({
|
|
69
|
-
sql: lowered.sql,
|
|
70
|
-
params: lowered.params ?? queryPlan.params,
|
|
71
|
-
ast: queryPlan.ast,
|
|
72
|
-
meta: queryPlan.meta
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
//#endregion
|
|
77
|
-
//#region src/middleware/budgets.ts
|
|
78
|
-
function hasAggregateWithoutGroupBy(ast) {
|
|
79
|
-
if (ast.groupBy !== void 0) return false;
|
|
80
|
-
return ast.projection.some((item) => item.expr.kind === "aggregate");
|
|
81
|
-
}
|
|
82
|
-
function estimateRowsFromAst(ast, tableRows, defaultTableRows, refs, hasAggregateWithoutGroup) {
|
|
83
|
-
if (hasAggregateWithoutGroup) return 1;
|
|
84
|
-
const table = refs?.tables?.[0];
|
|
85
|
-
if (!table) return null;
|
|
86
|
-
const tableEstimate = tableRows[table] ?? defaultTableRows;
|
|
87
|
-
if (ast.limit !== void 0) return Math.min(ast.limit, tableEstimate);
|
|
88
|
-
return tableEstimate;
|
|
89
|
-
}
|
|
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
|
-
function emitBudgetViolation(error, shouldBlock, ctx) {
|
|
102
|
-
if (shouldBlock) throw error;
|
|
103
|
-
ctx.log.warn({
|
|
104
|
-
code: error.code,
|
|
105
|
-
message: error.message,
|
|
106
|
-
details: error.details
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
function budgets(options) {
|
|
110
|
-
const maxRows = options?.maxRows ?? 1e4;
|
|
111
|
-
const defaultTableRows = options?.defaultTableRows ?? 1e4;
|
|
112
|
-
const tableRows = options?.tableRows ?? {};
|
|
113
|
-
const maxLatencyMs = options?.maxLatencyMs ?? 1e3;
|
|
114
|
-
const rowSeverity = options?.severities?.rowCount ?? "error";
|
|
115
|
-
const observedRowsByPlan = /* @__PURE__ */ new WeakMap();
|
|
116
|
-
return Object.freeze({
|
|
117
|
-
name: "budgets",
|
|
118
|
-
familyId: "sql",
|
|
119
|
-
async beforeExecute(plan, ctx) {
|
|
120
|
-
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);
|
|
126
|
-
},
|
|
127
|
-
async onRow(_row, plan, _ctx) {
|
|
128
|
-
const state = observedRowsByPlan.get(plan);
|
|
129
|
-
if (!state) return;
|
|
130
|
-
state.count += 1;
|
|
131
|
-
if (state.count > maxRows) throw runtimeError("BUDGET.ROWS_EXCEEDED", "Observed row count exceeds budget", {
|
|
132
|
-
source: "observed",
|
|
133
|
-
observedRows: state.count,
|
|
134
|
-
maxRows
|
|
135
|
-
});
|
|
136
|
-
},
|
|
137
|
-
async afterExecute(_plan, result, ctx) {
|
|
138
|
-
const latencyMs = result.latencyMs;
|
|
139
|
-
if (latencyMs > maxLatencyMs) {
|
|
140
|
-
const shouldBlock = ctx.mode === "strict";
|
|
141
|
-
emitBudgetViolation(runtimeError("BUDGET.TIME_EXCEEDED", "Query latency exceeds budget", {
|
|
142
|
-
latencyMs,
|
|
143
|
-
maxLatencyMs
|
|
144
|
-
}), shouldBlock, ctx);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
function evaluateSelectAst(plan, ast, ctx) {
|
|
149
|
-
const hasAggNoGroup = hasAggregateWithoutGroupBy(ast);
|
|
150
|
-
const estimated = estimateRowsFromAst(ast, tableRows, defaultTableRows, plan.meta.refs, hasAggNoGroup);
|
|
151
|
-
const isUnbounded = ast.limit === void 0 && !hasAggNoGroup;
|
|
152
|
-
const shouldBlock = rowSeverity === "error" || ctx.mode === "strict";
|
|
153
|
-
if (isUnbounded) {
|
|
154
|
-
if (estimated !== null && estimated >= maxRows) {
|
|
155
|
-
emitBudgetViolation(runtimeError("BUDGET.ROWS_EXCEEDED", "Unbounded SELECT query exceeds budget", {
|
|
156
|
-
source: "ast",
|
|
157
|
-
estimatedRows: estimated,
|
|
158
|
-
maxRows
|
|
159
|
-
}), shouldBlock, ctx);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
emitBudgetViolation(runtimeError("BUDGET.ROWS_EXCEEDED", "Unbounded SELECT query exceeds budget", {
|
|
163
|
-
source: "ast",
|
|
164
|
-
maxRows
|
|
165
|
-
}), shouldBlock, ctx);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (estimated !== null && estimated > maxRows) emitBudgetViolation(runtimeError("BUDGET.ROWS_EXCEEDED", "Estimated row count exceeds budget", {
|
|
169
|
-
source: "ast",
|
|
170
|
-
estimatedRows: estimated,
|
|
171
|
-
maxRows
|
|
172
|
-
}), shouldBlock, ctx);
|
|
173
|
-
}
|
|
174
|
-
async function evaluateWithHeuristics(plan, ctx) {
|
|
175
|
-
const estimated = estimateRowsFromHeuristics(plan, tableRows, defaultTableRows);
|
|
176
|
-
const isUnbounded = !hasDetectableLimitFromHeuristics(plan);
|
|
177
|
-
const isSelect = plan.sql.trimStart().toUpperCase().startsWith("SELECT");
|
|
178
|
-
const shouldBlock = rowSeverity === "error" || ctx.mode === "strict";
|
|
179
|
-
if (isSelect && isUnbounded) {
|
|
180
|
-
if (estimated !== null && estimated >= maxRows) {
|
|
181
|
-
emitBudgetViolation(runtimeError("BUDGET.ROWS_EXCEEDED", "Unbounded SELECT query exceeds budget", {
|
|
182
|
-
source: "heuristic",
|
|
183
|
-
estimatedRows: estimated,
|
|
184
|
-
maxRows
|
|
185
|
-
}), shouldBlock, ctx);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
emitBudgetViolation(runtimeError("BUDGET.ROWS_EXCEEDED", "Unbounded SELECT query exceeds budget", {
|
|
189
|
-
source: "heuristic",
|
|
190
|
-
maxRows
|
|
191
|
-
}), shouldBlock, ctx);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (estimated !== null) {
|
|
195
|
-
if (estimated > maxRows) emitBudgetViolation(runtimeError("BUDGET.ROWS_EXCEEDED", "Estimated row count exceeds budget", {
|
|
196
|
-
source: "heuristic",
|
|
197
|
-
estimatedRows: estimated,
|
|
198
|
-
maxRows
|
|
199
|
-
}), shouldBlock, ctx);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
//#endregion
|
|
206
|
-
//#region src/guardrails/raw.ts
|
|
207
|
-
const SELECT_STAR_REGEX = /select\s+\*/i;
|
|
208
|
-
const LIMIT_REGEX = /\blimit\b/i;
|
|
209
|
-
const MUTATION_PREFIX_REGEX = /^(insert|update|delete|create|alter|drop|truncate)\b/i;
|
|
210
|
-
const READ_ONLY_INTENTS = new Set([
|
|
211
|
-
"read",
|
|
212
|
-
"report",
|
|
213
|
-
"readonly"
|
|
214
|
-
]);
|
|
215
|
-
function evaluateRawGuardrails(plan, config) {
|
|
216
|
-
const lints$1 = [];
|
|
217
|
-
const budgets$1 = [];
|
|
218
|
-
const normalized = normalizeWhitespace(plan.sql);
|
|
219
|
-
const statementType = classifyStatement(normalized);
|
|
220
|
-
if (statementType === "select") {
|
|
221
|
-
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) }));
|
|
222
|
-
if (!LIMIT_REGEX.test(normalized)) {
|
|
223
|
-
const severity = config?.budgets?.unboundedSelectSeverity ?? "error";
|
|
224
|
-
lints$1.push(createLint("LINT.NO_LIMIT", "warn", "Raw SQL plan omits LIMIT clause", { sql: snippet(plan.sql) }));
|
|
225
|
-
budgets$1.push(createBudget("BUDGET.ROWS_EXCEEDED", severity, "Raw SQL plan is unbounded and may exceed row budget", {
|
|
226
|
-
sql: snippet(plan.sql),
|
|
227
|
-
...config?.budgets?.estimatedRows !== void 0 ? { estimatedRows: config.budgets.estimatedRows } : {}
|
|
228
|
-
}));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (isMutationStatement(statementType) && isReadOnlyIntent(plan.meta)) lints$1.push(createLint("LINT.READ_ONLY_MUTATION", "error", "Raw SQL plan mutates data despite read-only intent", {
|
|
232
|
-
sql: snippet(plan.sql),
|
|
233
|
-
intent: plan.meta.annotations?.["intent"]
|
|
234
|
-
}));
|
|
235
|
-
const refs = plan.meta.refs;
|
|
236
|
-
if (refs) evaluateIndexCoverage(refs, lints$1);
|
|
237
|
-
return {
|
|
238
|
-
lints: lints$1,
|
|
239
|
-
budgets: budgets$1,
|
|
240
|
-
statement: statementType
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
function evaluateIndexCoverage(refs, lints$1) {
|
|
244
|
-
const predicateColumns = refs.columns ?? [];
|
|
245
|
-
if (predicateColumns.length === 0) return;
|
|
246
|
-
const indexes = refs.indexes ?? [];
|
|
247
|
-
if (indexes.length === 0) {
|
|
248
|
-
lints$1.push(createLint("LINT.UNINDEXED_PREDICATE", "warn", "Raw SQL plan predicates lack supporting indexes", { predicates: predicateColumns }));
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
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 }));
|
|
252
|
-
}
|
|
253
|
-
function classifyStatement(sql) {
|
|
254
|
-
const trimmed = sql.trim();
|
|
255
|
-
const lower = trimmed.toLowerCase();
|
|
256
|
-
if (lower.startsWith("with")) {
|
|
257
|
-
if (lower.includes("select")) return "select";
|
|
258
|
-
}
|
|
259
|
-
if (lower.startsWith("select")) return "select";
|
|
260
|
-
if (MUTATION_PREFIX_REGEX.test(trimmed)) return "mutation";
|
|
261
|
-
return "other";
|
|
262
|
-
}
|
|
263
|
-
function isMutationStatement(statement) {
|
|
264
|
-
return statement === "mutation";
|
|
265
|
-
}
|
|
266
|
-
function isReadOnlyIntent(meta) {
|
|
267
|
-
const annotations = meta.annotations;
|
|
268
|
-
const intent = typeof annotations?.intent === "string" ? annotations.intent.toLowerCase() : void 0;
|
|
269
|
-
return intent !== void 0 && READ_ONLY_INTENTS.has(intent);
|
|
270
|
-
}
|
|
271
|
-
function normalizeWhitespace(value) {
|
|
272
|
-
return value.replace(/\s+/g, " ").trim();
|
|
273
|
-
}
|
|
274
|
-
function snippet(sql) {
|
|
275
|
-
return normalizeWhitespace(sql).slice(0, 200);
|
|
276
|
-
}
|
|
277
|
-
function createLint(code, severity, message, details) {
|
|
278
|
-
return {
|
|
279
|
-
code,
|
|
280
|
-
severity,
|
|
281
|
-
message,
|
|
282
|
-
...details ? { details } : {}
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
function createBudget(code, severity, message, details) {
|
|
286
|
-
return {
|
|
287
|
-
code,
|
|
288
|
-
severity,
|
|
289
|
-
message,
|
|
290
|
-
...details ? { details } : {}
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
//#endregion
|
|
295
|
-
//#region src/middleware/lints.ts
|
|
296
|
-
function getFromSourceTableDetail(source) {
|
|
297
|
-
switch (source.kind) {
|
|
298
|
-
case "table-source": return source.name;
|
|
299
|
-
case "derived-table-source": return source.alias;
|
|
300
|
-
default: throw new Error(`Unsupported source kind: ${source.kind}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
function evaluateAstLints(ast) {
|
|
304
|
-
const findings = [];
|
|
305
|
-
switch (ast.kind) {
|
|
306
|
-
case "delete":
|
|
307
|
-
if (ast.where === void 0) findings.push({
|
|
308
|
-
code: "LINT.DELETE_WITHOUT_WHERE",
|
|
309
|
-
severity: "error",
|
|
310
|
-
message: "DELETE without WHERE clause blocks execution to prevent accidental full-table deletion",
|
|
311
|
-
details: { table: ast.table.name }
|
|
312
|
-
});
|
|
313
|
-
break;
|
|
314
|
-
case "update":
|
|
315
|
-
if (ast.where === void 0) findings.push({
|
|
316
|
-
code: "LINT.UPDATE_WITHOUT_WHERE",
|
|
317
|
-
severity: "error",
|
|
318
|
-
message: "UPDATE without WHERE clause blocks execution to prevent accidental full-table update",
|
|
319
|
-
details: { table: ast.table.name }
|
|
320
|
-
});
|
|
321
|
-
break;
|
|
322
|
-
case "select":
|
|
323
|
-
if (ast.limit === void 0) {
|
|
324
|
-
const table = getFromSourceTableDetail(ast.from);
|
|
325
|
-
findings.push({
|
|
326
|
-
code: "LINT.NO_LIMIT",
|
|
327
|
-
severity: "warn",
|
|
328
|
-
message: "Unbounded SELECT may return large result sets",
|
|
329
|
-
...ifDefined("details", table !== void 0 ? { table } : void 0)
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
if (ast.selectAllIntent !== void 0) {
|
|
333
|
-
const table = ast.selectAllIntent.table;
|
|
334
|
-
findings.push({
|
|
335
|
-
code: "LINT.SELECT_STAR",
|
|
336
|
-
severity: "warn",
|
|
337
|
-
message: "Query selects all columns via selectAll intent",
|
|
338
|
-
...ifDefined("details", table !== void 0 ? { table } : void 0)
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
break;
|
|
342
|
-
case "insert": break;
|
|
343
|
-
default: throw new Error(`Unsupported AST kind: ${ast.kind}`);
|
|
344
|
-
}
|
|
345
|
-
return findings;
|
|
346
|
-
}
|
|
347
|
-
function getConfiguredSeverity(code, options) {
|
|
348
|
-
const severities = options?.severities;
|
|
349
|
-
if (!severities) return void 0;
|
|
350
|
-
switch (code) {
|
|
351
|
-
case "LINT.SELECT_STAR": return severities.selectStar;
|
|
352
|
-
case "LINT.NO_LIMIT": return severities.noLimit;
|
|
353
|
-
case "LINT.DELETE_WITHOUT_WHERE": return severities.deleteWithoutWhere;
|
|
354
|
-
case "LINT.UPDATE_WITHOUT_WHERE": return severities.updateWithoutWhere;
|
|
355
|
-
case "LINT.READ_ONLY_MUTATION": return severities.readOnlyMutation;
|
|
356
|
-
case "LINT.UNINDEXED_PREDICATE": return severities.unindexedPredicate;
|
|
357
|
-
default: return;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* AST-first lint middleware for SQL plans. When `plan.ast` is a SQL QueryAst, inspects
|
|
362
|
-
* the AST structurally. When `plan.ast` is missing, falls back to raw heuristic
|
|
363
|
-
* guardrails or skips linting depending on `fallbackWhenAstMissing`.
|
|
364
|
-
*
|
|
365
|
-
* Rules (AST-based):
|
|
366
|
-
* - DELETE without WHERE: blocks execution (configurable severity, default error)
|
|
367
|
-
* - UPDATE without WHERE: blocks execution (configurable severity, default error)
|
|
368
|
-
* - Unbounded SELECT: warn/error (severity from noLimit)
|
|
369
|
-
* - SELECT * intent: warn/error (severity from selectStar)
|
|
370
|
-
*
|
|
371
|
-
* Fallback: When ast is missing, `fallbackWhenAstMissing: 'raw'` uses heuristic
|
|
372
|
-
* SQL parsing; `'skip'` skips all lints. Default is `'raw'`.
|
|
373
|
-
*/
|
|
374
|
-
function lints(options) {
|
|
375
|
-
const fallback = options?.fallbackWhenAstMissing ?? "raw";
|
|
376
|
-
return Object.freeze({
|
|
377
|
-
name: "lints",
|
|
378
|
-
familyId: "sql",
|
|
379
|
-
async beforeExecute(plan, ctx) {
|
|
380
|
-
if (isQueryAst(plan.ast)) {
|
|
381
|
-
const findings = evaluateAstLints(plan.ast);
|
|
382
|
-
for (const lint of findings) {
|
|
383
|
-
const effectiveSeverity = getConfiguredSeverity(lint.code, options) ?? lint.severity;
|
|
384
|
-
if (effectiveSeverity === "error") throw runtimeError(lint.code, lint.message, lint.details);
|
|
385
|
-
if (effectiveSeverity === "warn") ctx.log.warn({
|
|
386
|
-
code: lint.code,
|
|
387
|
-
message: lint.message,
|
|
388
|
-
details: lint.details
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (fallback === "skip") return;
|
|
394
|
-
const evaluation = evaluateRawGuardrails(plan);
|
|
395
|
-
for (const lint of evaluation.lints) {
|
|
396
|
-
const effectiveSeverity = getConfiguredSeverity(lint.code, options) ?? lint.severity;
|
|
397
|
-
if (effectiveSeverity === "error") throw runtimeError(lint.code, lint.message, lint.details);
|
|
398
|
-
if (effectiveSeverity === "warn") ctx.log.warn({
|
|
399
|
-
code: lint.code,
|
|
400
|
-
message: lint.message,
|
|
401
|
-
details: lint.details
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
//#endregion
|
|
409
|
-
//#region src/sql-context.ts
|
|
410
|
-
function createSqlExecutionStack(options) {
|
|
411
|
-
return createExecutionStack({
|
|
412
|
-
target: options.target,
|
|
413
|
-
adapter: options.adapter,
|
|
414
|
-
driver: options.driver,
|
|
415
|
-
extensionPacks: options.extensionPacks
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
function assertExecutionStackContractRequirements(contract, stack) {
|
|
419
|
-
const providedComponentIds = new Set([
|
|
420
|
-
stack.target.id,
|
|
421
|
-
stack.adapter.id,
|
|
422
|
-
...stack.extensionPacks.map((pack) => pack.id)
|
|
423
|
-
]);
|
|
424
|
-
const result = checkContractComponentRequirements({
|
|
425
|
-
contract,
|
|
426
|
-
expectedTargetFamily: "sql",
|
|
427
|
-
expectedTargetId: stack.target.targetId,
|
|
428
|
-
providedComponentIds
|
|
429
|
-
});
|
|
430
|
-
if (result.familyMismatch) throw runtimeError("RUNTIME.CONTRACT_FAMILY_MISMATCH", `Contract target family '${result.familyMismatch.actual}' does not match runtime family '${result.familyMismatch.expected}'.`, {
|
|
431
|
-
actual: result.familyMismatch.actual,
|
|
432
|
-
expected: result.familyMismatch.expected
|
|
433
|
-
});
|
|
434
|
-
if (result.targetMismatch) throw runtimeError("RUNTIME.CONTRACT_TARGET_MISMATCH", `Contract target '${result.targetMismatch.actual}' does not match runtime target descriptor '${result.targetMismatch.expected}'.`, {
|
|
435
|
-
actual: result.targetMismatch.actual,
|
|
436
|
-
expected: result.targetMismatch.expected
|
|
437
|
-
});
|
|
438
|
-
if (result.missingExtensionPackIds.length > 0) {
|
|
439
|
-
const packIds = result.missingExtensionPackIds;
|
|
440
|
-
throw runtimeError("RUNTIME.MISSING_EXTENSION_PACK", `Contract requires extension pack(s) ${packIds.map((id) => `'${id}'`).join(", ")}, but runtime descriptors do not provide matching component(s).`, { packIds });
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
function validateTypeParams(typeParams, codecDescriptor, context) {
|
|
444
|
-
const result = codecDescriptor.paramsSchema(typeParams);
|
|
445
|
-
if (result instanceof type.errors) {
|
|
446
|
-
const messages = result.map((p) => p.message).join("; ");
|
|
447
|
-
throw runtimeError("RUNTIME.TYPE_PARAMS_INVALID", `Invalid typeParams for ${context.typeName ? `type '${context.typeName}'` : `column '${context.tableName}.${context.columnName}'`} (codecId: ${codecDescriptor.codecId}): ${messages}`, {
|
|
448
|
-
...context,
|
|
449
|
-
codecId: codecDescriptor.codecId,
|
|
450
|
-
typeParams
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
return result;
|
|
454
|
-
}
|
|
455
|
-
function collectParameterizedCodecDescriptors(contributors) {
|
|
456
|
-
const descriptors = /* @__PURE__ */ new Map();
|
|
457
|
-
for (const contributor of contributors) for (const descriptor of contributor.parameterizedCodecs()) {
|
|
458
|
-
if (descriptors.has(descriptor.codecId)) throw runtimeError("RUNTIME.DUPLICATE_PARAMETERIZED_CODEC", `Duplicate parameterized codec descriptor for codecId '${descriptor.codecId}'.`, { codecId: descriptor.codecId });
|
|
459
|
-
descriptors.set(descriptor.codecId, descriptor);
|
|
460
|
-
}
|
|
461
|
-
return descriptors;
|
|
462
|
-
}
|
|
463
|
-
function initializeTypeHelpers(storageTypes, codecDescriptors) {
|
|
464
|
-
const helpers = {};
|
|
465
|
-
if (!storageTypes) return helpers;
|
|
466
|
-
for (const [typeName, typeInstance] of Object.entries(storageTypes)) {
|
|
467
|
-
const descriptor = codecDescriptors.get(typeInstance.codecId);
|
|
468
|
-
if (descriptor) {
|
|
469
|
-
const validatedParams = validateTypeParams(typeInstance.typeParams, descriptor, { typeName });
|
|
470
|
-
if (descriptor.init) helpers[typeName] = descriptor.init(validatedParams);
|
|
471
|
-
else helpers[typeName] = typeInstance;
|
|
472
|
-
} else helpers[typeName] = typeInstance;
|
|
473
|
-
}
|
|
474
|
-
return helpers;
|
|
475
|
-
}
|
|
476
|
-
function validateColumnTypeParams(storage, codecDescriptors) {
|
|
477
|
-
for (const [tableName, table] of Object.entries(storage.tables)) for (const [columnName, column] of Object.entries(table.columns)) if (column.typeParams) {
|
|
478
|
-
const descriptor = codecDescriptors.get(column.codecId);
|
|
479
|
-
if (descriptor) validateTypeParams(column.typeParams, descriptor, {
|
|
480
|
-
tableName,
|
|
481
|
-
columnName
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
/**
|
|
486
|
-
* Builds a registry of compiled JSON Schema validators by scanning the contract
|
|
487
|
-
* for columns whose codec descriptor provides an `init` hook returning `{ validate }`.
|
|
488
|
-
*
|
|
489
|
-
* Handles both:
|
|
490
|
-
* - Inline `typeParams.schema` on columns
|
|
491
|
-
* - `typeRef` → `storage.types[ref]` with init hook results already in `types` registry
|
|
492
|
-
*/
|
|
493
|
-
function buildJsonSchemaValidatorRegistry(contract, types, codecDescriptors) {
|
|
494
|
-
const validators = /* @__PURE__ */ new Map();
|
|
495
|
-
const codecIdsWithInit = /* @__PURE__ */ new Set();
|
|
496
|
-
for (const [codecId, descriptor] of codecDescriptors) if (descriptor.init) codecIdsWithInit.add(codecId);
|
|
497
|
-
if (codecIdsWithInit.size === 0) return;
|
|
498
|
-
for (const [tableName, table] of Object.entries(contract.storage.tables)) for (const [columnName, column] of Object.entries(table.columns)) {
|
|
499
|
-
if (!codecIdsWithInit.has(column.codecId)) continue;
|
|
500
|
-
const key = `${tableName}.${columnName}`;
|
|
501
|
-
if (column.typeRef) {
|
|
502
|
-
const helper = types[column.typeRef];
|
|
503
|
-
if (helper?.validate) validators.set(key, helper.validate);
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
if (column.typeParams) {
|
|
507
|
-
const descriptor = codecDescriptors.get(column.codecId);
|
|
508
|
-
if (descriptor?.init) {
|
|
509
|
-
const helper = descriptor.init(column.typeParams);
|
|
510
|
-
if (helper?.validate) validators.set(key, helper.validate);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
if (validators.size === 0) return void 0;
|
|
515
|
-
return {
|
|
516
|
-
get: (key) => validators.get(key),
|
|
517
|
-
size: validators.size
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
function collectMutationDefaultGenerators(contributors) {
|
|
521
|
-
const generators = /* @__PURE__ */ new Map();
|
|
522
|
-
const owners = /* @__PURE__ */ new Map();
|
|
523
|
-
for (const contributor of contributors) {
|
|
524
|
-
const nextGenerators = contributor.mutationDefaultGenerators?.() ?? [];
|
|
525
|
-
for (const generator of nextGenerators) {
|
|
526
|
-
const existingOwner = owners.get(generator.id);
|
|
527
|
-
if (existingOwner !== void 0) throw runtimeError("RUNTIME.DUPLICATE_MUTATION_DEFAULT_GENERATOR", `Duplicate mutation default generator '${generator.id}'.`, {
|
|
528
|
-
id: generator.id,
|
|
529
|
-
existingOwner,
|
|
530
|
-
incomingOwner: contributor.id
|
|
531
|
-
});
|
|
532
|
-
generators.set(generator.id, generator);
|
|
533
|
-
owners.set(generator.id, contributor.id);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return generators;
|
|
537
|
-
}
|
|
538
|
-
function computeExecutionDefaultValue(spec, generatorRegistry) {
|
|
539
|
-
switch (spec.kind) {
|
|
540
|
-
case "generator": {
|
|
541
|
-
const generator = generatorRegistry.get(spec.id);
|
|
542
|
-
if (!generator) throw runtimeError("RUNTIME.MUTATION_DEFAULT_GENERATOR_MISSING", `Contract references mutation default generator '${spec.id}' but no runtime component provides it.`, { id: spec.id });
|
|
543
|
-
return generator.generate(spec.params);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
function applyMutationDefaults(contract, generatorRegistry, options) {
|
|
548
|
-
const defaults = contract.execution?.mutations.defaults ?? [];
|
|
549
|
-
if (defaults.length === 0) return [];
|
|
550
|
-
const applied = [];
|
|
551
|
-
const appliedColumns = /* @__PURE__ */ new Set();
|
|
552
|
-
for (const mutationDefault of defaults) {
|
|
553
|
-
if (mutationDefault.ref.table !== options.table) continue;
|
|
554
|
-
const defaultSpec = options.op === "create" ? mutationDefault.onCreate : mutationDefault.onUpdate;
|
|
555
|
-
if (!defaultSpec) continue;
|
|
556
|
-
const columnName = mutationDefault.ref.column;
|
|
557
|
-
if (Object.hasOwn(options.values, columnName) || appliedColumns.has(columnName)) continue;
|
|
558
|
-
applied.push({
|
|
559
|
-
column: columnName,
|
|
560
|
-
value: computeExecutionDefaultValue(defaultSpec, generatorRegistry)
|
|
561
|
-
});
|
|
562
|
-
appliedColumns.add(columnName);
|
|
563
|
-
}
|
|
564
|
-
return applied;
|
|
565
|
-
}
|
|
566
|
-
function createExecutionContext(options) {
|
|
567
|
-
const { contract, stack } = options;
|
|
568
|
-
assertExecutionStackContractRequirements(contract, stack);
|
|
569
|
-
const codecRegistry = createCodecRegistry();
|
|
570
|
-
const contributors = [
|
|
571
|
-
stack.target,
|
|
572
|
-
stack.adapter,
|
|
573
|
-
...stack.extensionPacks
|
|
574
|
-
];
|
|
575
|
-
for (const contributor of contributors) for (const c of contributor.codecs().values()) codecRegistry.register(c);
|
|
576
|
-
const queryOperationRegistry = createSqlOperationRegistry();
|
|
577
|
-
for (const contributor of contributors) for (const op of contributor.queryOperations?.() ?? []) queryOperationRegistry.register(op);
|
|
578
|
-
const parameterizedCodecDescriptors = collectParameterizedCodecDescriptors(contributors);
|
|
579
|
-
const mutationDefaultGeneratorRegistry = collectMutationDefaultGenerators(contributors);
|
|
580
|
-
if (parameterizedCodecDescriptors.size > 0) validateColumnTypeParams(contract.storage, parameterizedCodecDescriptors);
|
|
581
|
-
const types = initializeTypeHelpers(contract.storage.types, parameterizedCodecDescriptors);
|
|
582
|
-
const jsonSchemaValidators = buildJsonSchemaValidatorRegistry(contract, types, parameterizedCodecDescriptors);
|
|
583
|
-
return {
|
|
584
|
-
contract,
|
|
585
|
-
codecs: codecRegistry,
|
|
586
|
-
queryOperations: queryOperationRegistry,
|
|
587
|
-
types,
|
|
588
|
-
...jsonSchemaValidators ? { jsonSchemaValidators } : {},
|
|
589
|
-
applyMutationDefaults: (options$1) => applyMutationDefaults(contract, mutationDefaultGeneratorRegistry, options$1)
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
//#endregion
|
|
594
|
-
//#region src/sql-marker.ts
|
|
595
|
-
const ensureSchemaStatement = {
|
|
596
|
-
sql: "create schema if not exists prisma_contract",
|
|
597
|
-
params: []
|
|
598
|
-
};
|
|
599
|
-
const ensureTableStatement = {
|
|
600
|
-
sql: `create table if not exists prisma_contract.marker (
|
|
601
|
-
id smallint primary key default 1,
|
|
602
|
-
core_hash text not null,
|
|
603
|
-
profile_hash text not null,
|
|
604
|
-
contract_json jsonb,
|
|
605
|
-
canonical_version int,
|
|
606
|
-
updated_at timestamptz not null default now(),
|
|
607
|
-
app_tag text,
|
|
608
|
-
meta jsonb not null default '{}'
|
|
609
|
-
)`,
|
|
610
|
-
params: []
|
|
611
|
-
};
|
|
612
|
-
function readContractMarker() {
|
|
613
|
-
return {
|
|
614
|
-
sql: `select
|
|
615
|
-
core_hash,
|
|
616
|
-
profile_hash,
|
|
617
|
-
contract_json,
|
|
618
|
-
canonical_version,
|
|
619
|
-
updated_at,
|
|
620
|
-
app_tag,
|
|
621
|
-
meta
|
|
622
|
-
from prisma_contract.marker
|
|
623
|
-
where id = $1`,
|
|
624
|
-
params: [1]
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
function writeContractMarker(input) {
|
|
628
|
-
const baseParams = [
|
|
629
|
-
1,
|
|
630
|
-
input.storageHash,
|
|
631
|
-
input.profileHash,
|
|
632
|
-
input.contractJson ?? null,
|
|
633
|
-
input.canonicalVersion ?? null,
|
|
634
|
-
input.appTag ?? null,
|
|
635
|
-
JSON.stringify(input.meta ?? {})
|
|
636
|
-
];
|
|
637
|
-
return {
|
|
638
|
-
insert: {
|
|
639
|
-
sql: `insert into prisma_contract.marker (
|
|
640
|
-
id,
|
|
641
|
-
core_hash,
|
|
642
|
-
profile_hash,
|
|
643
|
-
contract_json,
|
|
644
|
-
canonical_version,
|
|
645
|
-
updated_at,
|
|
646
|
-
app_tag,
|
|
647
|
-
meta
|
|
648
|
-
) values (
|
|
649
|
-
$1,
|
|
650
|
-
$2,
|
|
651
|
-
$3,
|
|
652
|
-
$4::jsonb,
|
|
653
|
-
$5,
|
|
654
|
-
now(),
|
|
655
|
-
$6,
|
|
656
|
-
$7::jsonb
|
|
657
|
-
)`,
|
|
658
|
-
params: baseParams
|
|
659
|
-
},
|
|
660
|
-
update: {
|
|
661
|
-
sql: `update prisma_contract.marker set
|
|
662
|
-
core_hash = $2,
|
|
663
|
-
profile_hash = $3,
|
|
664
|
-
contract_json = $4::jsonb,
|
|
665
|
-
canonical_version = $5,
|
|
666
|
-
updated_at = now(),
|
|
667
|
-
app_tag = $6,
|
|
668
|
-
meta = $7::jsonb
|
|
669
|
-
where id = $1`,
|
|
670
|
-
params: baseParams
|
|
671
|
-
}
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
//#endregion
|
|
676
|
-
//#region src/codecs/json-schema-validation.ts
|
|
677
|
-
/**
|
|
678
|
-
* Validates a JSON value against its column's JSON Schema, if a validator exists.
|
|
679
|
-
*
|
|
680
|
-
* Throws `RUNTIME.JSON_SCHEMA_VALIDATION_FAILED` on validation failure.
|
|
681
|
-
* No-ops if no validator is registered for the column.
|
|
682
|
-
*/
|
|
683
|
-
function validateJsonValue(registry, table, column, value, direction, codecId) {
|
|
684
|
-
const key = `${table}.${column}`;
|
|
685
|
-
const validate = registry.get(key);
|
|
686
|
-
if (!validate) return;
|
|
687
|
-
const result = validate(value);
|
|
688
|
-
if (result.valid) return;
|
|
689
|
-
throw createJsonSchemaValidationError(table, column, direction, result.errors, codecId);
|
|
690
|
-
}
|
|
691
|
-
function createJsonSchemaValidationError(table, column, direction, errors, codecId) {
|
|
692
|
-
return runtimeError("RUNTIME.JSON_SCHEMA_VALIDATION_FAILED", `JSON schema validation failed for column '${table}.${column}' (${direction}): ${formatErrorSummary(errors)}`, {
|
|
693
|
-
table,
|
|
694
|
-
column,
|
|
695
|
-
codecId,
|
|
696
|
-
direction,
|
|
697
|
-
errors: [...errors]
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
function formatErrorSummary(errors) {
|
|
701
|
-
if (errors.length === 0) return "unknown validation error";
|
|
702
|
-
if (errors.length === 1) {
|
|
703
|
-
const err = errors[0];
|
|
704
|
-
return err.path === "/" ? err.message : `${err.path}: ${err.message}`;
|
|
705
|
-
}
|
|
706
|
-
return errors.map((err) => err.path === "/" ? err.message : `${err.path}: ${err.message}`).join("; ");
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
//#endregion
|
|
710
|
-
//#region src/codecs/decoding.ts
|
|
711
|
-
const WIRE_PREVIEW_LIMIT = 100;
|
|
712
|
-
function resolveRowCodec(alias, plan, registry) {
|
|
713
|
-
const planCodecId = plan.meta.annotations?.codecs?.[alias];
|
|
714
|
-
if (planCodecId) {
|
|
715
|
-
const codec$1 = registry.get(planCodecId);
|
|
716
|
-
if (codec$1) return codec$1;
|
|
717
|
-
}
|
|
718
|
-
if (plan.meta.projectionTypes) {
|
|
719
|
-
const typeId = plan.meta.projectionTypes[alias];
|
|
720
|
-
if (typeId) {
|
|
721
|
-
const codec$1 = registry.get(typeId);
|
|
722
|
-
if (codec$1) return codec$1;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
return null;
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Builds a lookup index from column name → { table, column } ref.
|
|
729
|
-
* Called once per decodeRow invocation to avoid O(aliases × refs) linear scans.
|
|
730
|
-
*/
|
|
731
|
-
function buildColumnRefIndex(plan) {
|
|
732
|
-
const columns = plan.meta.refs?.columns;
|
|
733
|
-
if (!columns) return null;
|
|
734
|
-
const index = /* @__PURE__ */ new Map();
|
|
735
|
-
for (const ref of columns) index.set(ref.column, ref);
|
|
736
|
-
return index;
|
|
737
|
-
}
|
|
738
|
-
function parseProjectionRef(value) {
|
|
739
|
-
if (value.startsWith("include:") || value.startsWith("operation:")) return null;
|
|
740
|
-
const separatorIndex = value.indexOf(".");
|
|
741
|
-
if (separatorIndex <= 0 || separatorIndex === value.length - 1) return null;
|
|
742
|
-
return {
|
|
743
|
-
table: value.slice(0, separatorIndex),
|
|
744
|
-
column: value.slice(separatorIndex + 1)
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
function resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex) {
|
|
748
|
-
if (projection && !Array.isArray(projection)) {
|
|
749
|
-
const mappedRef = projection[alias];
|
|
750
|
-
if (typeof mappedRef !== "string") return;
|
|
751
|
-
return parseProjectionRef(mappedRef) ?? void 0;
|
|
752
|
-
}
|
|
753
|
-
return fallbackColumnRefIndex?.get(alias);
|
|
754
|
-
}
|
|
755
|
-
function previewWireValue(wireValue) {
|
|
756
|
-
if (typeof wireValue === "string") return wireValue.length > WIRE_PREVIEW_LIMIT ? `${wireValue.substring(0, WIRE_PREVIEW_LIMIT)}...` : wireValue;
|
|
757
|
-
return String(wireValue).substring(0, WIRE_PREVIEW_LIMIT);
|
|
758
|
-
}
|
|
759
|
-
function isJsonSchemaValidationError(error) {
|
|
760
|
-
return isRuntimeError(error) && error.code === "RUNTIME.JSON_SCHEMA_VALIDATION_FAILED";
|
|
761
|
-
}
|
|
762
|
-
function wrapDecodeFailure(error, alias, ref, codec$1, wireValue) {
|
|
763
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
764
|
-
const wrapped = runtimeError("RUNTIME.DECODE_FAILED", `Failed to decode column ${ref ? `${ref.table}.${ref.column}` : alias} with codec '${codec$1.id}': ${message}`, {
|
|
765
|
-
...ref ? {
|
|
766
|
-
table: ref.table,
|
|
767
|
-
column: ref.column
|
|
768
|
-
} : { alias },
|
|
769
|
-
codec: codec$1.id,
|
|
770
|
-
wirePreview: previewWireValue(wireValue)
|
|
771
|
-
});
|
|
772
|
-
wrapped.cause = error;
|
|
773
|
-
throw wrapped;
|
|
774
|
-
}
|
|
775
|
-
function wrapIncludeAggregateFailure(error, alias, wireValue) {
|
|
776
|
-
const wrapped = runtimeError("RUNTIME.DECODE_FAILED", `Failed to parse JSON array for include alias '${alias}': ${error instanceof Error ? error.message : String(error)}`, {
|
|
777
|
-
alias,
|
|
778
|
-
wirePreview: previewWireValue(wireValue)
|
|
779
|
-
});
|
|
780
|
-
wrapped.cause = error;
|
|
781
|
-
throw wrapped;
|
|
782
|
-
}
|
|
783
|
-
function decodeIncludeAggregate(alias, wireValue) {
|
|
784
|
-
if (wireValue === null || wireValue === void 0) return [];
|
|
785
|
-
try {
|
|
786
|
-
let parsed;
|
|
787
|
-
if (typeof wireValue === "string") parsed = JSON.parse(wireValue);
|
|
788
|
-
else if (Array.isArray(wireValue)) parsed = wireValue;
|
|
789
|
-
else parsed = JSON.parse(String(wireValue));
|
|
790
|
-
if (!Array.isArray(parsed)) throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
|
|
791
|
-
return parsed;
|
|
792
|
-
} catch (error) {
|
|
793
|
-
wrapIncludeAggregateFailure(error, alias, wireValue);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* Decodes a single field. Single-armed: every cell takes the same path —
|
|
798
|
-
* `codec.decode → await → JSON-Schema validate → return plain value` — so
|
|
799
|
-
* sync- and async-authored codecs are indistinguishable to callers.
|
|
800
|
-
*/
|
|
801
|
-
async function decodeField(alias, wireValue, plan, registry, jsonValidators, projection, fallbackColumnRefIndex) {
|
|
802
|
-
if (wireValue === null || wireValue === void 0) return wireValue;
|
|
803
|
-
const codec$1 = resolveRowCodec(alias, plan, registry);
|
|
804
|
-
if (!codec$1) return wireValue;
|
|
805
|
-
const ref = resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex);
|
|
806
|
-
let decoded;
|
|
807
|
-
try {
|
|
808
|
-
decoded = await codec$1.decode(wireValue);
|
|
809
|
-
} catch (error) {
|
|
810
|
-
wrapDecodeFailure(error, alias, ref, codec$1, wireValue);
|
|
811
|
-
}
|
|
812
|
-
if (jsonValidators && ref) try {
|
|
813
|
-
validateJsonValue(jsonValidators, ref.table, ref.column, decoded, "decode", codec$1.id);
|
|
814
|
-
} catch (error) {
|
|
815
|
-
if (isJsonSchemaValidationError(error)) throw error;
|
|
816
|
-
wrapDecodeFailure(error, alias, ref, codec$1, wireValue);
|
|
817
|
-
}
|
|
818
|
-
return decoded;
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Decodes a row by dispatching all per-cell codec calls concurrently via
|
|
822
|
-
* `Promise.all`. Each cell follows the single-armed `decodeField` path.
|
|
823
|
-
* Failures are wrapped in `RUNTIME.DECODE_FAILED` with `{ table, column,
|
|
824
|
-
* codec }` (or `{ alias, codec }` when no column ref is resolvable) and the
|
|
825
|
-
* original error attached on `cause`.
|
|
826
|
-
*/
|
|
827
|
-
async function decodeRow(row, plan, registry, jsonValidators) {
|
|
828
|
-
const projection = plan.meta.projection;
|
|
829
|
-
const fallbackColumnRefIndex = !projection || Array.isArray(projection) ? buildColumnRefIndex(plan) : null;
|
|
830
|
-
let aliases;
|
|
831
|
-
if (projection && !Array.isArray(projection)) aliases = Object.keys(projection);
|
|
832
|
-
else if (projection && Array.isArray(projection)) aliases = projection;
|
|
833
|
-
else aliases = Object.keys(row);
|
|
834
|
-
const tasks = [];
|
|
835
|
-
const includeIndices = [];
|
|
836
|
-
for (let i = 0; i < aliases.length; i++) {
|
|
837
|
-
const alias = aliases[i];
|
|
838
|
-
const wireValue = row[alias];
|
|
839
|
-
const projectionValue = projection && typeof projection === "object" && !Array.isArray(projection) ? projection[alias] : void 0;
|
|
840
|
-
if (typeof projectionValue === "string" && projectionValue.startsWith("include:")) {
|
|
841
|
-
includeIndices.push({
|
|
842
|
-
index: i,
|
|
843
|
-
alias,
|
|
844
|
-
value: wireValue
|
|
845
|
-
});
|
|
846
|
-
tasks.push(Promise.resolve(void 0));
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
849
|
-
tasks.push(decodeField(alias, wireValue, plan, registry, jsonValidators, projection, fallbackColumnRefIndex));
|
|
850
|
-
}
|
|
851
|
-
const settled = await Promise.all(tasks);
|
|
852
|
-
for (const entry of includeIndices) settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value);
|
|
853
|
-
const decoded = {};
|
|
854
|
-
for (let i = 0; i < aliases.length; i++) decoded[aliases[i]] = settled[i];
|
|
855
|
-
return decoded;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
//#endregion
|
|
859
|
-
//#region src/codecs/encoding.ts
|
|
860
|
-
function resolveParamCodec(paramDescriptor, registry) {
|
|
861
|
-
if (paramDescriptor.codecId) {
|
|
862
|
-
const codec$1 = registry.get(paramDescriptor.codecId);
|
|
863
|
-
if (codec$1) return codec$1;
|
|
864
|
-
}
|
|
865
|
-
return null;
|
|
866
|
-
}
|
|
867
|
-
function paramLabel(paramDescriptor, paramIndex) {
|
|
868
|
-
return paramDescriptor.name ?? `param[${paramIndex}]`;
|
|
869
|
-
}
|
|
870
|
-
function wrapEncodeFailure(error, paramDescriptor, paramIndex, codecId) {
|
|
871
|
-
const label = paramLabel(paramDescriptor, paramIndex);
|
|
872
|
-
const wrapped = runtimeError("RUNTIME.ENCODE_FAILED", `Failed to encode parameter ${label} with codec '${codecId}': ${error instanceof Error ? error.message : String(error)}`, {
|
|
873
|
-
label,
|
|
874
|
-
codec: codecId,
|
|
875
|
-
paramIndex
|
|
876
|
-
});
|
|
877
|
-
wrapped.cause = error;
|
|
878
|
-
throw wrapped;
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
|
-
* Encodes a single parameter through its codec. Always awaits codec.encode so
|
|
882
|
-
* a Promise can never leak into the driver, even if a sync-authored codec is
|
|
883
|
-
* lifted to async by the codec() factory. Failures are wrapped in
|
|
884
|
-
* `RUNTIME.ENCODE_FAILED` with `{ label, codec, paramIndex }` and the original
|
|
885
|
-
* error attached on `cause`.
|
|
886
|
-
*/
|
|
887
|
-
async function encodeParam(value, paramDescriptor, paramIndex, registry) {
|
|
888
|
-
if (value === null || value === void 0) return null;
|
|
889
|
-
const codec$1 = resolveParamCodec(paramDescriptor, registry);
|
|
890
|
-
if (!codec$1) return value;
|
|
891
|
-
try {
|
|
892
|
-
return await codec$1.encode(value);
|
|
893
|
-
} catch (error) {
|
|
894
|
-
wrapEncodeFailure(error, paramDescriptor, paramIndex, codec$1.id);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* Encodes all parameters concurrently via `Promise.all`. Per parameter, sync-
|
|
899
|
-
* and async-authored codecs share the same path: `codec.encode → await →
|
|
900
|
-
* return`. Param-level failures are wrapped in `RUNTIME.ENCODE_FAILED`.
|
|
901
|
-
*/
|
|
902
|
-
async function encodeParams(plan, registry) {
|
|
903
|
-
if (plan.params.length === 0) return plan.params;
|
|
904
|
-
const descriptorCount = plan.meta.paramDescriptors.length;
|
|
905
|
-
const paramCount = plan.params.length;
|
|
906
|
-
const tasks = new Array(paramCount);
|
|
907
|
-
for (let i = 0; i < paramCount; i++) {
|
|
908
|
-
const paramValue = plan.params[i];
|
|
909
|
-
const paramDescriptor = plan.meta.paramDescriptors[i];
|
|
910
|
-
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.`, {
|
|
911
|
-
paramIndex: i,
|
|
912
|
-
paramCount,
|
|
913
|
-
descriptorCount
|
|
914
|
-
});
|
|
915
|
-
tasks[i] = encodeParam(paramValue, paramDescriptor, i, registry);
|
|
916
|
-
}
|
|
917
|
-
const encoded = await Promise.all(tasks);
|
|
918
|
-
return Object.freeze(encoded);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
//#endregion
|
|
922
|
-
//#region src/fingerprint.ts
|
|
923
|
-
const STRING_LITERAL_REGEX = /'(?:''|[^'])*'/g;
|
|
924
|
-
const NUMERIC_LITERAL_REGEX = /\b\d+(?:\.\d+)?\b/g;
|
|
925
|
-
const WHITESPACE_REGEX = /\s+/g;
|
|
926
|
-
/**
|
|
927
|
-
* Computes a literal-stripped, normalized fingerprint of a SQL statement.
|
|
928
|
-
*
|
|
929
|
-
* The function strips string and numeric literals, collapses whitespace, and
|
|
930
|
-
* lowercases the result before hashing — so two structurally equivalent
|
|
931
|
-
* statements (with different parameter values) produce the same fingerprint.
|
|
932
|
-
* Used by SQL telemetry to group queries.
|
|
933
|
-
*/
|
|
934
|
-
function computeSqlFingerprint(sql) {
|
|
935
|
-
const normalized = sql.replace(STRING_LITERAL_REGEX, "?").replace(NUMERIC_LITERAL_REGEX, "?").replace(WHITESPACE_REGEX, " ").trim().toLowerCase();
|
|
936
|
-
return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
//#endregion
|
|
940
|
-
//#region src/marker.ts
|
|
941
|
-
const MetaSchema = type({ "[string]": "unknown" });
|
|
942
|
-
function parseMeta(meta) {
|
|
943
|
-
if (meta === null || meta === void 0) return {};
|
|
944
|
-
let parsed;
|
|
945
|
-
if (typeof meta === "string") try {
|
|
946
|
-
parsed = JSON.parse(meta);
|
|
947
|
-
} catch {
|
|
948
|
-
return {};
|
|
949
|
-
}
|
|
950
|
-
else parsed = meta;
|
|
951
|
-
const result = MetaSchema(parsed);
|
|
952
|
-
if (result instanceof type.errors) return {};
|
|
953
|
-
return result;
|
|
954
|
-
}
|
|
955
|
-
const ContractMarkerRowSchema = type({
|
|
956
|
-
core_hash: "string",
|
|
957
|
-
profile_hash: "string",
|
|
958
|
-
"contract_json?": "unknown | null",
|
|
959
|
-
"canonical_version?": "number | null",
|
|
960
|
-
"updated_at?": "Date | string",
|
|
961
|
-
"app_tag?": "string | null",
|
|
962
|
-
"meta?": "unknown | null"
|
|
963
|
-
});
|
|
964
|
-
function parseContractMarkerRow(row) {
|
|
965
|
-
const result = ContractMarkerRowSchema(row);
|
|
966
|
-
if (result instanceof type.errors) {
|
|
967
|
-
const messages = result.map((p) => p.message).join("; ");
|
|
968
|
-
throw new Error(`Invalid contract marker row: ${messages}`);
|
|
969
|
-
}
|
|
970
|
-
const validatedRow = result;
|
|
971
|
-
const updatedAt = validatedRow.updated_at ? validatedRow.updated_at instanceof Date ? validatedRow.updated_at : new Date(validatedRow.updated_at) : /* @__PURE__ */ new Date();
|
|
972
|
-
return {
|
|
973
|
-
storageHash: validatedRow.core_hash,
|
|
974
|
-
profileHash: validatedRow.profile_hash,
|
|
975
|
-
contractJson: validatedRow.contract_json ?? null,
|
|
976
|
-
canonicalVersion: validatedRow.canonical_version ?? null,
|
|
977
|
-
updatedAt,
|
|
978
|
-
appTag: validatedRow.app_tag ?? null,
|
|
979
|
-
meta: parseMeta(validatedRow.meta)
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
//#endregion
|
|
984
|
-
//#region src/middleware/before-compile-chain.ts
|
|
985
|
-
async function runBeforeCompileChain(middleware, initial, ctx) {
|
|
986
|
-
let current = initial;
|
|
987
|
-
for (const mw of middleware) {
|
|
988
|
-
if (!mw.beforeCompile) continue;
|
|
989
|
-
const result = await mw.beforeCompile(current, ctx);
|
|
990
|
-
if (result === void 0) continue;
|
|
991
|
-
if (result.ast === current.ast) continue;
|
|
992
|
-
ctx.log.debug?.({
|
|
993
|
-
event: "middleware.rewrite",
|
|
994
|
-
middleware: mw.name,
|
|
995
|
-
lane: current.meta.lane
|
|
996
|
-
});
|
|
997
|
-
current = result;
|
|
998
|
-
}
|
|
999
|
-
if (current.ast === initial.ast) return current;
|
|
1000
|
-
const paramDescriptors = deriveParamDescriptorsFromAst(current.ast);
|
|
1001
|
-
const meta = {
|
|
1002
|
-
...current.meta,
|
|
1003
|
-
paramDescriptors
|
|
1004
|
-
};
|
|
1005
|
-
return {
|
|
1006
|
-
ast: current.ast,
|
|
1007
|
-
meta
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
function deriveParamDescriptorsFromAst(ast) {
|
|
1011
|
-
const refs = ast.collectParamRefs();
|
|
1012
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1013
|
-
const descriptors = [];
|
|
1014
|
-
for (const ref of refs) {
|
|
1015
|
-
if (seen.has(ref)) continue;
|
|
1016
|
-
seen.add(ref);
|
|
1017
|
-
descriptors.push({
|
|
1018
|
-
index: descriptors.length + 1,
|
|
1019
|
-
...ref.name !== void 0 ? { name: ref.name } : {},
|
|
1020
|
-
source: "dsl",
|
|
1021
|
-
...ref.codecId !== void 0 ? { codecId: ref.codecId } : {}
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
return descriptors;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
//#endregion
|
|
1028
|
-
//#region src/sql-family-adapter.ts
|
|
1029
|
-
var SqlFamilyAdapter = class {
|
|
1030
|
-
contract;
|
|
1031
|
-
markerReader;
|
|
1032
|
-
constructor(contract, adapterProfile) {
|
|
1033
|
-
this.contract = contract;
|
|
1034
|
-
this.markerReader = adapterProfile;
|
|
1035
|
-
}
|
|
1036
|
-
validatePlan(plan, contract) {
|
|
1037
|
-
if (plan.meta.target !== contract.target) throw runtimeError("PLAN.TARGET_MISMATCH", "Plan target does not match runtime target", {
|
|
1038
|
-
planTarget: plan.meta.target,
|
|
1039
|
-
runtimeTarget: contract.target
|
|
1040
|
-
});
|
|
1041
|
-
if (plan.meta.storageHash !== contract.storage.storageHash) throw runtimeError("PLAN.HASH_MISMATCH", "Plan storage hash does not match runtime contract", {
|
|
1042
|
-
planStorageHash: plan.meta.storageHash,
|
|
1043
|
-
runtimeStorageHash: contract.storage.storageHash
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
};
|
|
1047
|
-
|
|
1048
|
-
//#endregion
|
|
1049
|
-
//#region src/sql-runtime.ts
|
|
1050
|
-
function isExecutionPlan(plan) {
|
|
1051
|
-
return "sql" in plan;
|
|
1052
|
-
}
|
|
1053
|
-
var SqlRuntimeImpl = class extends RuntimeCore {
|
|
1054
|
-
contract;
|
|
1055
|
-
adapter;
|
|
1056
|
-
driver;
|
|
1057
|
-
familyAdapter;
|
|
1058
|
-
codecRegistry;
|
|
1059
|
-
jsonSchemaValidators;
|
|
1060
|
-
sqlCtx;
|
|
1061
|
-
verify;
|
|
1062
|
-
codecRegistryValidated;
|
|
1063
|
-
verified;
|
|
1064
|
-
startupVerified;
|
|
1065
|
-
_telemetry;
|
|
1066
|
-
constructor(options) {
|
|
1067
|
-
const { context, adapter, driver, verify, middleware, mode, log } = options;
|
|
1068
|
-
if (middleware) for (const mw of middleware) checkMiddlewareCompatibility(mw, "sql", context.contract.target);
|
|
1069
|
-
const sqlCtx = {
|
|
1070
|
-
contract: context.contract,
|
|
1071
|
-
mode: mode ?? "strict",
|
|
1072
|
-
now: () => Date.now(),
|
|
1073
|
-
log: log ?? {
|
|
1074
|
-
info: () => {},
|
|
1075
|
-
warn: () => {},
|
|
1076
|
-
error: () => {}
|
|
1077
|
-
}
|
|
1078
|
-
};
|
|
1079
|
-
super({
|
|
1080
|
-
middleware: middleware ?? [],
|
|
1081
|
-
ctx: sqlCtx
|
|
1082
|
-
});
|
|
1083
|
-
this.contract = context.contract;
|
|
1084
|
-
this.adapter = adapter;
|
|
1085
|
-
this.driver = driver;
|
|
1086
|
-
this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
1087
|
-
this.codecRegistry = context.codecs;
|
|
1088
|
-
this.jsonSchemaValidators = context.jsonSchemaValidators;
|
|
1089
|
-
this.sqlCtx = sqlCtx;
|
|
1090
|
-
this.verify = verify;
|
|
1091
|
-
this.codecRegistryValidated = false;
|
|
1092
|
-
this.verified = verify.mode === "startup" ? false : verify.mode === "always";
|
|
1093
|
-
this.startupVerified = false;
|
|
1094
|
-
this._telemetry = null;
|
|
1095
|
-
if (verify.mode === "startup") {
|
|
1096
|
-
validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
|
|
1097
|
-
this.codecRegistryValidated = true;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with
|
|
1102
|
-
* encoded parameters ready for the driver. This is the single point at
|
|
1103
|
-
* which params transition from app-layer values to driver wire-format.
|
|
1104
|
-
*/
|
|
1105
|
-
async lower(plan) {
|
|
1106
|
-
const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
|
|
1107
|
-
return Object.freeze({
|
|
1108
|
-
...lowered,
|
|
1109
|
-
params: await encodeParams(lowered, this.codecRegistry)
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Default driver invocation. Production execution paths override the
|
|
1114
|
-
* queryable target (e.g. transaction or connection) by going through
|
|
1115
|
-
* `executeAgainstQueryable`; this implementation supports any caller of
|
|
1116
|
-
* `super.execute(plan)` and the abstract-base contract.
|
|
1117
|
-
*/
|
|
1118
|
-
runDriver(exec) {
|
|
1119
|
-
return this.driver.execute({
|
|
1120
|
-
sql: exec.sql,
|
|
1121
|
-
params: exec.params
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
/**
|
|
1125
|
-
* SQL pre-compile hook. Runs the registered middleware `beforeCompile`
|
|
1126
|
-
* chain over the plan's draft (AST + meta) and returns a `SqlQueryPlan`
|
|
1127
|
-
* with the rewritten AST and meta when the chain mutates them. The chain
|
|
1128
|
-
* re-derives `meta.paramDescriptors` from the rewritten AST so descriptors
|
|
1129
|
-
* stay in lockstep with the params the adapter will emit during lowering.
|
|
1130
|
-
*/
|
|
1131
|
-
async runBeforeCompile(plan) {
|
|
1132
|
-
const rewrittenDraft = await runBeforeCompileChain(this.middleware, {
|
|
1133
|
-
ast: plan.ast,
|
|
1134
|
-
meta: plan.meta
|
|
1135
|
-
}, this.sqlCtx);
|
|
1136
|
-
return rewrittenDraft.ast === plan.ast ? plan : {
|
|
1137
|
-
...plan,
|
|
1138
|
-
ast: rewrittenDraft.ast,
|
|
1139
|
-
meta: rewrittenDraft.meta
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
execute(plan) {
|
|
1143
|
-
return this.executeAgainstQueryable(plan, this.driver);
|
|
1144
|
-
}
|
|
1145
|
-
executeAgainstQueryable(plan, queryable) {
|
|
1146
|
-
this.ensureCodecRegistryValidated();
|
|
1147
|
-
const self = this;
|
|
1148
|
-
const generator = async function* () {
|
|
1149
|
-
const exec = isExecutionPlan(plan) ? Object.freeze({
|
|
1150
|
-
...plan,
|
|
1151
|
-
params: await encodeParams(plan, self.codecRegistry)
|
|
1152
|
-
}) : await self.lower(await self.runBeforeCompile(plan));
|
|
1153
|
-
self.familyAdapter.validatePlan(exec, self.contract);
|
|
1154
|
-
self._telemetry = null;
|
|
1155
|
-
if (!self.startupVerified && self.verify.mode === "startup") await self.verifyMarker();
|
|
1156
|
-
if (!self.verified && self.verify.mode === "onFirstUse") await self.verifyMarker();
|
|
1157
|
-
const startedAt = Date.now();
|
|
1158
|
-
let outcome = null;
|
|
1159
|
-
try {
|
|
1160
|
-
if (self.verify.mode === "always") await self.verifyMarker();
|
|
1161
|
-
const stream = runWithMiddleware(exec, self.middleware, self.ctx, () => queryable.execute({
|
|
1162
|
-
sql: exec.sql,
|
|
1163
|
-
params: exec.params
|
|
1164
|
-
}));
|
|
1165
|
-
for await (const rawRow of stream) yield await decodeRow(rawRow, exec, self.codecRegistry, self.jsonSchemaValidators);
|
|
1166
|
-
outcome = "success";
|
|
1167
|
-
} catch (error) {
|
|
1168
|
-
outcome = "runtime-error";
|
|
1169
|
-
throw error;
|
|
1170
|
-
} finally {
|
|
1171
|
-
if (outcome !== null) self.recordTelemetry(exec, outcome, Date.now() - startedAt);
|
|
1172
|
-
}
|
|
1173
|
-
};
|
|
1174
|
-
return new AsyncIterableResult(generator());
|
|
1175
|
-
}
|
|
1176
|
-
async connection() {
|
|
1177
|
-
const driverConn = await this.driver.acquireConnection();
|
|
1178
|
-
const self = this;
|
|
1179
|
-
return {
|
|
1180
|
-
async transaction() {
|
|
1181
|
-
const driverTx = await driverConn.beginTransaction();
|
|
1182
|
-
return self.wrapTransaction(driverTx);
|
|
1183
|
-
},
|
|
1184
|
-
async release() {
|
|
1185
|
-
await driverConn.release();
|
|
1186
|
-
},
|
|
1187
|
-
async destroy(reason) {
|
|
1188
|
-
await driverConn.destroy(reason);
|
|
1189
|
-
},
|
|
1190
|
-
execute(plan) {
|
|
1191
|
-
return self.executeAgainstQueryable(plan, driverConn);
|
|
1192
|
-
}
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
wrapTransaction(driverTx) {
|
|
1196
|
-
const self = this;
|
|
1197
|
-
return {
|
|
1198
|
-
async commit() {
|
|
1199
|
-
await driverTx.commit();
|
|
1200
|
-
},
|
|
1201
|
-
async rollback() {
|
|
1202
|
-
await driverTx.rollback();
|
|
1203
|
-
},
|
|
1204
|
-
execute(plan) {
|
|
1205
|
-
return self.executeAgainstQueryable(plan, driverTx);
|
|
1206
|
-
}
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
telemetry() {
|
|
1210
|
-
return this._telemetry;
|
|
1211
|
-
}
|
|
1212
|
-
async close() {
|
|
1213
|
-
await this.driver.close();
|
|
1214
|
-
}
|
|
1215
|
-
ensureCodecRegistryValidated() {
|
|
1216
|
-
if (!this.codecRegistryValidated) {
|
|
1217
|
-
validateCodecRegistryCompleteness(this.codecRegistry, this.contract);
|
|
1218
|
-
this.codecRegistryValidated = true;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
async verifyMarker() {
|
|
1222
|
-
if (this.verify.mode === "always") this.verified = false;
|
|
1223
|
-
if (this.verified) return;
|
|
1224
|
-
const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
|
|
1225
|
-
const result = await this.driver.query(readStatement.sql, readStatement.params);
|
|
1226
|
-
if (result.rows.length === 0) {
|
|
1227
|
-
if (this.verify.requireMarker) throw runtimeError("CONTRACT.MARKER_MISSING", "Contract marker not found in database");
|
|
1228
|
-
this.verified = true;
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
const marker = parseContractMarkerRow(result.rows[0]);
|
|
1232
|
-
const contract = this.contract;
|
|
1233
|
-
if (marker.storageHash !== contract.storage.storageHash) throw runtimeError("CONTRACT.MARKER_MISMATCH", "Database storage hash does not match contract", {
|
|
1234
|
-
expected: contract.storage.storageHash,
|
|
1235
|
-
actual: marker.storageHash
|
|
1236
|
-
});
|
|
1237
|
-
const expectedProfile = contract.profileHash ?? null;
|
|
1238
|
-
if (expectedProfile !== null && marker.profileHash !== expectedProfile) throw runtimeError("CONTRACT.MARKER_MISMATCH", "Database profile hash does not match contract", {
|
|
1239
|
-
expectedProfile,
|
|
1240
|
-
actualProfile: marker.profileHash
|
|
1241
|
-
});
|
|
1242
|
-
this.verified = true;
|
|
1243
|
-
this.startupVerified = true;
|
|
1244
|
-
}
|
|
1245
|
-
recordTelemetry(plan, outcome, durationMs) {
|
|
1246
|
-
const contract = this.contract;
|
|
1247
|
-
this._telemetry = Object.freeze({
|
|
1248
|
-
lane: plan.meta.lane,
|
|
1249
|
-
target: contract.target,
|
|
1250
|
-
fingerprint: computeSqlFingerprint(plan.sql),
|
|
1251
|
-
outcome,
|
|
1252
|
-
...durationMs !== void 0 ? { durationMs } : {}
|
|
1253
|
-
});
|
|
1254
|
-
}
|
|
1255
|
-
};
|
|
1256
|
-
function transactionClosedError() {
|
|
1257
|
-
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.", {});
|
|
1258
|
-
}
|
|
1259
|
-
async function withTransaction(runtime, fn) {
|
|
1260
|
-
const connection = await runtime.connection();
|
|
1261
|
-
const transaction = await connection.transaction();
|
|
1262
|
-
let invalidated = false;
|
|
1263
|
-
const txContext = {
|
|
1264
|
-
get invalidated() {
|
|
1265
|
-
return invalidated;
|
|
1266
|
-
},
|
|
1267
|
-
execute(plan) {
|
|
1268
|
-
if (invalidated) throw transactionClosedError();
|
|
1269
|
-
const inner = transaction.execute(plan);
|
|
1270
|
-
const guarded = async function* () {
|
|
1271
|
-
for await (const row of inner) {
|
|
1272
|
-
if (invalidated) throw transactionClosedError();
|
|
1273
|
-
yield row;
|
|
1274
|
-
}
|
|
1275
|
-
};
|
|
1276
|
-
return new AsyncIterableResult(guarded());
|
|
1277
|
-
}
|
|
1278
|
-
};
|
|
1279
|
-
let connectionDisposed = false;
|
|
1280
|
-
const destroyConnection = async (reason) => {
|
|
1281
|
-
if (connectionDisposed) return;
|
|
1282
|
-
connectionDisposed = true;
|
|
1283
|
-
await connection.destroy(reason).catch(() => void 0);
|
|
1284
|
-
};
|
|
1285
|
-
try {
|
|
1286
|
-
let result;
|
|
1287
|
-
try {
|
|
1288
|
-
result = await fn(txContext);
|
|
1289
|
-
} catch (error) {
|
|
1290
|
-
try {
|
|
1291
|
-
await transaction.rollback();
|
|
1292
|
-
} catch (rollbackError) {
|
|
1293
|
-
await destroyConnection(rollbackError);
|
|
1294
|
-
const wrapped = runtimeError("RUNTIME.TRANSACTION_ROLLBACK_FAILED", "Transaction rollback failed after callback error", { rollbackError });
|
|
1295
|
-
wrapped.cause = error;
|
|
1296
|
-
throw wrapped;
|
|
1297
|
-
}
|
|
1298
|
-
throw error;
|
|
1299
|
-
} finally {
|
|
1300
|
-
invalidated = true;
|
|
1301
|
-
}
|
|
1302
|
-
try {
|
|
1303
|
-
await transaction.commit();
|
|
1304
|
-
} catch (commitError) {
|
|
1305
|
-
try {
|
|
1306
|
-
await transaction.rollback();
|
|
1307
|
-
} catch {
|
|
1308
|
-
await destroyConnection(commitError);
|
|
1309
|
-
}
|
|
1310
|
-
const wrapped = runtimeError("RUNTIME.TRANSACTION_COMMIT_FAILED", "Transaction commit failed", { commitError });
|
|
1311
|
-
wrapped.cause = commitError;
|
|
1312
|
-
throw wrapped;
|
|
1313
|
-
}
|
|
1314
|
-
return result;
|
|
1315
|
-
} finally {
|
|
1316
|
-
if (!connectionDisposed) await connection.release();
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
function createRuntime(options) {
|
|
1320
|
-
const { stackInstance, context, driver, verify, middleware, mode, log } = options;
|
|
1321
|
-
return new SqlRuntimeImpl({
|
|
1322
|
-
context,
|
|
1323
|
-
adapter: stackInstance.adapter,
|
|
1324
|
-
driver,
|
|
1325
|
-
verify,
|
|
1326
|
-
...ifDefined("middleware", middleware),
|
|
1327
|
-
...ifDefined("mode", mode),
|
|
1328
|
-
...ifDefined("log", log)
|
|
1329
|
-
});
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
//#endregion
|
|
1333
|
-
export { readContractMarker as a, createSqlExecutionStack as c, lowerSqlPlan as d, extractCodecIds as f, ensureTableStatement as i, lints as l, validateContractCodecMappings as m, withTransaction as n, writeContractMarker as o, validateCodecRegistryCompleteness as p, ensureSchemaStatement as r, createExecutionContext as s, createRuntime as t, budgets as u };
|
|
1334
|
-
//# sourceMappingURL=exports-BOHa3Emo.mjs.map
|