@prisma-next/sql-runtime 0.5.0-dev.22 → 0.5.0-dev.24

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.
@@ -1,6 +1,6 @@
1
1
  import { AsyncIterableResult, RuntimeCore, checkMiddlewareCompatibility, isRuntimeError, runWithMiddleware, runtimeError } from "@prisma-next/framework-components/runtime";
2
2
  import { type } from "arktype";
3
- import { createCodecRegistry, isQueryAst } from "@prisma-next/sql-relational-core/ast";
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";
@@ -124,25 +124,21 @@ function hasAggregateWithoutGroupBy(ast) {
124
124
  if (ast.groupBy !== void 0) return false;
125
125
  return ast.projection.some((item) => item.expr.kind === "aggregate");
126
126
  }
127
- 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) {
128
135
  if (hasAggregateWithoutGroup) return 1;
129
- const table = refs?.tables?.[0];
136
+ const table = primaryTableFromAst(ast);
130
137
  if (!table) return null;
131
138
  const tableEstimate = tableRows[table] ?? defaultTableRows;
132
139
  if (ast.limit !== void 0) return Math.min(ast.limit, tableEstimate);
133
140
  return tableEstimate;
134
141
  }
135
- function estimateRowsFromHeuristics(plan, tableRows, defaultTableRows) {
136
- const table = plan.meta.refs?.tables?.[0];
137
- if (!table) return null;
138
- const tableEstimate = tableRows[table] ?? defaultTableRows;
139
- const limit = plan.meta.annotations?.["limit"];
140
- if (typeof limit === "number") return Math.min(limit, tableEstimate);
141
- return tableEstimate;
142
- }
143
- function hasDetectableLimitFromHeuristics(plan) {
144
- return typeof plan.meta.annotations?.["limit"] === "number";
145
- }
146
142
  function emitBudgetViolation(error, shouldBlock, ctx) {
147
143
  if (shouldBlock) throw error;
148
144
  ctx.log.warn({
@@ -163,11 +159,7 @@ function budgets(options) {
163
159
  familyId: "sql",
164
160
  async beforeExecute(plan, ctx) {
165
161
  observedRowsByPlan.set(plan, { count: 0 });
166
- if (isQueryAst(plan.ast)) {
167
- if (plan.ast.kind === "select") return evaluateSelectAst(plan, plan.ast, ctx);
168
- return;
169
- }
170
- return evaluateWithHeuristics(plan, ctx);
162
+ if (isQueryAst(plan.ast) && plan.ast.kind === "select") return evaluateSelectAst(plan.ast, ctx);
171
163
  },
172
164
  async onRow(_row, plan, _ctx) {
173
165
  const state = observedRowsByPlan.get(plan);
@@ -190,9 +182,9 @@ function budgets(options) {
190
182
  }
191
183
  }
192
184
  });
193
- function evaluateSelectAst(plan, ast, ctx) {
185
+ function evaluateSelectAst(ast, ctx) {
194
186
  const hasAggNoGroup = hasAggregateWithoutGroupBy(ast);
195
- const estimated = estimateRowsFromAst(ast, tableRows, defaultTableRows, plan.meta.refs, hasAggNoGroup);
187
+ const estimated = estimateRowsFromAst(ast, tableRows, defaultTableRows, hasAggNoGroup);
196
188
  const isUnbounded = ast.limit === void 0 && !hasAggNoGroup;
197
189
  const shouldBlock = rowSeverity === "error" || ctx.mode === "strict";
198
190
  if (isUnbounded) {
@@ -216,35 +208,6 @@ function budgets(options) {
216
208
  maxRows
217
209
  }), shouldBlock, ctx);
218
210
  }
219
- async function evaluateWithHeuristics(plan, ctx) {
220
- const estimated = estimateRowsFromHeuristics(plan, tableRows, defaultTableRows);
221
- const isUnbounded = !hasDetectableLimitFromHeuristics(plan);
222
- const isSelect = plan.sql.trimStart().toUpperCase().startsWith("SELECT");
223
- const shouldBlock = rowSeverity === "error" || ctx.mode === "strict";
224
- if (isSelect && isUnbounded) {
225
- if (estimated !== null && estimated >= maxRows) {
226
- emitBudgetViolation(runtimeError("BUDGET.ROWS_EXCEEDED", "Unbounded SELECT query exceeds budget", {
227
- source: "heuristic",
228
- estimatedRows: estimated,
229
- maxRows
230
- }), shouldBlock, ctx);
231
- return;
232
- }
233
- emitBudgetViolation(runtimeError("BUDGET.ROWS_EXCEEDED", "Unbounded SELECT query exceeds budget", {
234
- source: "heuristic",
235
- maxRows
236
- }), shouldBlock, ctx);
237
- return;
238
- }
239
- if (estimated !== null) {
240
- if (estimated > maxRows) emitBudgetViolation(runtimeError("BUDGET.ROWS_EXCEEDED", "Estimated row count exceeds budget", {
241
- source: "heuristic",
242
- estimatedRows: estimated,
243
- maxRows
244
- }), shouldBlock, ctx);
245
- return;
246
- }
247
- }
248
211
  }
249
212
 
250
213
  //#endregion
@@ -277,24 +240,12 @@ function evaluateRawGuardrails(plan, config) {
277
240
  sql: snippet(plan.sql),
278
241
  intent: plan.meta.annotations?.["intent"]
279
242
  }));
280
- const refs = plan.meta.refs;
281
- if (refs) evaluateIndexCoverage(refs, lints$1);
282
243
  return {
283
244
  lints: lints$1,
284
245
  budgets: budgets$1,
285
246
  statement: statementType
286
247
  };
287
248
  }
288
- function evaluateIndexCoverage(refs, lints$1) {
289
- const predicateColumns = refs.columns ?? [];
290
- if (predicateColumns.length === 0) return;
291
- const indexes = refs.indexes ?? [];
292
- if (indexes.length === 0) {
293
- lints$1.push(createLint("LINT.UNINDEXED_PREDICATE", "warn", "Raw SQL plan predicates lack supporting indexes", { predicates: predicateColumns }));
294
- return;
295
- }
296
- 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 }));
297
- }
298
249
  function classifyStatement(sql) {
299
250
  const trimmed = sql.trim();
300
251
  const lower = trimmed.toLowerCase();
@@ -781,49 +732,51 @@ function formatErrorSummary(errors) {
781
732
  //#endregion
782
733
  //#region src/codecs/decoding.ts
783
734
  const WIRE_PREVIEW_LIMIT = 100;
784
- function resolveRowCodec(alias, plan, registry) {
785
- const planCodecId = plan.meta.annotations?.codecs?.[alias];
786
- if (planCodecId) {
787
- const codec$1 = registry.get(planCodecId);
788
- if (codec$1) return codec$1;
789
- }
790
- if (plan.meta.projectionTypes) {
791
- const typeId = plan.meta.projectionTypes[alias];
792
- if (typeId) {
793
- const codec$1 = registry.get(typeId);
794
- if (codec$1) return codec$1;
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);
795
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);
796
772
  }
797
- return null;
798
- }
799
- /**
800
- * Builds a lookup index from column name → { table, column } ref.
801
- * Called once per decodeRow invocation to avoid O(aliases × refs) linear scans.
802
- */
803
- function buildColumnRefIndex(plan) {
804
- const columns = plan.meta.refs?.columns;
805
- if (!columns) return null;
806
- const index = /* @__PURE__ */ new Map();
807
- for (const ref of columns) index.set(ref.column, ref);
808
- return index;
809
- }
810
- function parseProjectionRef(value) {
811
- if (value.startsWith("include:") || value.startsWith("operation:")) return null;
812
- const separatorIndex = value.indexOf(".");
813
- if (separatorIndex <= 0 || separatorIndex === value.length - 1) return null;
814
773
  return {
815
- table: value.slice(0, separatorIndex),
816
- column: value.slice(separatorIndex + 1)
774
+ aliases,
775
+ codecs,
776
+ columnRefs,
777
+ includeAliases
817
778
  };
818
779
  }
819
- function resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex) {
820
- if (projection && !Array.isArray(projection)) {
821
- const mappedRef = projection[alias];
822
- if (typeof mappedRef !== "string") return;
823
- return parseProjectionRef(mappedRef) ?? void 0;
824
- }
825
- return fallbackColumnRefIndex?.get(alias);
826
- }
827
780
  function previewWireValue(wireValue) {
828
781
  if (typeof wireValue === "string") return wireValue.length > WIRE_PREVIEW_LIMIT ? `${wireValue.substring(0, WIRE_PREVIEW_LIMIT)}...` : wireValue;
829
782
  return String(wireValue).substring(0, WIRE_PREVIEW_LIMIT);
@@ -870,11 +823,11 @@ function decodeIncludeAggregate(alias, wireValue) {
870
823
  * `codec.decode → await → JSON-Schema validate → return plain value` — so
871
824
  * sync- and async-authored codecs are indistinguishable to callers.
872
825
  */
873
- async function decodeField(alias, wireValue, plan, registry, jsonValidators, projection, fallbackColumnRefIndex) {
874
- if (wireValue === null || wireValue === void 0) return wireValue;
875
- const codec$1 = resolveRowCodec(alias, plan, registry);
826
+ async function decodeField(alias, wireValue, ctx, jsonValidators) {
827
+ if (wireValue === null) return null;
828
+ const codec$1 = ctx.codecs.get(alias);
876
829
  if (!codec$1) return wireValue;
877
- const ref = resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex);
830
+ const ref = ctx.columnRefs.get(alias);
878
831
  let decoded;
879
832
  try {
880
833
  decoded = await codec$1.decode(wireValue);
@@ -897,19 +850,21 @@ async function decodeField(alias, wireValue, plan, registry, jsonValidators, pro
897
850
  * original error attached on `cause`.
898
851
  */
899
852
  async function decodeRow(row, plan, registry, jsonValidators) {
900
- const projection = plan.meta.projection;
901
- const fallbackColumnRefIndex = !projection || Array.isArray(projection) ? buildColumnRefIndex(plan) : null;
902
- let aliases;
903
- if (projection && !Array.isArray(projection)) aliases = Object.keys(projection);
904
- else if (projection && Array.isArray(projection)) aliases = projection;
905
- else aliases = Object.keys(row);
853
+ const ctx = buildDecodeContext(plan, registry);
854
+ const aliases = ctx.aliases ?? Object.keys(row);
855
+ if (ctx.aliases !== void 0) {
856
+ for (const alias of ctx.aliases) if (!Object.hasOwn(row, alias)) throw runtimeError("RUNTIME.DECODE_FAILED", `Row missing projection alias "${alias}"`, {
857
+ alias,
858
+ expectedAliases: ctx.aliases,
859
+ presentKeys: Object.keys(row)
860
+ });
861
+ }
906
862
  const tasks = [];
907
863
  const includeIndices = [];
908
864
  for (let i = 0; i < aliases.length; i++) {
909
865
  const alias = aliases[i];
910
866
  const wireValue = row[alias];
911
- const projectionValue = projection && typeof projection === "object" && !Array.isArray(projection) ? projection[alias] : void 0;
912
- if (typeof projectionValue === "string" && projectionValue.startsWith("include:")) {
867
+ if (ctx.includeAliases.has(alias)) {
913
868
  includeIndices.push({
914
869
  index: i,
915
870
  alias,
@@ -918,7 +873,7 @@ async function decodeRow(row, plan, registry, jsonValidators) {
918
873
  tasks.push(Promise.resolve(void 0));
919
874
  continue;
920
875
  }
921
- tasks.push(decodeField(alias, wireValue, plan, registry, jsonValidators, projection, fallbackColumnRefIndex));
876
+ tasks.push(decodeField(alias, wireValue, ctx, jsonValidators));
922
877
  }
923
878
  const settled = await Promise.all(tasks);
924
879
  for (const entry of includeIndices) settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value);
@@ -929,18 +884,15 @@ async function decodeRow(row, plan, registry, jsonValidators) {
929
884
 
930
885
  //#endregion
931
886
  //#region src/codecs/encoding.ts
932
- function resolveParamCodec(paramDescriptor, registry) {
933
- if (paramDescriptor.codecId) {
934
- const codec$1 = registry.get(paramDescriptor.codecId);
935
- if (codec$1) return codec$1;
936
- }
937
- return null;
938
- }
939
- function paramLabel(paramDescriptor, paramIndex) {
940
- return paramDescriptor.name ?? `param[${paramIndex}]`;
887
+ const NO_METADATA = Object.freeze({
888
+ codecId: void 0,
889
+ name: void 0
890
+ });
891
+ function paramLabel(metadata, paramIndex) {
892
+ return metadata.name ?? `param[${paramIndex}]`;
941
893
  }
942
- function wrapEncodeFailure(error, paramDescriptor, paramIndex, codecId) {
943
- const label = paramLabel(paramDescriptor, paramIndex);
894
+ function wrapEncodeFailure(error, metadata, paramIndex, codecId) {
895
+ const label = paramLabel(metadata, paramIndex);
944
896
  const wrapped = runtimeError("RUNTIME.ENCODE_FAILED", `Failed to encode parameter ${label} with codec '${codecId}': ${error instanceof Error ? error.message : String(error)}`, {
945
897
  label,
946
898
  codec: codecId,
@@ -949,21 +901,15 @@ function wrapEncodeFailure(error, paramDescriptor, paramIndex, codecId) {
949
901
  wrapped.cause = error;
950
902
  throw wrapped;
951
903
  }
952
- /**
953
- * Encodes a single parameter through its codec. Always awaits codec.encode so
954
- * a Promise can never leak into the driver, even if a sync-authored codec is
955
- * lifted to async by the codec() factory. Failures are wrapped in
956
- * `RUNTIME.ENCODE_FAILED` with `{ label, codec, paramIndex }` and the original
957
- * error attached on `cause`.
958
- */
959
- async function encodeParam(value, paramDescriptor, paramIndex, registry) {
904
+ async function encodeParamValue(value, metadata, paramIndex, registry) {
960
905
  if (value === null || value === void 0) return null;
961
- const codec$1 = resolveParamCodec(paramDescriptor, registry);
906
+ if (!metadata.codecId) return value;
907
+ const codec$1 = registry.get(metadata.codecId);
962
908
  if (!codec$1) return value;
963
909
  try {
964
910
  return await codec$1.encode(value);
965
911
  } catch (error) {
966
- wrapEncodeFailure(error, paramDescriptor, paramIndex, codec$1.id);
912
+ wrapEncodeFailure(error, metadata, paramIndex, codec$1.id);
967
913
  }
968
914
  }
969
915
  /**
@@ -973,19 +919,20 @@ async function encodeParam(value, paramDescriptor, paramIndex, registry) {
973
919
  */
974
920
  async function encodeParams(plan, registry) {
975
921
  if (plan.params.length === 0) return plan.params;
976
- const descriptorCount = plan.meta.paramDescriptors.length;
977
922
  const paramCount = plan.params.length;
978
- const tasks = new Array(paramCount);
979
- for (let i = 0; i < paramCount; i++) {
980
- const paramValue = plan.params[i];
981
- const paramDescriptor = plan.meta.paramDescriptors[i];
982
- 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.`, {
983
- paramIndex: i,
984
- paramCount,
985
- descriptorCount
986
- });
987
- tasks[i] = encodeParam(paramValue, paramDescriptor, i, registry);
923
+ const metadata = new Array(paramCount).fill(NO_METADATA);
924
+ if (plan.ast) {
925
+ const refs = collectOrderedParamRefs(plan.ast);
926
+ for (let i = 0; i < paramCount && i < refs.length; i++) {
927
+ const ref = refs[i];
928
+ if (ref) metadata[i] = {
929
+ codecId: ref.codecId,
930
+ name: ref.name
931
+ };
932
+ }
988
933
  }
934
+ const tasks = new Array(paramCount);
935
+ for (let i = 0; i < paramCount; i++) tasks[i] = encodeParamValue(plan.params[i], metadata[i] ?? NO_METADATA, i, registry);
989
936
  const encoded = await Promise.all(tasks);
990
937
  return Object.freeze(encoded);
991
938
  }
@@ -1024,32 +971,7 @@ async function runBeforeCompileChain(middleware, initial, ctx) {
1024
971
  });
1025
972
  current = result;
1026
973
  }
1027
- if (current.ast === initial.ast) return current;
1028
- const paramDescriptors = deriveParamDescriptorsFromAst(current.ast);
1029
- const meta = {
1030
- ...current.meta,
1031
- paramDescriptors
1032
- };
1033
- return {
1034
- ast: current.ast,
1035
- meta
1036
- };
1037
- }
1038
- function deriveParamDescriptorsFromAst(ast) {
1039
- const refs = ast.collectParamRefs();
1040
- const seen = /* @__PURE__ */ new Set();
1041
- const descriptors = [];
1042
- for (const ref of refs) {
1043
- if (seen.has(ref)) continue;
1044
- seen.add(ref);
1045
- descriptors.push({
1046
- index: descriptors.length + 1,
1047
- ...ref.name !== void 0 ? { name: ref.name } : {},
1048
- source: "dsl",
1049
- ...ref.codecId !== void 0 ? { codecId: ref.codecId } : {}
1050
- });
1051
- }
1052
- return descriptors;
974
+ return current;
1053
975
  }
1054
976
 
1055
977
  //#endregion
@@ -1151,10 +1073,12 @@ var SqlRuntimeImpl = class extends RuntimeCore {
1151
1073
  }
1152
1074
  /**
1153
1075
  * SQL pre-compile hook. Runs the registered middleware `beforeCompile`
1154
- * chain over the plan's draft (AST + meta) and returns a `SqlQueryPlan`
1155
- * with the rewritten AST and meta when the chain mutates them. The chain
1156
- * re-derives `meta.paramDescriptors` from the rewritten AST so descriptors
1157
- * stay in lockstep with the params the adapter will emit during lowering.
1076
+ * chain over the plan's draft (AST + meta). Returns the original plan
1077
+ * unchanged when no middleware rewrote the AST; otherwise returns a new
1078
+ * plan carrying the rewritten AST and meta. The AST is the authoritative
1079
+ * source of execution metadata, so a rewrite needs no sidecar
1080
+ * reconciliation here — the lowering adapter and the encoder both walk
1081
+ * the rewritten AST directly.
1158
1082
  */
1159
1083
  async runBeforeCompile(plan) {
1160
1084
  const rewrittenDraft = await runBeforeCompileChain(this.middleware, {
@@ -1359,4 +1283,4 @@ function createRuntime(options) {
1359
1283
 
1360
1284
  //#endregion
1361
1285
  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 };
1362
- //# sourceMappingURL=exports-BET5HxxT.mjs.map
1286
+ //# sourceMappingURL=exports-CwCgOv6w.mjs.map