@prisma-next/sql-runtime 0.5.0-dev.7 → 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.
Files changed (42) hide show
  1. package/README.md +29 -21
  2. package/dist/{exports-BQZSVXXt.mjs → exports-BOHa3Emo.mjs} +481 -128
  3. package/dist/exports-BOHa3Emo.mjs.map +1 -0
  4. package/dist/{index-yb51L_1h.d.mts → index-CZmC2kD3.d.mts} +53 -16
  5. package/dist/index-CZmC2kD3.d.mts.map +1 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +1 -1
  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 +7 -2
  11. package/dist/test/utils.mjs.map +1 -1
  12. package/package.json +12 -14
  13. package/src/codecs/decoding.ts +172 -116
  14. package/src/codecs/encoding.ts +59 -21
  15. package/src/exports/index.ts +10 -7
  16. package/src/fingerprint.ts +22 -0
  17. package/src/guardrails/raw.ts +214 -0
  18. package/src/lower-sql-plan.ts +3 -3
  19. package/src/marker.ts +82 -0
  20. package/src/middleware/before-compile-chain.ts +32 -1
  21. package/src/middleware/budgets.ts +14 -11
  22. package/src/middleware/lints.ts +3 -3
  23. package/src/middleware/sql-middleware.ts +6 -5
  24. package/src/runtime-spi.ts +43 -0
  25. package/src/sql-family-adapter.ts +3 -2
  26. package/src/sql-marker.ts +1 -1
  27. package/src/sql-runtime.ts +272 -110
  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";
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) {
@@ -619,6 +708,7 @@ function formatErrorSummary(errors) {
619
708
 
620
709
  //#endregion
621
710
  //#region src/codecs/decoding.ts
711
+ const WIRE_PREVIEW_LIMIT = 100;
622
712
  function resolveRowCodec(alias, plan, registry) {
623
713
  const planCodecId = plan.meta.annotations?.codecs?.[alias];
624
714
  if (planCodecId) {
@@ -662,72 +752,106 @@ function resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex) {
662
752
  }
663
753
  return fallbackColumnRefIndex?.get(alias);
664
754
  }
665
- function decodeRow(row, plan, registry, jsonValidators) {
666
- const decoded = {};
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) {
667
828
  const projection = plan.meta.projection;
668
- const fallbackColumnRefIndex = jsonValidators && (!projection || Array.isArray(projection)) ? buildColumnRefIndex(plan) : null;
829
+ const fallbackColumnRefIndex = !projection || Array.isArray(projection) ? buildColumnRefIndex(plan) : null;
669
830
  let aliases;
670
831
  if (projection && !Array.isArray(projection)) aliases = Object.keys(projection);
671
832
  else if (projection && Array.isArray(projection)) aliases = projection;
672
833
  else aliases = Object.keys(row);
673
- for (const alias of aliases) {
834
+ const tasks = [];
835
+ const includeIndices = [];
836
+ for (let i = 0; i < aliases.length; i++) {
837
+ const alias = aliases[i];
674
838
  const wireValue = row[alias];
675
839
  const projectionValue = projection && typeof projection === "object" && !Array.isArray(projection) ? projection[alias] : void 0;
676
840
  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 = {
841
+ includeIndices.push({
842
+ index: i,
724
843
  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;
844
+ value: wireValue
845
+ });
846
+ tasks.push(Promise.resolve(void 0));
847
+ continue;
729
848
  }
849
+ tasks.push(decodeField(alias, wireValue, plan, registry, jsonValidators, projection, fallbackColumnRefIndex));
730
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];
731
855
  return decoded;
732
856
  }
733
857
 
@@ -740,30 +864,122 @@ function resolveParamCodec(paramDescriptor, registry) {
740
864
  }
741
865
  return null;
742
866
  }
743
- function encodeParam(value, paramDescriptor, paramIndex, registry) {
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) {
744
888
  if (value === null || value === void 0) return null;
745
889
  const codec$1 = resolveParamCodec(paramDescriptor, registry);
746
890
  if (!codec$1) return value;
747
- if (codec$1.encode) try {
748
- return codec$1.encode(value);
891
+ try {
892
+ return await codec$1.encode(value);
749
893
  } 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)}`);
894
+ wrapEncodeFailure(error, paramDescriptor, paramIndex, codec$1.id);
752
895
  }
753
- return value;
754
896
  }
755
- function encodeParams(plan, registry) {
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) {
756
903
  if (plan.params.length === 0) return plan.params;
757
- const encoded = [];
758
- for (let i = 0; i < plan.params.length; i++) {
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++) {
759
908
  const paramValue = plan.params[i];
760
909
  const paramDescriptor = plan.meta.paramDescriptors[i];
761
- if (paramDescriptor) encoded.push(encodeParam(paramValue, paramDescriptor, i, registry));
762
- else encoded.push(paramValue);
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);
763
916
  }
917
+ const encoded = await Promise.all(tasks);
764
918
  return Object.freeze(encoded);
765
919
  }
766
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
+
767
983
  //#endregion
768
984
  //#region src/middleware/before-compile-chain.ts
769
985
  async function runBeforeCompileChain(middleware, initial, ctx) {
@@ -780,7 +996,32 @@ async function runBeforeCompileChain(middleware, initial, ctx) {
780
996
  });
781
997
  current = result;
782
998
  }
783
- return current;
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;
784
1025
  }
785
1026
 
786
1027
  //#endregion
@@ -806,102 +1047,214 @@ var SqlFamilyAdapter = class {
806
1047
 
807
1048
  //#endregion
808
1049
  //#region src/sql-runtime.ts
809
- var SqlRuntimeImpl = class {
810
- core;
1050
+ function isExecutionPlan(plan) {
1051
+ return "sql" in plan;
1052
+ }
1053
+ var SqlRuntimeImpl = class extends RuntimeCore {
811
1054
  contract;
812
1055
  adapter;
1056
+ driver;
1057
+ familyAdapter;
813
1058
  codecRegistry;
814
1059
  jsonSchemaValidators;
1060
+ sqlCtx;
1061
+ verify;
815
1062
  codecRegistryValidated;
1063
+ verified;
1064
+ startupVerified;
1065
+ _telemetry;
816
1066
  constructor(options) {
817
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
+ });
818
1083
  this.contract = context.contract;
819
1084
  this.adapter = adapter;
1085
+ this.driver = driver;
1086
+ this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
820
1087
  this.codecRegistry = context.codecs;
821
1088
  this.jsonSchemaValidators = context.jsonSchemaValidators;
1089
+ this.sqlCtx = sqlCtx;
1090
+ this.verify = verify;
822
1091
  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
- });
1092
+ this.verified = verify.mode === "startup" ? false : verify.mode === "always";
1093
+ this.startupVerified = false;
1094
+ this._telemetry = null;
832
1095
  if (verify.mode === "startup") {
833
1096
  validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
834
1097
  this.codecRegistryValidated = true;
835
1098
  }
836
1099
  }
837
- ensureCodecRegistryValidated(contract) {
838
- if (!this.codecRegistryValidated) {
839
- validateCodecRegistryCompleteness(this.codecRegistry, contract);
840
- this.codecRegistryValidated = true;
841
- }
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
+ });
842
1111
  }
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, {
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, {
849
1133
  ast: plan.ast,
850
1134
  meta: plan.meta
851
- }, this.core.middlewareContext);
852
- const planToLower = rewrittenDraft.ast === plan.ast ? plan : {
1135
+ }, this.sqlCtx);
1136
+ return rewrittenDraft.ast === plan.ast ? plan : {
853
1137
  ...plan,
854
- ast: rewrittenDraft.ast
1138
+ ast: rewrittenDraft.ast,
1139
+ meta: rewrittenDraft.meta
855
1140
  };
856
- return lowerSqlPlan(this.adapter, this.contract, planToLower);
1141
+ }
1142
+ execute(plan) {
1143
+ return this.executeAgainstQueryable(plan, this.driver);
857
1144
  }
858
1145
  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);
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
+ }
869
1173
  };
870
- return new AsyncIterableResult(iterator(this));
871
- }
872
- execute(plan) {
873
- return this.executeAgainstQueryable(plan, this.core);
1174
+ return new AsyncIterableResult(generator());
874
1175
  }
875
1176
  async connection() {
876
- const coreConn = await this.core.connection();
1177
+ const driverConn = await this.driver.acquireConnection();
877
1178
  const self = this;
878
1179
  return {
879
1180
  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
- };
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();
888
1203
  },
889
- release: coreConn.release.bind(coreConn),
890
- destroy: coreConn.destroy.bind(coreConn),
891
1204
  execute(plan) {
892
- return self.executeAgainstQueryable(plan, coreConn);
1205
+ return self.executeAgainstQueryable(plan, driverTx);
893
1206
  }
894
1207
  };
895
1208
  }
896
1209
  telemetry() {
897
- return this.core.telemetry();
1210
+ return this._telemetry;
1211
+ }
1212
+ async close() {
1213
+ await this.driver.close();
898
1214
  }
899
- close() {
900
- return this.core.close();
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
+ });
901
1254
  }
902
1255
  };
903
1256
  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.", {});
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.", {});
905
1258
  }
906
1259
  async function withTransaction(runtime, fn) {
907
1260
  const connection = await runtime.connection();
@@ -938,7 +1291,7 @@ async function withTransaction(runtime, fn) {
938
1291
  await transaction.rollback();
939
1292
  } catch (rollbackError) {
940
1293
  await destroyConnection(rollbackError);
941
- const wrapped = runtimeError$1("RUNTIME.TRANSACTION_ROLLBACK_FAILED", "Transaction rollback failed after callback error", { rollbackError });
1294
+ const wrapped = runtimeError("RUNTIME.TRANSACTION_ROLLBACK_FAILED", "Transaction rollback failed after callback error", { rollbackError });
942
1295
  wrapped.cause = error;
943
1296
  throw wrapped;
944
1297
  }
@@ -954,7 +1307,7 @@ async function withTransaction(runtime, fn) {
954
1307
  } catch {
955
1308
  await destroyConnection(commitError);
956
1309
  }
957
- const wrapped = runtimeError$1("RUNTIME.TRANSACTION_COMMIT_FAILED", "Transaction commit failed", { commitError });
1310
+ const wrapped = runtimeError("RUNTIME.TRANSACTION_COMMIT_FAILED", "Transaction commit failed", { commitError });
958
1311
  wrapped.cause = commitError;
959
1312
  throw wrapped;
960
1313
  }
@@ -978,4 +1331,4 @@ function createRuntime(options) {
978
1331
 
979
1332
  //#endregion
980
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 };
981
- //# sourceMappingURL=exports-BQZSVXXt.mjs.map
1334
+ //# sourceMappingURL=exports-BOHa3Emo.mjs.map