@prisma-next/sql-runtime 0.5.0-dev.1 → 0.5.0-dev.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -21
- package/dist/{exports-TJ70Qw3r.mjs → exports-BOHa3Emo.mjs} +502 -121
- package/dist/exports-BOHa3Emo.mjs.map +1 -0
- package/dist/{index-DyDQ4fyK.d.mts → index-CZmC2kD3.d.mts} +87 -23
- package/dist/index-CZmC2kD3.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/test/utils.d.mts +6 -5
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +7 -2
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +10 -12
- package/src/codecs/decoding.ts +172 -116
- package/src/codecs/encoding.ts +59 -21
- package/src/exports/index.ts +10 -7
- package/src/fingerprint.ts +22 -0
- package/src/guardrails/raw.ts +214 -0
- package/src/lower-sql-plan.ts +3 -3
- package/src/marker.ts +82 -0
- package/src/middleware/before-compile-chain.ts +59 -0
- package/src/middleware/budgets.ts +25 -33
- package/src/middleware/lints.ts +5 -5
- package/src/middleware/sql-middleware.ts +36 -6
- package/src/runtime-spi.ts +43 -0
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +1 -1
- package/src/sql-runtime.ts +279 -101
- package/dist/exports-TJ70Qw3r.mjs.map +0 -1
- package/dist/index-DyDQ4fyK.d.mts.map +0 -1
- package/test/async-iterable-result.test.ts +0 -141
- package/test/budgets.test.ts +0 -431
- package/test/context.types.test-d.ts +0 -68
- package/test/execution-stack.test.ts +0 -161
- package/test/json-schema-validation.test.ts +0 -571
- package/test/lints.test.ts +0 -159
- package/test/mutation-default-generators.test.ts +0 -254
- package/test/parameterized-types.test.ts +0 -529
- package/test/sql-context.test.ts +0 -384
- package/test/sql-family-adapter.test.ts +0 -103
- package/test/sql-runtime.test.ts +0 -634
- 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
|
|
666
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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,166 @@ function resolveParamCodec(paramDescriptor, registry) {
|
|
|
740
864
|
}
|
|
741
865
|
return null;
|
|
742
866
|
}
|
|
743
|
-
function
|
|
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
|
-
|
|
748
|
-
return codec$1.encode(value);
|
|
891
|
+
try {
|
|
892
|
+
return await codec$1.encode(value);
|
|
749
893
|
} catch (error) {
|
|
750
|
-
|
|
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
|
-
|
|
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
|
|
758
|
-
|
|
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)
|
|
762
|
-
|
|
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
|
+
|
|
983
|
+
//#endregion
|
|
984
|
+
//#region src/middleware/before-compile-chain.ts
|
|
985
|
+
async function runBeforeCompileChain(middleware, initial, ctx) {
|
|
986
|
+
let current = initial;
|
|
987
|
+
for (const mw of middleware) {
|
|
988
|
+
if (!mw.beforeCompile) continue;
|
|
989
|
+
const result = await mw.beforeCompile(current, ctx);
|
|
990
|
+
if (result === void 0) continue;
|
|
991
|
+
if (result.ast === current.ast) continue;
|
|
992
|
+
ctx.log.debug?.({
|
|
993
|
+
event: "middleware.rewrite",
|
|
994
|
+
middleware: mw.name,
|
|
995
|
+
lane: current.meta.lane
|
|
996
|
+
});
|
|
997
|
+
current = result;
|
|
998
|
+
}
|
|
999
|
+
if (current.ast === initial.ast) return current;
|
|
1000
|
+
const paramDescriptors = deriveParamDescriptorsFromAst(current.ast);
|
|
1001
|
+
const meta = {
|
|
1002
|
+
...current.meta,
|
|
1003
|
+
paramDescriptors
|
|
1004
|
+
};
|
|
1005
|
+
return {
|
|
1006
|
+
ast: current.ast,
|
|
1007
|
+
meta
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function deriveParamDescriptorsFromAst(ast) {
|
|
1011
|
+
const refs = ast.collectParamRefs();
|
|
1012
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1013
|
+
const descriptors = [];
|
|
1014
|
+
for (const ref of refs) {
|
|
1015
|
+
if (seen.has(ref)) continue;
|
|
1016
|
+
seen.add(ref);
|
|
1017
|
+
descriptors.push({
|
|
1018
|
+
index: descriptors.length + 1,
|
|
1019
|
+
...ref.name !== void 0 ? { name: ref.name } : {},
|
|
1020
|
+
source: "dsl",
|
|
1021
|
+
...ref.codecId !== void 0 ? { codecId: ref.codecId } : {}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
return descriptors;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
767
1027
|
//#endregion
|
|
768
1028
|
//#region src/sql-family-adapter.ts
|
|
769
1029
|
var SqlFamilyAdapter = class {
|
|
@@ -787,93 +1047,214 @@ var SqlFamilyAdapter = class {
|
|
|
787
1047
|
|
|
788
1048
|
//#endregion
|
|
789
1049
|
//#region src/sql-runtime.ts
|
|
790
|
-
|
|
791
|
-
|
|
1050
|
+
function isExecutionPlan(plan) {
|
|
1051
|
+
return "sql" in plan;
|
|
1052
|
+
}
|
|
1053
|
+
var SqlRuntimeImpl = class extends RuntimeCore {
|
|
792
1054
|
contract;
|
|
793
1055
|
adapter;
|
|
1056
|
+
driver;
|
|
1057
|
+
familyAdapter;
|
|
794
1058
|
codecRegistry;
|
|
795
1059
|
jsonSchemaValidators;
|
|
1060
|
+
sqlCtx;
|
|
1061
|
+
verify;
|
|
796
1062
|
codecRegistryValidated;
|
|
1063
|
+
verified;
|
|
1064
|
+
startupVerified;
|
|
1065
|
+
_telemetry;
|
|
797
1066
|
constructor(options) {
|
|
798
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
|
+
});
|
|
799
1083
|
this.contract = context.contract;
|
|
800
1084
|
this.adapter = adapter;
|
|
1085
|
+
this.driver = driver;
|
|
1086
|
+
this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
801
1087
|
this.codecRegistry = context.codecs;
|
|
802
1088
|
this.jsonSchemaValidators = context.jsonSchemaValidators;
|
|
1089
|
+
this.sqlCtx = sqlCtx;
|
|
1090
|
+
this.verify = verify;
|
|
803
1091
|
this.codecRegistryValidated = false;
|
|
804
|
-
|
|
805
|
-
this.
|
|
806
|
-
|
|
807
|
-
driver,
|
|
808
|
-
verify,
|
|
809
|
-
...ifDefined("middleware", middleware),
|
|
810
|
-
...ifDefined("mode", mode),
|
|
811
|
-
...ifDefined("log", log)
|
|
812
|
-
});
|
|
1092
|
+
this.verified = verify.mode === "startup" ? false : verify.mode === "always";
|
|
1093
|
+
this.startupVerified = false;
|
|
1094
|
+
this._telemetry = null;
|
|
813
1095
|
if (verify.mode === "startup") {
|
|
814
1096
|
validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
|
|
815
1097
|
this.codecRegistryValidated = true;
|
|
816
1098
|
}
|
|
817
1099
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
+
});
|
|
823
1111
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
+
});
|
|
829
1123
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1124
|
+
/**
|
|
1125
|
+
* SQL pre-compile hook. Runs the registered middleware `beforeCompile`
|
|
1126
|
+
* chain over the plan's draft (AST + meta) and returns a `SqlQueryPlan`
|
|
1127
|
+
* with the rewritten AST and meta when the chain mutates them. The chain
|
|
1128
|
+
* re-derives `meta.paramDescriptors` from the rewritten AST so descriptors
|
|
1129
|
+
* stay in lockstep with the params the adapter will emit during lowering.
|
|
1130
|
+
*/
|
|
1131
|
+
async runBeforeCompile(plan) {
|
|
1132
|
+
const rewrittenDraft = await runBeforeCompileChain(this.middleware, {
|
|
1133
|
+
ast: plan.ast,
|
|
1134
|
+
meta: plan.meta
|
|
1135
|
+
}, this.sqlCtx);
|
|
1136
|
+
return rewrittenDraft.ast === plan.ast ? plan : {
|
|
1137
|
+
...plan,
|
|
1138
|
+
ast: rewrittenDraft.ast,
|
|
1139
|
+
meta: rewrittenDraft.meta
|
|
841
1140
|
};
|
|
842
|
-
return new AsyncIterableResult(iterator(this));
|
|
843
1141
|
}
|
|
844
1142
|
execute(plan) {
|
|
845
|
-
return this.executeAgainstQueryable(plan, this.
|
|
1143
|
+
return this.executeAgainstQueryable(plan, this.driver);
|
|
1144
|
+
}
|
|
1145
|
+
executeAgainstQueryable(plan, queryable) {
|
|
1146
|
+
this.ensureCodecRegistryValidated();
|
|
1147
|
+
const self = this;
|
|
1148
|
+
const generator = async function* () {
|
|
1149
|
+
const exec = isExecutionPlan(plan) ? Object.freeze({
|
|
1150
|
+
...plan,
|
|
1151
|
+
params: await encodeParams(plan, self.codecRegistry)
|
|
1152
|
+
}) : await self.lower(await self.runBeforeCompile(plan));
|
|
1153
|
+
self.familyAdapter.validatePlan(exec, self.contract);
|
|
1154
|
+
self._telemetry = null;
|
|
1155
|
+
if (!self.startupVerified && self.verify.mode === "startup") await self.verifyMarker();
|
|
1156
|
+
if (!self.verified && self.verify.mode === "onFirstUse") await self.verifyMarker();
|
|
1157
|
+
const startedAt = Date.now();
|
|
1158
|
+
let outcome = null;
|
|
1159
|
+
try {
|
|
1160
|
+
if (self.verify.mode === "always") await self.verifyMarker();
|
|
1161
|
+
const stream = runWithMiddleware(exec, self.middleware, self.ctx, () => queryable.execute({
|
|
1162
|
+
sql: exec.sql,
|
|
1163
|
+
params: exec.params
|
|
1164
|
+
}));
|
|
1165
|
+
for await (const rawRow of stream) yield await decodeRow(rawRow, exec, self.codecRegistry, self.jsonSchemaValidators);
|
|
1166
|
+
outcome = "success";
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
outcome = "runtime-error";
|
|
1169
|
+
throw error;
|
|
1170
|
+
} finally {
|
|
1171
|
+
if (outcome !== null) self.recordTelemetry(exec, outcome, Date.now() - startedAt);
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
return new AsyncIterableResult(generator());
|
|
846
1175
|
}
|
|
847
1176
|
async connection() {
|
|
848
|
-
const
|
|
1177
|
+
const driverConn = await this.driver.acquireConnection();
|
|
849
1178
|
const self = this;
|
|
850
1179
|
return {
|
|
851
1180
|
async transaction() {
|
|
852
|
-
const
|
|
853
|
-
return
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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);
|
|
860
1189
|
},
|
|
861
|
-
release: coreConn.release.bind(coreConn),
|
|
862
|
-
destroy: coreConn.destroy.bind(coreConn),
|
|
863
1190
|
execute(plan) {
|
|
864
|
-
return self.executeAgainstQueryable(plan,
|
|
1191
|
+
return self.executeAgainstQueryable(plan, driverConn);
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
wrapTransaction(driverTx) {
|
|
1196
|
+
const self = this;
|
|
1197
|
+
return {
|
|
1198
|
+
async commit() {
|
|
1199
|
+
await driverTx.commit();
|
|
1200
|
+
},
|
|
1201
|
+
async rollback() {
|
|
1202
|
+
await driverTx.rollback();
|
|
1203
|
+
},
|
|
1204
|
+
execute(plan) {
|
|
1205
|
+
return self.executeAgainstQueryable(plan, driverTx);
|
|
865
1206
|
}
|
|
866
1207
|
};
|
|
867
1208
|
}
|
|
868
1209
|
telemetry() {
|
|
869
|
-
return this.
|
|
1210
|
+
return this._telemetry;
|
|
1211
|
+
}
|
|
1212
|
+
async close() {
|
|
1213
|
+
await this.driver.close();
|
|
870
1214
|
}
|
|
871
|
-
|
|
872
|
-
|
|
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
|
+
});
|
|
873
1254
|
}
|
|
874
1255
|
};
|
|
875
1256
|
function transactionClosedError() {
|
|
876
|
-
return runtimeError
|
|
1257
|
+
return runtimeError("RUNTIME.TRANSACTION_CLOSED", "Cannot read from a query result after the transaction has ended. Await the result or call .toArray() inside the transaction callback.", {});
|
|
877
1258
|
}
|
|
878
1259
|
async function withTransaction(runtime, fn) {
|
|
879
1260
|
const connection = await runtime.connection();
|
|
@@ -910,7 +1291,7 @@ async function withTransaction(runtime, fn) {
|
|
|
910
1291
|
await transaction.rollback();
|
|
911
1292
|
} catch (rollbackError) {
|
|
912
1293
|
await destroyConnection(rollbackError);
|
|
913
|
-
const wrapped = runtimeError
|
|
1294
|
+
const wrapped = runtimeError("RUNTIME.TRANSACTION_ROLLBACK_FAILED", "Transaction rollback failed after callback error", { rollbackError });
|
|
914
1295
|
wrapped.cause = error;
|
|
915
1296
|
throw wrapped;
|
|
916
1297
|
}
|
|
@@ -926,7 +1307,7 @@ async function withTransaction(runtime, fn) {
|
|
|
926
1307
|
} catch {
|
|
927
1308
|
await destroyConnection(commitError);
|
|
928
1309
|
}
|
|
929
|
-
const wrapped = runtimeError
|
|
1310
|
+
const wrapped = runtimeError("RUNTIME.TRANSACTION_COMMIT_FAILED", "Transaction commit failed", { commitError });
|
|
930
1311
|
wrapped.cause = commitError;
|
|
931
1312
|
throw wrapped;
|
|
932
1313
|
}
|
|
@@ -950,4 +1331,4 @@ function createRuntime(options) {
|
|
|
950
1331
|
|
|
951
1332
|
//#endregion
|
|
952
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 };
|
|
953
|
-
//# sourceMappingURL=exports-
|
|
1334
|
+
//# sourceMappingURL=exports-BOHa3Emo.mjs.map
|