@prisma-next/sql-runtime 0.5.0-dev.8 → 0.5.0-dev.9
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-Cssiepsb.mjs → exports-BOHa3Emo.mjs} +326 -60
- package/dist/exports-BOHa3Emo.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-CZmC2kD3.d.mts} +53 -16
- package/dist/index-CZmC2kD3.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/test/utils.d.mts +6 -5
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +1 -1
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +9 -10
- package/src/codecs/decoding.ts +11 -7
- package/src/codecs/encoding.ts +3 -2
- package/src/exports/index.ts +10 -7
- package/src/fingerprint.ts +22 -0
- package/src/guardrails/raw.ts +214 -0
- package/src/lower-sql-plan.ts +3 -3
- package/src/marker.ts +82 -0
- package/src/middleware/budgets.ts +14 -11
- package/src/middleware/lints.ts +3 -3
- package/src/middleware/sql-middleware.ts +6 -5
- package/src/runtime-spi.ts +43 -0
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +1 -1
- package/src/sql-runtime.ts +272 -112
- package/dist/exports-Cssiepsb.mjs.map +0 -1
- package/dist/index-yb51L_1h.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ SQL runtime implementation for Prisma Next.
|
|
|
10
10
|
|
|
11
11
|
## Overview
|
|
12
12
|
|
|
13
|
-
The SQL runtime package implements the SQL family runtime by
|
|
13
|
+
The SQL runtime package implements the SQL family runtime by extending the abstract `RuntimeCore` base class from `@prisma-next/framework-components/runtime` with SQL-specific adapters, drivers, codecs, marker verification, telemetry fingerprinting, and a `beforeCompile` middleware chain. It provides the public runtime API for SQL-based databases, including descriptor-based static context derivation via `SqlStaticContributions` and execution-plane composition via `ExecutionStack`.
|
|
14
14
|
|
|
15
15
|
## Purpose
|
|
16
16
|
|
|
@@ -26,13 +26,16 @@ Execute SQL query Plans with deterministic verification, guardrails, and feedbac
|
|
|
26
26
|
- **SQL Marker Management**: Provide SQL statements for reading/writing contract markers
|
|
27
27
|
- **Codec Encoding/Decoding**: Encode parameters and decode rows using SQL codec registries
|
|
28
28
|
- **Codec Validation**: Validate that codec registries contain all required codecs
|
|
29
|
-
- **SQL Family Adapter**: Implement `RuntimeFamilyAdapter` for SQL contracts
|
|
30
|
-
- **
|
|
29
|
+
- **SQL Family Adapter**: Implement `RuntimeFamilyAdapter` for SQL contracts (defined in `runtime-spi.ts`)
|
|
30
|
+
- **Marker Verification**: Parse contract-marker rows from the database (`marker.ts`) and gate execution on hash equality
|
|
31
|
+
- **Telemetry Fingerprinting**: Compute SQL fingerprints for telemetry events (`fingerprint.ts`)
|
|
32
|
+
- **Raw-SQL Guardrails**: Heuristic safety checks for raw SQL plans (`guardrails/raw.ts`)
|
|
33
|
+
- **`beforeCompile` Chain**: AST-rewrite middleware chain run pre-lowering (`middleware/before-compile-chain.ts`)
|
|
34
|
+
- **SQL Runtime**: `SqlRuntime` extends `RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>` and overrides `lower`, `runDriver`, `runBeforeCompile`, and `close` with SQL-specific behaviour
|
|
31
35
|
|
|
32
36
|
## Dependencies
|
|
33
37
|
|
|
34
|
-
- `@prisma-next/framework-components` - Runtime component descriptor types (
|
|
35
|
-
- `@prisma-next/runtime-executor` - Target-neutral execution engine
|
|
38
|
+
- `@prisma-next/framework-components` - Runtime component descriptor types (`./execution`) and the abstract `RuntimeCore` base class plus `runWithMiddleware` helper (`./runtime`)
|
|
36
39
|
- `@prisma-next/sql-contract` - SQL contract types (via `@prisma-next/sql-contract/types`)
|
|
37
40
|
- `@prisma-next/operations` - Operation registry
|
|
38
41
|
|
|
@@ -44,7 +47,12 @@ import postgresDriver from '@prisma-next/driver-postgres/runtime';
|
|
|
44
47
|
import pgvector from '@prisma-next/extension-pgvector/runtime';
|
|
45
48
|
import postgresTarget from '@prisma-next/target-postgres/runtime';
|
|
46
49
|
import { instantiateExecutionStack } from '@prisma-next/framework-components/execution';
|
|
47
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
budgets,
|
|
52
|
+
createExecutionContext,
|
|
53
|
+
createRuntime,
|
|
54
|
+
createSqlExecutionStack,
|
|
55
|
+
} from '@prisma-next/sql-runtime';
|
|
48
56
|
|
|
49
57
|
const contract = validateContract<Contract>(contractJson);
|
|
50
58
|
const stack = createSqlExecutionStack({
|
|
@@ -65,7 +73,7 @@ const runtime = createRuntime({
|
|
|
65
73
|
context,
|
|
66
74
|
driver,
|
|
67
75
|
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
68
|
-
|
|
76
|
+
middleware: [budgets()],
|
|
69
77
|
});
|
|
70
78
|
|
|
71
79
|
for await (const row of runtime.execute(plan)) {
|
|
@@ -116,44 +124,44 @@ for await (const row of runtime.execute(plan)) {
|
|
|
116
124
|
|
|
117
125
|
- `lowerSqlPlan` - SQL plan lowering via adapter
|
|
118
126
|
|
|
119
|
-
###
|
|
127
|
+
### Middleware
|
|
120
128
|
|
|
121
|
-
- `budgets` - **AST-first budget
|
|
122
|
-
- `lints` - **AST-first lint
|
|
123
|
-
- `
|
|
124
|
-
- `
|
|
125
|
-
- `AfterExecuteResult` -
|
|
126
|
-
- `Log` - Log entry type
|
|
129
|
+
- `budgets` - **AST-first budget middleware** (canonical in SQL domain), inspects `plan.ast` when present for row estimation
|
|
130
|
+
- `lints` - **AST-first lint middleware** (canonical in SQL domain), inspects `plan.ast` when present
|
|
131
|
+
- `SqlMiddleware`, `SqlMiddlewareContext` - SQL-family middleware interface and per-execution context
|
|
132
|
+
- `BudgetsOptions`, `LintsOptions` - Middleware option types
|
|
133
|
+
- `AfterExecuteResult` - Middleware `afterExecute` hook result type (re-exported from `@prisma-next/framework-components/runtime`)
|
|
134
|
+
- `Log` - Log entry type (re-exported from `@prisma-next/framework-components/runtime`)
|
|
127
135
|
|
|
128
|
-
#### Lints
|
|
136
|
+
#### Lints middleware (SQL domain)
|
|
129
137
|
|
|
130
|
-
The `lints`
|
|
138
|
+
The `lints` middleware operates on `plan.ast` when it is a SQL `QueryAst`:
|
|
131
139
|
|
|
132
140
|
- **DELETE without WHERE** — blocks execution (configurable severity)
|
|
133
141
|
- **UPDATE without WHERE** — blocks execution (configurable severity)
|
|
134
142
|
- **Unbounded SELECT** — warns/errors when `limit` is missing
|
|
135
143
|
- **SELECT \* intent** — warns/errors when `selectAllIntent` is present
|
|
136
144
|
|
|
137
|
-
When `plan.ast` is missing, the
|
|
145
|
+
When `plan.ast` is missing, the middleware falls back to raw heuristic guardrails (`fallbackWhenAstMissing: 'raw'`) or skips linting (`fallbackWhenAstMissing: 'skip'`). Default is `'raw'`.
|
|
138
146
|
|
|
139
147
|
```typescript
|
|
140
148
|
import { createRuntime, lints } from '@prisma-next/sql-runtime';
|
|
141
149
|
|
|
142
150
|
const runtime = createRuntime({
|
|
143
151
|
// ...
|
|
144
|
-
|
|
152
|
+
middleware: [lints({ severities: { noLimit: 'error' } })],
|
|
145
153
|
});
|
|
146
154
|
```
|
|
147
155
|
|
|
148
156
|
## Architecture
|
|
149
157
|
|
|
150
|
-
The SQL runtime
|
|
158
|
+
The SQL runtime extends the abstract `RuntimeCore` base class from `@prisma-next/framework-components/runtime` with SQL-specific implementations. Descriptors implement `SqlStaticContributions` so `ExecutionContext` can be derived from the descriptors-only stack without instantiation.
|
|
151
159
|
|
|
152
160
|
1. **ExecutionStack**: Descriptors-only stack (from `@prisma-next/framework-components/execution`)
|
|
153
161
|
2. **SqlStaticContributions**: Codecs, operation signatures, parameterized codecs, and mutation default generators contributed by each descriptor
|
|
154
162
|
3. **ExecutionContext**: Built from contract + stack descriptors (no instantiation)
|
|
155
163
|
4. **ExecutionStackInstance**: Instantiated components used at runtime for execution
|
|
156
|
-
5. **SqlRuntime**:
|
|
164
|
+
5. **SqlRuntime**: `class SqlRuntimeImpl extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>` — overrides `lower` (with codec param-encoding), `runDriver`, `runBeforeCompile` (delegates to the SQL `beforeCompile` chain), and `close`. The execution path also wraps the `runWithMiddleware` helper from `framework-components/runtime` with codec row-decoding, marker verification (via the `RuntimeFamilyAdapter` defined in `runtime-spi.ts`), and telemetry fingerprinting (via `computeSqlFingerprint` from `fingerprint.ts`).
|
|
157
165
|
6. **SqlMarker**: Provides SQL statements for marker management
|
|
158
166
|
|
|
159
167
|
```mermaid
|
|
@@ -164,7 +172,7 @@ flowchart LR
|
|
|
164
172
|
Stack --> AdapterDesc[Adapter Descriptor]
|
|
165
173
|
Stack --> Packs[Extension Packs]
|
|
166
174
|
Context --> Runtime[SqlRuntime]
|
|
167
|
-
Runtime
|
|
175
|
+
Runtime -.extends.-> Core[RuntimeCore]
|
|
168
176
|
DriverDesc --> DriverInst[Driver Instance]
|
|
169
177
|
AdapterDesc --> AdapterInst[Adapter Instance]
|
|
170
178
|
Runtime --> DriverInst
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { checkMiddlewareCompatibility, isRuntimeError, runtimeError } from "@prisma-next/framework-components/runtime";
|
|
1
|
+
import { AsyncIterableResult, RuntimeCore, checkMiddlewareCompatibility, isRuntimeError, runWithMiddleware, runtimeError } from "@prisma-next/framework-components/runtime";
|
|
2
2
|
import { createCodecRegistry, isQueryAst } from "@prisma-next/sql-relational-core/ast";
|
|
3
|
-
import { AsyncIterableResult, createRuntimeCore, evaluateRawGuardrails, runtimeError as runtimeError$1 } from "@prisma-next/runtime-executor";
|
|
4
3
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
5
4
|
import { checkContractComponentRequirements } from "@prisma-next/framework-components/components";
|
|
6
5
|
import { createExecutionStack } from "@prisma-next/framework-components/execution";
|
|
7
6
|
import { createSqlOperationRegistry } from "@prisma-next/sql-operations";
|
|
8
7
|
import { type } from "arktype";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
9
|
|
|
10
10
|
//#region src/codecs/validation.ts
|
|
11
11
|
function extractCodecIds(contract) {
|
|
@@ -202,6 +202,95 @@ function budgets(options) {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
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
|
+
|
|
205
294
|
//#endregion
|
|
206
295
|
//#region src/middleware/lints.ts
|
|
207
296
|
function getFromSourceTableDetail(source) {
|
|
@@ -635,6 +724,10 @@ function resolveRowCodec(alias, plan, registry) {
|
|
|
635
724
|
}
|
|
636
725
|
return null;
|
|
637
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
|
+
*/
|
|
638
731
|
function buildColumnRefIndex(plan) {
|
|
639
732
|
const columns = plan.meta.refs?.columns;
|
|
640
733
|
if (!columns) return null;
|
|
@@ -825,6 +918,68 @@ async function encodeParams(plan, registry) {
|
|
|
825
918
|
return Object.freeze(encoded);
|
|
826
919
|
}
|
|
827
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
|
+
|
|
828
983
|
//#endregion
|
|
829
984
|
//#region src/middleware/before-compile-chain.ts
|
|
830
985
|
async function runBeforeCompileChain(middleware, initial, ctx) {
|
|
@@ -892,103 +1047,214 @@ var SqlFamilyAdapter = class {
|
|
|
892
1047
|
|
|
893
1048
|
//#endregion
|
|
894
1049
|
//#region src/sql-runtime.ts
|
|
895
|
-
|
|
896
|
-
|
|
1050
|
+
function isExecutionPlan(plan) {
|
|
1051
|
+
return "sql" in plan;
|
|
1052
|
+
}
|
|
1053
|
+
var SqlRuntimeImpl = class extends RuntimeCore {
|
|
897
1054
|
contract;
|
|
898
1055
|
adapter;
|
|
1056
|
+
driver;
|
|
1057
|
+
familyAdapter;
|
|
899
1058
|
codecRegistry;
|
|
900
1059
|
jsonSchemaValidators;
|
|
1060
|
+
sqlCtx;
|
|
1061
|
+
verify;
|
|
901
1062
|
codecRegistryValidated;
|
|
1063
|
+
verified;
|
|
1064
|
+
startupVerified;
|
|
1065
|
+
_telemetry;
|
|
902
1066
|
constructor(options) {
|
|
903
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
|
+
});
|
|
904
1083
|
this.contract = context.contract;
|
|
905
1084
|
this.adapter = adapter;
|
|
1085
|
+
this.driver = driver;
|
|
1086
|
+
this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
906
1087
|
this.codecRegistry = context.codecs;
|
|
907
1088
|
this.jsonSchemaValidators = context.jsonSchemaValidators;
|
|
1089
|
+
this.sqlCtx = sqlCtx;
|
|
1090
|
+
this.verify = verify;
|
|
908
1091
|
this.codecRegistryValidated = false;
|
|
909
|
-
|
|
910
|
-
this.
|
|
911
|
-
|
|
912
|
-
driver,
|
|
913
|
-
verify,
|
|
914
|
-
...ifDefined("middleware", middleware),
|
|
915
|
-
...ifDefined("mode", mode),
|
|
916
|
-
...ifDefined("log", log)
|
|
917
|
-
});
|
|
1092
|
+
this.verified = verify.mode === "startup" ? false : verify.mode === "always";
|
|
1093
|
+
this.startupVerified = false;
|
|
1094
|
+
this._telemetry = null;
|
|
918
1095
|
if (verify.mode === "startup") {
|
|
919
1096
|
validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
|
|
920
1097
|
this.codecRegistryValidated = true;
|
|
921
1098
|
}
|
|
922
1099
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
+
});
|
|
928
1111
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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, {
|
|
935
1133
|
ast: plan.ast,
|
|
936
1134
|
meta: plan.meta
|
|
937
|
-
}, this.
|
|
938
|
-
|
|
1135
|
+
}, this.sqlCtx);
|
|
1136
|
+
return rewrittenDraft.ast === plan.ast ? plan : {
|
|
939
1137
|
...plan,
|
|
940
1138
|
ast: rewrittenDraft.ast,
|
|
941
1139
|
meta: rewrittenDraft.meta
|
|
942
1140
|
};
|
|
943
|
-
|
|
1141
|
+
}
|
|
1142
|
+
execute(plan) {
|
|
1143
|
+
return this.executeAgainstQueryable(plan, this.driver);
|
|
944
1144
|
}
|
|
945
1145
|
executeAgainstQueryable(plan, queryable) {
|
|
946
|
-
this.ensureCodecRegistryValidated(
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
+
}
|
|
956
1173
|
};
|
|
957
|
-
return new AsyncIterableResult(
|
|
958
|
-
}
|
|
959
|
-
execute(plan) {
|
|
960
|
-
return this.executeAgainstQueryable(plan, this.core);
|
|
1174
|
+
return new AsyncIterableResult(generator());
|
|
961
1175
|
}
|
|
962
1176
|
async connection() {
|
|
963
|
-
const
|
|
1177
|
+
const driverConn = await this.driver.acquireConnection();
|
|
964
1178
|
const self = this;
|
|
965
1179
|
return {
|
|
966
1180
|
async transaction() {
|
|
967
|
-
const
|
|
968
|
-
return
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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();
|
|
975
1203
|
},
|
|
976
|
-
release: coreConn.release.bind(coreConn),
|
|
977
|
-
destroy: coreConn.destroy.bind(coreConn),
|
|
978
1204
|
execute(plan) {
|
|
979
|
-
return self.executeAgainstQueryable(plan,
|
|
1205
|
+
return self.executeAgainstQueryable(plan, driverTx);
|
|
980
1206
|
}
|
|
981
1207
|
};
|
|
982
1208
|
}
|
|
983
1209
|
telemetry() {
|
|
984
|
-
return this.
|
|
1210
|
+
return this._telemetry;
|
|
1211
|
+
}
|
|
1212
|
+
async close() {
|
|
1213
|
+
await this.driver.close();
|
|
985
1214
|
}
|
|
986
|
-
|
|
987
|
-
|
|
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
|
+
});
|
|
988
1254
|
}
|
|
989
1255
|
};
|
|
990
1256
|
function transactionClosedError() {
|
|
991
|
-
return runtimeError
|
|
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.", {});
|
|
992
1258
|
}
|
|
993
1259
|
async function withTransaction(runtime, fn) {
|
|
994
1260
|
const connection = await runtime.connection();
|
|
@@ -1025,7 +1291,7 @@ async function withTransaction(runtime, fn) {
|
|
|
1025
1291
|
await transaction.rollback();
|
|
1026
1292
|
} catch (rollbackError) {
|
|
1027
1293
|
await destroyConnection(rollbackError);
|
|
1028
|
-
const wrapped = runtimeError
|
|
1294
|
+
const wrapped = runtimeError("RUNTIME.TRANSACTION_ROLLBACK_FAILED", "Transaction rollback failed after callback error", { rollbackError });
|
|
1029
1295
|
wrapped.cause = error;
|
|
1030
1296
|
throw wrapped;
|
|
1031
1297
|
}
|
|
@@ -1041,7 +1307,7 @@ async function withTransaction(runtime, fn) {
|
|
|
1041
1307
|
} catch {
|
|
1042
1308
|
await destroyConnection(commitError);
|
|
1043
1309
|
}
|
|
1044
|
-
const wrapped = runtimeError
|
|
1310
|
+
const wrapped = runtimeError("RUNTIME.TRANSACTION_COMMIT_FAILED", "Transaction commit failed", { commitError });
|
|
1045
1311
|
wrapped.cause = commitError;
|
|
1046
1312
|
throw wrapped;
|
|
1047
1313
|
}
|
|
@@ -1065,4 +1331,4 @@ function createRuntime(options) {
|
|
|
1065
1331
|
|
|
1066
1332
|
//#endregion
|
|
1067
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 };
|
|
1068
|
-
//# sourceMappingURL=exports-
|
|
1334
|
+
//# sourceMappingURL=exports-BOHa3Emo.mjs.map
|