@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.
- package/README.md +29 -21
- package/dist/{exports-BQZSVXXt.mjs → exports-BOHa3Emo.mjs} +481 -128
- package/dist/exports-BOHa3Emo.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-CZmC2kD3.d.mts} +53 -16
- package/dist/index-CZmC2kD3.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/test/utils.d.mts +6 -5
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +7 -2
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +12 -14
- 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 +32 -1
- package/src/middleware/budgets.ts +14 -11
- package/src/middleware/lints.ts +3 -3
- package/src/middleware/sql-middleware.ts +6 -5
- package/src/runtime-spi.ts +43 -0
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +1 -1
- package/src/sql-runtime.ts +272 -110
- package/dist/exports-BQZSVXXt.mjs.map +0 -1
- package/dist/index-yb51L_1h.d.mts.map +0 -1
- package/test/async-iterable-result.test.ts +0 -141
- package/test/before-compile-chain.test.ts +0 -223
- 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 -160
- 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 -792
- package/test/utils.ts +0 -297
package/package.json
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-runtime",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "SQL runtime implementation for Prisma Next",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"arktype": "^2.1.26",
|
|
9
|
-
"@prisma-next/
|
|
10
|
-
"@prisma-next/
|
|
11
|
-
"@prisma-next/framework-components": "0.5.0-dev.
|
|
12
|
-
"@prisma-next/
|
|
13
|
-
"@prisma-next/
|
|
14
|
-
"@prisma-next/sql-contract": "0.5.0-dev.
|
|
15
|
-
"@prisma-next/sql-operations": "0.5.0-dev.
|
|
16
|
-
"@prisma-next/
|
|
17
|
-
"@prisma-next/sql-relational-core": "0.5.0-dev.7"
|
|
9
|
+
"@prisma-next/utils": "0.5.0-dev.9",
|
|
10
|
+
"@prisma-next/contract": "0.5.0-dev.9",
|
|
11
|
+
"@prisma-next/framework-components": "0.5.0-dev.9",
|
|
12
|
+
"@prisma-next/ids": "0.5.0-dev.9",
|
|
13
|
+
"@prisma-next/operations": "0.5.0-dev.9",
|
|
14
|
+
"@prisma-next/sql-contract": "0.5.0-dev.9",
|
|
15
|
+
"@prisma-next/sql-operations": "0.5.0-dev.9",
|
|
16
|
+
"@prisma-next/sql-relational-core": "0.5.0-dev.9"
|
|
18
17
|
},
|
|
19
18
|
"devDependencies": {
|
|
20
19
|
"@types/pg": "8.16.0",
|
|
@@ -22,14 +21,13 @@
|
|
|
22
21
|
"tsdown": "0.18.4",
|
|
23
22
|
"typescript": "5.9.3",
|
|
24
23
|
"vitest": "4.0.17",
|
|
25
|
-
"@prisma-next/test-utils": "0.0.1",
|
|
26
24
|
"@prisma-next/tsconfig": "0.0.0",
|
|
27
|
-
"@prisma-next/tsdown": "0.0.0"
|
|
25
|
+
"@prisma-next/tsdown": "0.0.0",
|
|
26
|
+
"@prisma-next/test-utils": "0.0.1"
|
|
28
27
|
},
|
|
29
28
|
"files": [
|
|
30
29
|
"dist",
|
|
31
|
-
"src"
|
|
32
|
-
"test"
|
|
30
|
+
"src"
|
|
33
31
|
],
|
|
34
32
|
"exports": {
|
|
35
33
|
".": "./dist/index.mjs",
|
package/src/codecs/decoding.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isRuntimeError, runtimeError } from '@prisma-next/framework-components/runtime';
|
|
2
2
|
import type { Codec, CodecRegistry } from '@prisma-next/sql-relational-core/ast';
|
|
3
|
+
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
3
4
|
import type { JsonSchemaValidatorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
|
|
4
5
|
import { validateJsonValue } from './json-schema-validation';
|
|
5
6
|
|
|
7
|
+
type ColumnRef = { table: string; column: string };
|
|
8
|
+
type ColumnRefIndex = Map<string, ColumnRef>;
|
|
9
|
+
|
|
10
|
+
const WIRE_PREVIEW_LIMIT = 100;
|
|
11
|
+
|
|
6
12
|
function resolveRowCodec(
|
|
7
13
|
alias: string,
|
|
8
|
-
plan:
|
|
14
|
+
plan: SqlExecutionPlan,
|
|
9
15
|
registry: CodecRegistry,
|
|
10
16
|
): Codec | null {
|
|
11
17
|
const planCodecId = plan.meta.annotations?.codecs?.[alias] as string | undefined;
|
|
@@ -29,13 +35,11 @@ function resolveRowCodec(
|
|
|
29
35
|
return null;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
type ColumnRefIndex = Map<string, { table: string; column: string }>;
|
|
33
|
-
|
|
34
38
|
/**
|
|
35
39
|
* Builds a lookup index from column name → { table, column } ref.
|
|
36
40
|
* Called once per decodeRow invocation to avoid O(aliases × refs) linear scans.
|
|
37
41
|
*/
|
|
38
|
-
function buildColumnRefIndex(plan:
|
|
42
|
+
function buildColumnRefIndex(plan: SqlExecutionPlan): ColumnRefIndex | null {
|
|
39
43
|
const columns = plan.meta.refs?.columns;
|
|
40
44
|
if (!columns) return null;
|
|
41
45
|
|
|
@@ -46,7 +50,7 @@ function buildColumnRefIndex(plan: ExecutionPlan): ColumnRefIndex | null {
|
|
|
46
50
|
return index;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
function parseProjectionRef(value: string):
|
|
53
|
+
function parseProjectionRef(value: string): ColumnRef | null {
|
|
50
54
|
if (value.startsWith('include:') || value.startsWith('operation:')) {
|
|
51
55
|
return null;
|
|
52
56
|
}
|
|
@@ -64,9 +68,9 @@ function parseProjectionRef(value: string): { table: string; column: string } |
|
|
|
64
68
|
|
|
65
69
|
function resolveColumnRefForAlias(
|
|
66
70
|
alias: string,
|
|
67
|
-
projection:
|
|
71
|
+
projection: SqlExecutionPlan['meta']['projection'],
|
|
68
72
|
fallbackColumnRefIndex: ColumnRefIndex | null,
|
|
69
|
-
):
|
|
73
|
+
): ColumnRef | undefined {
|
|
70
74
|
if (projection && !Array.isArray(projection)) {
|
|
71
75
|
const mappedRef = (projection as Record<string, string>)[alias];
|
|
72
76
|
if (typeof mappedRef !== 'string') {
|
|
@@ -78,18 +82,144 @@ function resolveColumnRefForAlias(
|
|
|
78
82
|
return fallbackColumnRefIndex?.get(alias);
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
function previewWireValue(wireValue: unknown): string {
|
|
86
|
+
if (typeof wireValue === 'string') {
|
|
87
|
+
return wireValue.length > WIRE_PREVIEW_LIMIT
|
|
88
|
+
? `${wireValue.substring(0, WIRE_PREVIEW_LIMIT)}...`
|
|
89
|
+
: wireValue;
|
|
90
|
+
}
|
|
91
|
+
return String(wireValue).substring(0, WIRE_PREVIEW_LIMIT);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isJsonSchemaValidationError(error: unknown): boolean {
|
|
95
|
+
return isRuntimeError(error) && error.code === 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function wrapDecodeFailure(
|
|
99
|
+
error: unknown,
|
|
100
|
+
alias: string,
|
|
101
|
+
ref: ColumnRef | undefined,
|
|
102
|
+
codec: Codec,
|
|
103
|
+
wireValue: unknown,
|
|
104
|
+
): never {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
const target = ref ? `${ref.table}.${ref.column}` : alias;
|
|
107
|
+
const wrapped = runtimeError(
|
|
108
|
+
'RUNTIME.DECODE_FAILED',
|
|
109
|
+
`Failed to decode column ${target} with codec '${codec.id}': ${message}`,
|
|
110
|
+
{
|
|
111
|
+
...(ref ? { table: ref.table, column: ref.column } : { alias }),
|
|
112
|
+
codec: codec.id,
|
|
113
|
+
wirePreview: previewWireValue(wireValue),
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
wrapped.cause = error;
|
|
117
|
+
throw wrapped;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function wrapIncludeAggregateFailure(error: unknown, alias: string, wireValue: unknown): never {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
const wrapped = runtimeError(
|
|
123
|
+
'RUNTIME.DECODE_FAILED',
|
|
124
|
+
`Failed to parse JSON array for include alias '${alias}': ${message}`,
|
|
125
|
+
{
|
|
126
|
+
alias,
|
|
127
|
+
wirePreview: previewWireValue(wireValue),
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
wrapped.cause = error;
|
|
131
|
+
throw wrapped;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function decodeIncludeAggregate(alias: string, wireValue: unknown): unknown {
|
|
135
|
+
if (wireValue === null || wireValue === undefined) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
let parsed: unknown;
|
|
141
|
+
if (typeof wireValue === 'string') {
|
|
142
|
+
parsed = JSON.parse(wireValue);
|
|
143
|
+
} else if (Array.isArray(wireValue)) {
|
|
144
|
+
parsed = wireValue;
|
|
145
|
+
} else {
|
|
146
|
+
parsed = JSON.parse(String(wireValue));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!Array.isArray(parsed)) {
|
|
150
|
+
throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return parsed;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
wrapIncludeAggregateFailure(error, alias, wireValue);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Decodes a single field. Single-armed: every cell takes the same path —
|
|
161
|
+
* `codec.decode → await → JSON-Schema validate → return plain value` — so
|
|
162
|
+
* sync- and async-authored codecs are indistinguishable to callers.
|
|
163
|
+
*/
|
|
164
|
+
async function decodeField(
|
|
165
|
+
alias: string,
|
|
166
|
+
wireValue: unknown,
|
|
167
|
+
plan: SqlExecutionPlan,
|
|
168
|
+
registry: CodecRegistry,
|
|
169
|
+
jsonValidators: JsonSchemaValidatorRegistry | undefined,
|
|
170
|
+
projection: SqlExecutionPlan['meta']['projection'],
|
|
171
|
+
fallbackColumnRefIndex: ColumnRefIndex | null,
|
|
172
|
+
): Promise<unknown> {
|
|
173
|
+
if (wireValue === null || wireValue === undefined) {
|
|
174
|
+
return wireValue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const codec = resolveRowCodec(alias, plan, registry);
|
|
178
|
+
if (!codec) {
|
|
179
|
+
return wireValue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ref = resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex);
|
|
183
|
+
|
|
184
|
+
let decoded: unknown;
|
|
185
|
+
try {
|
|
186
|
+
decoded = await codec.decode(wireValue);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
wrapDecodeFailure(error, alias, ref, codec, wireValue);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (jsonValidators && ref) {
|
|
192
|
+
try {
|
|
193
|
+
validateJsonValue(jsonValidators, ref.table, ref.column, decoded, 'decode', codec.id);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (isJsonSchemaValidationError(error)) throw error;
|
|
196
|
+
wrapDecodeFailure(error, alias, ref, codec, wireValue);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return decoded;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Decodes a row by dispatching all per-cell codec calls concurrently via
|
|
205
|
+
* `Promise.all`. Each cell follows the single-armed `decodeField` path.
|
|
206
|
+
* Failures are wrapped in `RUNTIME.DECODE_FAILED` with `{ table, column,
|
|
207
|
+
* codec }` (or `{ alias, codec }` when no column ref is resolvable) and the
|
|
208
|
+
* original error attached on `cause`.
|
|
209
|
+
*/
|
|
210
|
+
export async function decodeRow(
|
|
82
211
|
row: Record<string, unknown>,
|
|
83
|
-
plan:
|
|
212
|
+
plan: SqlExecutionPlan,
|
|
84
213
|
registry: CodecRegistry,
|
|
85
214
|
jsonValidators?: JsonSchemaValidatorRegistry,
|
|
86
|
-
): Record<string, unknown
|
|
87
|
-
const decoded: Record<string, unknown> = {};
|
|
215
|
+
): Promise<Record<string, unknown>> {
|
|
88
216
|
const projection = plan.meta.projection;
|
|
89
217
|
|
|
90
|
-
//
|
|
218
|
+
// Build a column-ref index when the projection alias-to-ref mapping is
|
|
219
|
+
// unavailable so that decode failures and JSON-Schema validation can both
|
|
220
|
+
// surface { table, column } from `meta.refs.columns` when present.
|
|
91
221
|
const fallbackColumnRefIndex =
|
|
92
|
-
|
|
222
|
+
!projection || Array.isArray(projection) ? buildColumnRefIndex(plan) : null;
|
|
93
223
|
|
|
94
224
|
let aliases: readonly string[];
|
|
95
225
|
if (projection && !Array.isArray(projection)) {
|
|
@@ -100,7 +230,11 @@ export function decodeRow(
|
|
|
100
230
|
aliases = Object.keys(row);
|
|
101
231
|
}
|
|
102
232
|
|
|
103
|
-
|
|
233
|
+
const tasks: Promise<unknown>[] = [];
|
|
234
|
+
const includeIndices: { index: number; alias: string; value: unknown }[] = [];
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < aliases.length; i++) {
|
|
237
|
+
const alias = aliases[i] as string;
|
|
104
238
|
const wireValue = row[alias];
|
|
105
239
|
|
|
106
240
|
const projectionValue =
|
|
@@ -109,113 +243,35 @@ export function decodeRow(
|
|
|
109
243
|
: undefined;
|
|
110
244
|
|
|
111
245
|
if (typeof projectionValue === 'string' && projectionValue.startsWith('include:')) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
let parsed: unknown;
|
|
119
|
-
if (typeof wireValue === 'string') {
|
|
120
|
-
parsed = JSON.parse(wireValue);
|
|
121
|
-
} else if (Array.isArray(wireValue)) {
|
|
122
|
-
parsed = wireValue;
|
|
123
|
-
} else {
|
|
124
|
-
parsed = JSON.parse(String(wireValue));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!Array.isArray(parsed)) {
|
|
128
|
-
throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
decoded[alias] = parsed;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
const decodeError = new Error(
|
|
134
|
-
`Failed to parse JSON array for include alias '${alias}': ${error instanceof Error ? error.message : String(error)}`,
|
|
135
|
-
) as Error & {
|
|
136
|
-
code: string;
|
|
137
|
-
category: string;
|
|
138
|
-
severity: string;
|
|
139
|
-
details?: Record<string, unknown>;
|
|
140
|
-
};
|
|
141
|
-
decodeError.code = 'RUNTIME.DECODE_FAILED';
|
|
142
|
-
decodeError.category = 'RUNTIME';
|
|
143
|
-
decodeError.severity = 'error';
|
|
144
|
-
decodeError.details = {
|
|
145
|
-
alias,
|
|
146
|
-
wirePreview:
|
|
147
|
-
typeof wireValue === 'string' && wireValue.length > 100
|
|
148
|
-
? `${wireValue.substring(0, 100)}...`
|
|
149
|
-
: String(wireValue).substring(0, 100),
|
|
150
|
-
};
|
|
151
|
-
throw decodeError;
|
|
152
|
-
}
|
|
246
|
+
includeIndices.push({ index: i, alias, value: wireValue });
|
|
247
|
+
tasks.push(Promise.resolve(undefined));
|
|
153
248
|
continue;
|
|
154
249
|
}
|
|
155
250
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
const decodedValue = codec.decode(wireValue);
|
|
170
|
-
|
|
171
|
-
// Validate decoded JSON value against schema
|
|
172
|
-
if (jsonValidators) {
|
|
173
|
-
const ref = resolveColumnRefForAlias(alias, projection, fallbackColumnRefIndex);
|
|
174
|
-
if (ref) {
|
|
175
|
-
validateJsonValue(
|
|
176
|
-
jsonValidators,
|
|
177
|
-
ref.table,
|
|
178
|
-
ref.column,
|
|
179
|
-
decodedValue,
|
|
180
|
-
'decode',
|
|
181
|
-
codec.id,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
251
|
+
tasks.push(
|
|
252
|
+
decodeField(
|
|
253
|
+
alias,
|
|
254
|
+
wireValue,
|
|
255
|
+
plan,
|
|
256
|
+
registry,
|
|
257
|
+
jsonValidators,
|
|
258
|
+
projection,
|
|
259
|
+
fallbackColumnRefIndex,
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
185
263
|
|
|
186
|
-
|
|
187
|
-
} catch (error) {
|
|
188
|
-
// Re-throw JSON schema validation errors as-is
|
|
189
|
-
if (
|
|
190
|
-
error instanceof Error &&
|
|
191
|
-
'code' in error &&
|
|
192
|
-
(error as Error & { code: string }).code === 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED'
|
|
193
|
-
) {
|
|
194
|
-
throw error;
|
|
195
|
-
}
|
|
264
|
+
const settled = await Promise.all(tasks);
|
|
196
265
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
category: string;
|
|
202
|
-
severity: string;
|
|
203
|
-
details?: Record<string, unknown>;
|
|
204
|
-
};
|
|
205
|
-
decodeError.code = 'RUNTIME.DECODE_FAILED';
|
|
206
|
-
decodeError.category = 'RUNTIME';
|
|
207
|
-
decodeError.severity = 'error';
|
|
208
|
-
decodeError.details = {
|
|
209
|
-
alias,
|
|
210
|
-
codec: codec.id,
|
|
211
|
-
wirePreview:
|
|
212
|
-
typeof wireValue === 'string' && wireValue.length > 100
|
|
213
|
-
? `${wireValue.substring(0, 100)}...`
|
|
214
|
-
: String(wireValue).substring(0, 100),
|
|
215
|
-
};
|
|
216
|
-
throw decodeError;
|
|
217
|
-
}
|
|
266
|
+
// Include aggregates are decoded synchronously after concurrent codec
|
|
267
|
+
// dispatch settles, so any decode failures upstream propagate first.
|
|
268
|
+
for (const entry of includeIndices) {
|
|
269
|
+
settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value);
|
|
218
270
|
}
|
|
219
271
|
|
|
272
|
+
const decoded: Record<string, unknown> = {};
|
|
273
|
+
for (let i = 0; i < aliases.length; i++) {
|
|
274
|
+
decoded[aliases[i] as string] = settled[i];
|
|
275
|
+
}
|
|
220
276
|
return decoded;
|
|
221
277
|
}
|
package/src/codecs/encoding.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ParamDescriptor } from '@prisma-next/contract/types';
|
|
2
|
+
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
2
3
|
import type { Codec, CodecRegistry } from '@prisma-next/sql-relational-core/ast';
|
|
4
|
+
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
3
5
|
|
|
4
6
|
function resolveParamCodec(
|
|
5
7
|
paramDescriptor: ParamDescriptor,
|
|
@@ -15,12 +17,40 @@ function resolveParamCodec(
|
|
|
15
17
|
return null;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
function paramLabel(paramDescriptor: ParamDescriptor, paramIndex: number): string {
|
|
21
|
+
return paramDescriptor.name ?? `param[${paramIndex}]`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function wrapEncodeFailure(
|
|
25
|
+
error: unknown,
|
|
26
|
+
paramDescriptor: ParamDescriptor,
|
|
27
|
+
paramIndex: number,
|
|
28
|
+
codecId: string,
|
|
29
|
+
): never {
|
|
30
|
+
const label = paramLabel(paramDescriptor, paramIndex);
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
const wrapped = runtimeError(
|
|
33
|
+
'RUNTIME.ENCODE_FAILED',
|
|
34
|
+
`Failed to encode parameter ${label} with codec '${codecId}': ${message}`,
|
|
35
|
+
{ label, codec: codecId, paramIndex },
|
|
36
|
+
);
|
|
37
|
+
wrapped.cause = error;
|
|
38
|
+
throw wrapped;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Encodes a single parameter through its codec. Always awaits codec.encode so
|
|
43
|
+
* a Promise can never leak into the driver, even if a sync-authored codec is
|
|
44
|
+
* lifted to async by the codec() factory. Failures are wrapped in
|
|
45
|
+
* `RUNTIME.ENCODE_FAILED` with `{ label, codec, paramIndex }` and the original
|
|
46
|
+
* error attached on `cause`.
|
|
47
|
+
*/
|
|
48
|
+
export async function encodeParam(
|
|
19
49
|
value: unknown,
|
|
20
50
|
paramDescriptor: ParamDescriptor,
|
|
21
51
|
paramIndex: number,
|
|
22
52
|
registry: CodecRegistry,
|
|
23
|
-
): unknown {
|
|
53
|
+
): Promise<unknown> {
|
|
24
54
|
if (value === null || value === undefined) {
|
|
25
55
|
return null;
|
|
26
56
|
}
|
|
@@ -30,37 +60,45 @@ export function encodeParam(
|
|
|
30
60
|
return value;
|
|
31
61
|
}
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const label = paramDescriptor.name ?? `param[${paramIndex}]`;
|
|
38
|
-
throw new Error(
|
|
39
|
-
`Failed to encode parameter ${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
63
|
+
try {
|
|
64
|
+
return await codec.encode(value);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
wrapEncodeFailure(error, paramDescriptor, paramIndex, codec.id);
|
|
42
67
|
}
|
|
43
|
-
|
|
44
|
-
return value;
|
|
45
68
|
}
|
|
46
69
|
|
|
47
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Encodes all parameters concurrently via `Promise.all`. Per parameter, sync-
|
|
72
|
+
* and async-authored codecs share the same path: `codec.encode → await →
|
|
73
|
+
* return`. Param-level failures are wrapped in `RUNTIME.ENCODE_FAILED`.
|
|
74
|
+
*/
|
|
75
|
+
export async function encodeParams(
|
|
76
|
+
plan: SqlExecutionPlan,
|
|
77
|
+
registry: CodecRegistry,
|
|
78
|
+
): Promise<readonly unknown[]> {
|
|
48
79
|
if (plan.params.length === 0) {
|
|
49
80
|
return plan.params;
|
|
50
81
|
}
|
|
51
82
|
|
|
52
|
-
const
|
|
83
|
+
const descriptorCount = plan.meta.paramDescriptors.length;
|
|
84
|
+
const paramCount = plan.params.length;
|
|
53
85
|
|
|
54
|
-
|
|
86
|
+
const tasks: Promise<unknown>[] = new Array(paramCount);
|
|
87
|
+
for (let i = 0; i < paramCount; i++) {
|
|
55
88
|
const paramValue = plan.params[i];
|
|
56
89
|
const paramDescriptor = plan.meta.paramDescriptors[i];
|
|
57
90
|
|
|
58
|
-
if (paramDescriptor) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
if (!paramDescriptor) {
|
|
92
|
+
throw runtimeError(
|
|
93
|
+
'RUNTIME.MISSING_PARAM_DESCRIPTOR',
|
|
94
|
+
`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.`,
|
|
95
|
+
{ paramIndex: i, paramCount, descriptorCount },
|
|
96
|
+
);
|
|
62
97
|
}
|
|
98
|
+
|
|
99
|
+
tasks[i] = encodeParam(paramValue, paramDescriptor, i, registry);
|
|
63
100
|
}
|
|
64
101
|
|
|
102
|
+
const encoded = await Promise.all(tasks);
|
|
65
103
|
return Object.freeze(encoded);
|
|
66
104
|
}
|
package/src/exports/index.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
export type {
|
|
2
2
|
AfterExecuteResult,
|
|
3
|
-
Log,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '@prisma-next/runtime-executor';
|
|
3
|
+
RuntimeLog as Log,
|
|
4
|
+
} from '@prisma-next/framework-components/runtime';
|
|
5
|
+
export type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
|
|
7
6
|
export {
|
|
8
7
|
extractCodecIds,
|
|
9
8
|
validateCodecRegistryCompleteness,
|
|
@@ -15,6 +14,13 @@ export { budgets } from '../middleware/budgets';
|
|
|
15
14
|
export type { LintsOptions } from '../middleware/lints';
|
|
16
15
|
export { lints } from '../middleware/lints';
|
|
17
16
|
export type { SqlMiddleware, SqlMiddlewareContext } from '../middleware/sql-middleware';
|
|
17
|
+
export type {
|
|
18
|
+
MarkerReader,
|
|
19
|
+
RuntimeFamilyAdapter,
|
|
20
|
+
RuntimeTelemetryEvent,
|
|
21
|
+
RuntimeVerifyOptions,
|
|
22
|
+
TelemetryOutcome,
|
|
23
|
+
} from '../runtime-spi';
|
|
18
24
|
export type {
|
|
19
25
|
ExecutionContext,
|
|
20
26
|
RuntimeMutationDefaultGenerator,
|
|
@@ -46,10 +52,7 @@ export type {
|
|
|
46
52
|
Runtime,
|
|
47
53
|
RuntimeConnection,
|
|
48
54
|
RuntimeQueryable,
|
|
49
|
-
RuntimeTelemetryEvent,
|
|
50
55
|
RuntimeTransaction,
|
|
51
|
-
RuntimeVerifyOptions,
|
|
52
|
-
TelemetryOutcome,
|
|
53
56
|
TransactionContext,
|
|
54
57
|
} from '../sql-runtime';
|
|
55
58
|
export { createRuntime, withTransaction } from '../sql-runtime';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const STRING_LITERAL_REGEX = /'(?:''|[^'])*'/g;
|
|
4
|
+
const NUMERIC_LITERAL_REGEX = /\b\d+(?:\.\d+)?\b/g;
|
|
5
|
+
const WHITESPACE_REGEX = /\s+/g;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Computes a literal-stripped, normalized fingerprint of a SQL statement.
|
|
9
|
+
*
|
|
10
|
+
* The function strips string and numeric literals, collapses whitespace, and
|
|
11
|
+
* lowercases the result before hashing — so two structurally equivalent
|
|
12
|
+
* statements (with different parameter values) produce the same fingerprint.
|
|
13
|
+
* Used by SQL telemetry to group queries.
|
|
14
|
+
*/
|
|
15
|
+
export function computeSqlFingerprint(sql: string): string {
|
|
16
|
+
const withoutStrings = sql.replace(STRING_LITERAL_REGEX, '?');
|
|
17
|
+
const withoutNumbers = withoutStrings.replace(NUMERIC_LITERAL_REGEX, '?');
|
|
18
|
+
const normalized = withoutNumbers.replace(WHITESPACE_REGEX, ' ').trim().toLowerCase();
|
|
19
|
+
|
|
20
|
+
const hash = createHash('sha256').update(normalized).digest('hex');
|
|
21
|
+
return `sha256:${hash}`;
|
|
22
|
+
}
|