@prisma-next/sql-runtime 0.5.0-dev.3 → 0.5.0-dev.30

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