@prisma-next/sql-runtime 0.5.0-dev.60 → 0.5.0-dev.61
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/dist/{exports-BcX9wp4z.mjs → exports-CZIUsCRE.mjs} +197 -320
- package/dist/exports-CZIUsCRE.mjs.map +1 -0
- package/dist/{index-DkthtnOX.d.mts → index-Ba3eysL3.d.mts} +19 -44
- package/dist/index-Ba3eysL3.d.mts.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/test/utils.d.mts +31 -26
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +99 -55
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +11 -11
- package/src/codecs/alias-resolver.ts +34 -0
- package/src/codecs/decoding.ts +49 -83
- package/src/codecs/encoding.ts +44 -56
- package/src/codecs/validation.ts +3 -22
- package/src/middleware/budgets.ts +13 -27
- package/src/sql-context.ts +119 -268
- package/src/sql-runtime.ts +39 -101
- package/dist/exports-BcX9wp4z.mjs.map +0 -1
- package/dist/index-DkthtnOX.d.mts.map +0 -1
- package/src/codecs/json-schema-validation.ts +0 -61
package/src/codecs/decoding.ts
CHANGED
|
@@ -7,14 +7,12 @@ import {
|
|
|
7
7
|
import type {
|
|
8
8
|
AnyQueryAst,
|
|
9
9
|
Codec,
|
|
10
|
-
CodecRegistry,
|
|
11
10
|
ContractCodecRegistry,
|
|
12
11
|
ProjectionItem,
|
|
13
12
|
SqlCodecCallContext,
|
|
14
13
|
} from '@prisma-next/sql-relational-core/ast';
|
|
15
14
|
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
16
|
-
import
|
|
17
|
-
import { validateJsonValue } from './json-schema-validation';
|
|
15
|
+
import { makeAliasResolver } from './alias-resolver';
|
|
18
16
|
|
|
19
17
|
type ColumnRef = { table: string; column: string };
|
|
20
18
|
|
|
@@ -44,40 +42,39 @@ function projectionListFromAst(ast: AnyQueryAst): ReadonlyArray<ProjectionItem>
|
|
|
44
42
|
/**
|
|
45
43
|
* Resolve the per-cell codec for a projection item.
|
|
46
44
|
*
|
|
47
|
-
*
|
|
48
|
-
* prefer `contractCodecs.forColumn(table, column)` — that's the per-
|
|
49
|
-
* instance resolved codec materialized from the codec descriptor's
|
|
50
|
-
* factory at context-construction time (carries any per-instance state
|
|
51
|
-
* such as the compiled JSON-Schema validator). When the projection
|
|
52
|
-
* resolves to a non-`column-ref` expression (computed projections, raw
|
|
53
|
-
* SQL aliases) but still carries a codec id (ADR 205 stamps every
|
|
54
|
-
* `ProjectionItem` with the producer's codec id), fall back to the
|
|
55
|
-
* codec-id-keyed `forCodecId(codecId)` lookup, which itself falls back
|
|
56
|
-
* to the legacy `CodecRegistry` for codec ids the contract walk
|
|
57
|
-
* couldn't resolve.
|
|
45
|
+
* When a `(table, column)` ref is available — either implicit on a `column-ref` expression or carried explicitly via `item.refs` for column-bound non-`column-ref` projections — prefer `contractCodecs.forColumn(table, column)`: that returns the per-instance codec materialized from the descriptor's factory for that column, encoding any per-instance state (typeParams like vector length, schema validators, etc.).
|
|
58
46
|
*
|
|
59
|
-
*
|
|
47
|
+
* The wrong-instance risk for parameterized codecs is closed off structurally:
|
|
48
|
+
*
|
|
49
|
+
* 1. `buildContractCodecRegistry` pre-populates `byCodecId` with one canonical instance per non-parameterized descriptor; parameterized descriptors are intentionally absent. 2. `forCodecId` rejects ambiguous parameterized fallbacks (`ambiguousCodecIds`). 3. The non-ambiguous parameterized case stores the column-correct per-instance codec under `byCodecId`, so the fall-through still resolves to the right instance.
|
|
50
|
+
*
|
|
51
|
+
* The `forCodecId` fallback otherwise covers projections that are *not* column-bound (computed projections, raw SQL aliases) but still carry a `codecId` (ADR 205 stamps every `ProjectionItem` with the producer's codec id).
|
|
52
|
+
*
|
|
53
|
+
* Codec-registry-unification spec § AC-4 / AC-5.
|
|
60
54
|
*/
|
|
61
55
|
function resolveProjectionCodec(
|
|
62
56
|
item: ProjectionItem,
|
|
63
|
-
registry: CodecRegistry,
|
|
64
57
|
contractCodecs: ContractCodecRegistry | undefined,
|
|
58
|
+
aliasResolver: (alias: string) => string,
|
|
65
59
|
): Codec | undefined {
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
if (contractCodecs) {
|
|
61
|
+
if (item.expr.kind === 'column-ref') {
|
|
62
|
+
const byColumn = contractCodecs.forColumn(aliasResolver(item.expr.table), item.expr.column);
|
|
63
|
+
// Only honour `byColumn` when its codec id agrees with `item.codecId`. They can legitimately disagree when an `OperationExpr`-shaped projection carries a single inner column-ref but transforms the value's codec (e.g. `cosineDistance(col, x)` projects `pg/float8@1` while the inner column-ref points at a `pg/vector@1` column).
|
|
64
|
+
if (byColumn && (item.codecId === undefined || byColumn.id === item.codecId)) return byColumn;
|
|
65
|
+
} else if (item.refs) {
|
|
66
|
+
const byColumn = contractCodecs.forColumn(aliasResolver(item.refs.table), item.refs.column);
|
|
67
|
+
if (byColumn && (item.codecId === undefined || byColumn.id === item.codecId)) return byColumn;
|
|
68
|
+
}
|
|
69
69
|
}
|
|
70
70
|
if (item.codecId) {
|
|
71
|
-
|
|
72
|
-
if (fromContract) return fromContract;
|
|
73
|
-
return registry.get(item.codecId);
|
|
71
|
+
return contractCodecs?.forCodecId(item.codecId);
|
|
74
72
|
}
|
|
75
73
|
return undefined;
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
function buildDecodeContext(
|
|
79
77
|
plan: SqlExecutionPlan,
|
|
80
|
-
registry: CodecRegistry,
|
|
81
78
|
contractCodecs: ContractCodecRegistry | undefined,
|
|
82
79
|
): DecodeContext {
|
|
83
80
|
if (!isAstBackedPlan(plan)) {
|
|
@@ -103,17 +100,26 @@ function buildDecodeContext(
|
|
|
103
100
|
const codecs = new Map<string, Codec>();
|
|
104
101
|
const columnRefs = new Map<string, ColumnRef>();
|
|
105
102
|
const includeAliases = new Set<string>();
|
|
103
|
+
const aliasResolver = makeAliasResolver(plan.ast);
|
|
106
104
|
|
|
107
105
|
for (const item of projection) {
|
|
108
106
|
aliases.push(item.alias);
|
|
109
107
|
|
|
110
|
-
const codec = resolveProjectionCodec(item,
|
|
108
|
+
const codec = resolveProjectionCodec(item, contractCodecs, aliasResolver);
|
|
111
109
|
if (codec) {
|
|
112
110
|
codecs.set(item.alias, codec);
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
if (item.expr.kind === 'column-ref') {
|
|
116
|
-
columnRefs.set(item.alias, {
|
|
114
|
+
columnRefs.set(item.alias, {
|
|
115
|
+
table: aliasResolver(item.expr.table),
|
|
116
|
+
column: item.expr.column,
|
|
117
|
+
});
|
|
118
|
+
} else if (item.refs) {
|
|
119
|
+
columnRefs.set(item.alias, {
|
|
120
|
+
table: aliasResolver(item.refs.table),
|
|
121
|
+
column: item.refs.column,
|
|
122
|
+
});
|
|
117
123
|
} else if (item.expr.kind === 'subquery' || item.expr.kind === 'json-array-agg') {
|
|
118
124
|
includeAliases.add(item.alias);
|
|
119
125
|
}
|
|
@@ -131,10 +137,6 @@ function previewWireValue(wireValue: unknown): string {
|
|
|
131
137
|
return String(wireValue).substring(0, WIRE_PREVIEW_LIMIT);
|
|
132
138
|
}
|
|
133
139
|
|
|
134
|
-
function isJsonSchemaValidationError(error: unknown): boolean {
|
|
135
|
-
return isRuntimeError(error) && error.code === 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
140
|
function wrapDecodeFailure(
|
|
139
141
|
error: unknown,
|
|
140
142
|
alias: string,
|
|
@@ -197,25 +199,16 @@ function decodeIncludeAggregate(alias: string, wireValue: unknown): unknown {
|
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
/**
|
|
200
|
-
* Decodes a single field. Single-armed: every cell takes the same path —
|
|
201
|
-
*
|
|
202
|
-
* sync- and async-authored codecs are indistinguishable to callers.
|
|
202
|
+
* Decodes a single field. Single-armed: every cell takes the same path — `codec.decode → await → return plain value` — so sync- and async-authored codecs are indistinguishable to callers. JSON-Schema validation, when required, lives inside the resolved codec's `decode` body (e.g. `arktype-json` validates against its rehydrated schema and throws `RUNTIME.JSON_SCHEMA_VALIDATION_FAILED` from `decode` directly); there is
|
|
203
|
+
* no separate validator-registry pass.
|
|
203
204
|
*
|
|
204
|
-
* The row-level `rowCtx` is repackaged into a per-cell
|
|
205
|
-
*
|
|
206
|
-
* projection of the per-cell `ColumnRef = { table, column }` resolved from
|
|
207
|
-
* the AST-backed `DecodeContext` (the same resolution `wrapDecodeFailure`
|
|
208
|
-
* uses for envelope construction — one resolution per cell, two consumers).
|
|
209
|
-
* Cells the runtime cannot resolve to a single underlying column (aggregate
|
|
210
|
-
* aliases, computed projections without a simple ref) get
|
|
211
|
-
* `column: undefined`, matching the spec contract that the runtime never
|
|
212
|
-
* silently defaults this field.
|
|
205
|
+
* The row-level `rowCtx` is repackaged into a per-cell `SqlCodecCallContext` whose `column = { table, name }` is a structural projection of the per-cell `ColumnRef = { table, column }` resolved from the AST-backed `DecodeContext` (the same resolution `wrapDecodeFailure` uses for envelope construction — one resolution per cell, two consumers). Cells the runtime cannot resolve to a single underlying column (aggregate
|
|
206
|
+
* aliases, computed projections without a simple ref) get `column: undefined`, matching the spec contract that the runtime never silently defaults this field.
|
|
213
207
|
*/
|
|
214
208
|
async function decodeField(
|
|
215
209
|
alias: string,
|
|
216
210
|
wireValue: unknown,
|
|
217
211
|
decodeCtx: DecodeContext,
|
|
218
|
-
jsonValidators: JsonSchemaValidatorRegistry | undefined,
|
|
219
212
|
rowCtx: SqlCodecCallContext,
|
|
220
213
|
): Promise<unknown> {
|
|
221
214
|
if (wireValue === null) {
|
|
@@ -229,15 +222,8 @@ async function decodeField(
|
|
|
229
222
|
|
|
230
223
|
const ref = decodeCtx.columnRefs.get(alias);
|
|
231
224
|
|
|
232
|
-
// Per-cell ctx: the cell-level `column` is a `SqlColumnRef = { table, name }`
|
|
233
|
-
//
|
|
234
|
-
// resolution `wrapDecodeFailure` uses below — no double work). Cells the
|
|
235
|
-
// runtime cannot resolve (aggregate aliases, computed projections without
|
|
236
|
-
// a simple ref) drop the `column` field entirely — explicitly cleared so
|
|
237
|
-
// a previously-populated `rowCtx.column` cannot leak through to unrelated
|
|
238
|
-
// cells. Destructuring (rather than `column: undefined`) is required
|
|
239
|
-
// because `SqlCodecCallContext.column` is declared `column?: SqlColumnRef`
|
|
240
|
-
// under `exactOptionalPropertyTypes`.
|
|
225
|
+
// Per-cell ctx: the cell-level `column` is a `SqlColumnRef = { table, name }` projection of the resolved `ColumnRef = { table, column }` (same resolution `wrapDecodeFailure` uses below — no double work). Cells the runtime cannot resolve (aggregate aliases, computed projections without a simple ref) drop the `column` field entirely — explicitly cleared so a previously-populated `rowCtx.column` cannot leak through to
|
|
226
|
+
// unrelated cells. Destructuring (rather than `column: undefined`) is required because `SqlCodecCallContext.column` is declared `column?: SqlColumnRef` under `exactOptionalPropertyTypes`.
|
|
241
227
|
let cellCtx: SqlCodecCallContext;
|
|
242
228
|
if (ref) {
|
|
243
229
|
cellCtx = { ...rowCtx, column: { table: ref.table, name: ref.column } };
|
|
@@ -246,55 +232,36 @@ async function decodeField(
|
|
|
246
232
|
cellCtx = rowCtxWithoutColumn;
|
|
247
233
|
}
|
|
248
234
|
|
|
249
|
-
let decoded: unknown;
|
|
250
235
|
try {
|
|
251
|
-
|
|
236
|
+
return await codec.decode(wireValue, cellCtx);
|
|
252
237
|
} catch (error) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (jsonValidators && ref) {
|
|
257
|
-
try {
|
|
258
|
-
validateJsonValue(jsonValidators, ref.table, ref.column, decoded, 'decode', codec.id);
|
|
259
|
-
} catch (error) {
|
|
260
|
-
if (isJsonSchemaValidationError(error)) throw error;
|
|
261
|
-
wrapDecodeFailure(error, alias, ref, codec, wireValue);
|
|
238
|
+
// Codec-authored runtime envelopes (e.g. `RUNTIME.DECODE_FAILED` thrown from inside the codec body, or `RUNTIME.ABORTED` raised via `CodecCallContext.signal` per ADR 207) carry their own per-codec context — wrapping them again would erase that context and coerce the abort intent into a generic decode failure. Pass them through unchanged; only foreign errors get the `wrapDecodeFailure` envelope.
|
|
239
|
+
if (isRuntimeError(error)) {
|
|
240
|
+
throw error;
|
|
262
241
|
}
|
|
242
|
+
wrapDecodeFailure(error, alias, ref, codec, wireValue);
|
|
263
243
|
}
|
|
264
|
-
|
|
265
|
-
return decoded;
|
|
266
244
|
}
|
|
267
245
|
|
|
268
246
|
/**
|
|
269
|
-
* Decodes a row by dispatching all per-cell codec calls concurrently via
|
|
270
|
-
* `Promise.all`. Each cell follows the single-armed `decodeField` path.
|
|
271
|
-
* Failures are wrapped in `RUNTIME.DECODE_FAILED` with `{ table, column,
|
|
272
|
-
* codec }` (or `{ alias, codec }` when no column ref is resolvable) and the
|
|
273
|
-
* original error attached on `cause`.
|
|
247
|
+
* Decodes a row by dispatching all per-cell codec calls concurrently via `Promise.all`. Each cell follows the single-armed `decodeField` path. Failures are wrapped in `RUNTIME.DECODE_FAILED` with `{ table, column, codec }` (or `{ alias, codec }` when no column ref is resolvable) and the original error attached on `cause`.
|
|
274
248
|
*
|
|
275
249
|
* When `rowCtx.signal` is provided:
|
|
276
250
|
*
|
|
277
|
-
* - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED`
|
|
278
|
-
*
|
|
279
|
-
* -
|
|
280
|
-
* signal so the runtime returns promptly even when codec bodies ignore
|
|
281
|
-
* it. In-flight bodies that ignore the signal complete in the
|
|
282
|
-
* background (cooperative cancellation).
|
|
283
|
-
* - Existing `RUNTIME.DECODE_FAILED` envelopes from codec bodies pass
|
|
284
|
-
* through unchanged (no double wrap).
|
|
251
|
+
* - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED` (`{ phase: 'decode' }`) before any `codec.decode` call is made.
|
|
252
|
+
* - **Mid-flight aborts** race the per-cell `Promise.all` against the signal so the runtime returns promptly even when codec bodies ignore it. In-flight bodies that ignore the signal complete in the background (cooperative cancellation).
|
|
253
|
+
* - Existing `RUNTIME.DECODE_FAILED` envelopes from codec bodies pass through unchanged (no double wrap).
|
|
285
254
|
*/
|
|
286
255
|
export async function decodeRow(
|
|
287
256
|
row: Record<string, unknown>,
|
|
288
257
|
plan: SqlExecutionPlan,
|
|
289
|
-
registry: CodecRegistry,
|
|
290
|
-
jsonValidators: JsonSchemaValidatorRegistry | undefined,
|
|
291
258
|
rowCtx: SqlCodecCallContext,
|
|
292
259
|
contractCodecs?: ContractCodecRegistry,
|
|
293
260
|
): Promise<Record<string, unknown>> {
|
|
294
261
|
checkAborted(rowCtx, 'decode');
|
|
295
262
|
const signal = rowCtx.signal;
|
|
296
263
|
|
|
297
|
-
const decodeCtx = buildDecodeContext(plan,
|
|
264
|
+
const decodeCtx = buildDecodeContext(plan, contractCodecs);
|
|
298
265
|
|
|
299
266
|
const aliases = decodeCtx.aliases ?? Object.keys(row);
|
|
300
267
|
|
|
@@ -323,13 +290,12 @@ export async function decodeRow(
|
|
|
323
290
|
continue;
|
|
324
291
|
}
|
|
325
292
|
|
|
326
|
-
tasks.push(decodeField(alias, wireValue, decodeCtx,
|
|
293
|
+
tasks.push(decodeField(alias, wireValue, decodeCtx, rowCtx));
|
|
327
294
|
}
|
|
328
295
|
|
|
329
296
|
const settled = await raceAgainstAbort(Promise.all(tasks), signal, 'decode');
|
|
330
297
|
|
|
331
|
-
// Include aggregates are decoded synchronously after concurrent codec
|
|
332
|
-
// dispatch settles, so any decode failures upstream propagate first.
|
|
298
|
+
// Include aggregates are decoded synchronously after concurrent codec dispatch settles, so any decode failures upstream propagate first.
|
|
333
299
|
for (const entry of includeIndices) {
|
|
334
300
|
settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value);
|
|
335
301
|
}
|
package/src/codecs/encoding.ts
CHANGED
|
@@ -5,51 +5,54 @@ import {
|
|
|
5
5
|
} from '@prisma-next/framework-components/runtime';
|
|
6
6
|
import {
|
|
7
7
|
type Codec,
|
|
8
|
-
type CodecRegistry,
|
|
9
8
|
type ContractCodecRegistry,
|
|
10
9
|
collectOrderedParamRefs,
|
|
10
|
+
type ParamRefBindingRefs,
|
|
11
11
|
type SqlCodecCallContext,
|
|
12
12
|
} from '@prisma-next/sql-relational-core/ast';
|
|
13
13
|
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
14
|
+
import { makeAliasResolver } from './alias-resolver';
|
|
14
15
|
|
|
15
16
|
interface ParamMetadata {
|
|
16
17
|
readonly codecId: string | undefined;
|
|
17
18
|
readonly name: string | undefined;
|
|
19
|
+
readonly refs: ParamRefBindingRefs | undefined;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
const NO_METADATA: ParamMetadata = Object.freeze({
|
|
22
|
+
const NO_METADATA: ParamMetadata = Object.freeze({
|
|
23
|
+
codecId: undefined,
|
|
24
|
+
name: undefined,
|
|
25
|
+
refs: undefined,
|
|
26
|
+
});
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
29
|
* Resolve the codec for an outgoing param.
|
|
24
30
|
*
|
|
25
|
-
*
|
|
26
|
-
* `(table, column)` ref today — every `ParamRef` carries `codecId` but
|
|
27
|
-
* not the column it relates to. Encode-side dispatch therefore consults
|
|
28
|
-
* `contractCodecs.forCodecId(codecId)` (which itself prefers the
|
|
29
|
-
* contract-walk-derived shared codec, falling back to the legacy
|
|
30
|
-
* `CodecRegistry.get` for parameterized codec ids whose contracts don't
|
|
31
|
-
* have a column the walk could resolve through).
|
|
31
|
+
* Column-aware dispatch: when `metadata.refs` is populated by a column-bound construction site, prefer `contractCodecs.forColumn(refs.table, refs.column)` — that returns the per-instance codec the contract walk materialized for the `(table, column)` pair, encoding the column's typeParams (e.g. `vector(1024)` vs. `vector(1536)`).
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
* json/jsonb), encode is per-instance-stateless w.r.t. params:
|
|
35
|
-
* - pgvector formats `[v1,v2,...]` regardless of declared length;
|
|
36
|
-
* - postgres json/jsonb encode is `JSON.stringify` regardless of schema.
|
|
33
|
+
* On a column-lookup miss the resolver falls through to `forCodecId`. The wrong-instance risk for parameterized codecs is closed off structurally:
|
|
37
34
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* construction sites.
|
|
35
|
+
* 1. `buildContractCodecRegistry` pre-populates `byCodecId` with one canonical instance per non-parameterized descriptor; parameterized descriptors are intentionally absent from this pre-population. 2. `forCodecId` rejects ambiguous parameterized fallbacks (`ambiguousCodecIds`) — if the contract walk resolved more than one distinct instance under a single parameterized id, the call throws rather than binding to
|
|
36
|
+
* whichever landed first. 3. For the non-ambiguous parameterized case (a single column with that id), `byCodecId` stores the column-correct per-instance codec, so the fall-through still resolves to the right instance.
|
|
37
|
+
*
|
|
38
|
+
* Refs-less fallback: ParamRefs constructed outside a column-bound site (literals, transient builder state) carry a non-parameterized `codecId` whose dispatch is ambiguity-free. The validator pass (`validateParamRefRefs`) already enforced refs on every parameterized ParamRef before encode runs.
|
|
43
39
|
*/
|
|
44
40
|
function resolveParamCodec(
|
|
45
41
|
metadata: ParamMetadata,
|
|
46
|
-
registry: CodecRegistry,
|
|
47
42
|
contractCodecs: ContractCodecRegistry | undefined,
|
|
43
|
+
aliasResolver: (alias: string) => string,
|
|
48
44
|
): Codec | undefined {
|
|
49
45
|
if (!metadata.codecId) return undefined;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
if (metadata.refs && contractCodecs) {
|
|
47
|
+
const byColumn = contractCodecs.forColumn(
|
|
48
|
+
aliasResolver(metadata.refs.table),
|
|
49
|
+
metadata.refs.column,
|
|
50
|
+
);
|
|
51
|
+
// Only honour `byColumn` when its codec id agrees with the `ParamRef`'s declared `codecId`. They can legitimately disagree when a heuristic (e.g. the ORM's `refsFromLeft`) lifts column refs out of an `OperationExpr` that changed the codec id — e.g. `cosineDistance(p.embedding, x).lt(1)` carries `refs={post,embedding}` (a vector column) but the comparison side's codec is `pg/float8@1`. Trusting `byColumn` blindly would dispatch the float
|
|
52
|
+
// literal through the vector codec.
|
|
53
|
+
if (byColumn && byColumn.id === metadata.codecId) return byColumn;
|
|
54
|
+
}
|
|
55
|
+
return contractCodecs?.forCodecId(metadata.codecId);
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
function paramLabel(metadata: ParamMetadata, paramIndex: number): string {
|
|
@@ -74,34 +77,28 @@ function wrapEncodeFailure(
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
/**
|
|
77
|
-
* Encodes a single parameter through its codec. Always awaits codec.encode so
|
|
78
|
-
* a Promise can never leak into the driver, even if a sync-authored codec is
|
|
79
|
-
* lifted to async by the codec() factory. Failures are wrapped in
|
|
80
|
-
* `RUNTIME.ENCODE_FAILED` with `{ label, codec, paramIndex }` and the original
|
|
81
|
-
* error attached on `cause`.
|
|
80
|
+
* Encodes a single parameter through its codec. Always awaits codec.encode so a Promise can never leak into the driver, even if a sync-authored codec is lifted to async by the codec factory. Failures are wrapped in `RUNTIME.ENCODE_FAILED` with `{ label, codec, paramIndex }` and the original error attached on `cause`.
|
|
82
81
|
*
|
|
83
|
-
* `ctx` is forwarded verbatim to `codec.encode` so codec authors who opt
|
|
84
|
-
* into the `(value, ctx)` arity see the same `SqlCodecCallContext` the
|
|
85
|
-
* runtime built for the surrounding `runtime.execute()` call. The ctx is
|
|
86
|
-
* always present; its `signal` field may be `undefined`. Encode call
|
|
87
|
-
* sites do not populate `ctx.column` — encode-time column context is the
|
|
88
|
-
* middleware's domain.
|
|
82
|
+
* `ctx` is forwarded verbatim to `codec.encode` so codec authors who opt into the `(value, ctx)` arity see the same `SqlCodecCallContext` the runtime built for the surrounding `runtime.execute()` call. The ctx is always present; its `signal` field may be `undefined`. Encode call sites do not populate `ctx.column` — encode-time column context is the middleware's domain.
|
|
89
83
|
*/
|
|
90
84
|
export async function encodeParam(
|
|
91
85
|
value: unknown,
|
|
92
|
-
paramRef: {
|
|
86
|
+
paramRef: {
|
|
87
|
+
readonly codecId?: string;
|
|
88
|
+
readonly name?: string;
|
|
89
|
+
readonly refs?: ParamRefBindingRefs;
|
|
90
|
+
},
|
|
93
91
|
paramIndex: number,
|
|
94
|
-
registry: CodecRegistry,
|
|
95
92
|
ctx: SqlCodecCallContext,
|
|
96
93
|
contractCodecs?: ContractCodecRegistry,
|
|
97
94
|
): Promise<unknown> {
|
|
98
95
|
return encodeParamValue(
|
|
99
96
|
value,
|
|
100
|
-
{ codecId: paramRef.codecId, name: paramRef.name },
|
|
97
|
+
{ codecId: paramRef.codecId, name: paramRef.name, refs: paramRef.refs },
|
|
101
98
|
paramIndex,
|
|
102
|
-
registry,
|
|
103
99
|
ctx,
|
|
104
100
|
contractCodecs,
|
|
101
|
+
(alias) => alias,
|
|
105
102
|
);
|
|
106
103
|
}
|
|
107
104
|
|
|
@@ -109,15 +106,15 @@ async function encodeParamValue(
|
|
|
109
106
|
value: unknown,
|
|
110
107
|
metadata: ParamMetadata,
|
|
111
108
|
paramIndex: number,
|
|
112
|
-
registry: CodecRegistry,
|
|
113
109
|
ctx: SqlCodecCallContext,
|
|
114
110
|
contractCodecs: ContractCodecRegistry | undefined,
|
|
111
|
+
aliasResolver: (alias: string) => string,
|
|
115
112
|
): Promise<unknown> {
|
|
116
113
|
if (value === null || value === undefined) {
|
|
117
114
|
return null;
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
const codec = resolveParamCodec(metadata,
|
|
117
|
+
const codec = resolveParamCodec(metadata, contractCodecs, aliasResolver);
|
|
121
118
|
if (!codec) {
|
|
122
119
|
return value;
|
|
123
120
|
}
|
|
@@ -130,27 +127,16 @@ async function encodeParamValue(
|
|
|
130
127
|
}
|
|
131
128
|
|
|
132
129
|
/**
|
|
133
|
-
* Encodes all parameters concurrently via `Promise.all`. Per parameter, sync-
|
|
134
|
-
* and async-authored codecs share the same path: `codec.encode → await →
|
|
135
|
-
* return`. Param-level failures are wrapped in `RUNTIME.ENCODE_FAILED`.
|
|
130
|
+
* Encodes all parameters concurrently via `Promise.all`. Per parameter, sync-and async-authored codecs share the same path: `codec.encode → await → return`. Param-level failures are wrapped in `RUNTIME.ENCODE_FAILED`.
|
|
136
131
|
*
|
|
137
132
|
* When `ctx.signal` is provided:
|
|
138
133
|
*
|
|
139
|
-
* - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED`
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* - **Mid-flight abort** races the per-param `Promise.all` against
|
|
143
|
-
* `abortable(ctx.signal)`. The runtime returns `RUNTIME.ABORTED` promptly
|
|
144
|
-
* even if codec bodies ignore the signal; the in-flight bodies are
|
|
145
|
-
* abandoned and run to completion in the background (cooperative
|
|
146
|
-
* cancellation, see ADR 204).
|
|
147
|
-
* - Existing `RUNTIME.ENCODE_FAILED` envelopes that surface from a codec
|
|
148
|
-
* body before the runtime observes the abort pass through unchanged
|
|
149
|
-
* (no double wrap).
|
|
134
|
+
* - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED` (`{ phase: 'encode' }`) before any `codec.encode` call is made — codecs can pin this with a per-call counter that stays at zero.
|
|
135
|
+
* - **Mid-flight abort** races the per-param `Promise.all` against `abortable(ctx.signal)`. The runtime returns `RUNTIME.ABORTED` promptly even if codec bodies ignore the signal; the in-flight bodies are abandoned and run to completion in the background (cooperative cancellation, see ADR 204).
|
|
136
|
+
* - Existing `RUNTIME.ENCODE_FAILED` envelopes that surface from a codec body before the runtime observes the abort pass through unchanged (no double wrap).
|
|
150
137
|
*/
|
|
151
138
|
export async function encodeParams(
|
|
152
139
|
plan: SqlExecutionPlan,
|
|
153
|
-
registry: CodecRegistry,
|
|
154
140
|
ctx: SqlCodecCallContext,
|
|
155
141
|
contractCodecs?: ContractCodecRegistry,
|
|
156
142
|
): Promise<readonly unknown[]> {
|
|
@@ -169,20 +155,22 @@ export async function encodeParams(
|
|
|
169
155
|
for (let i = 0; i < paramCount && i < refs.length; i++) {
|
|
170
156
|
const ref = refs[i];
|
|
171
157
|
if (ref) {
|
|
172
|
-
metadata[i] = { codecId: ref.codecId, name: ref.name };
|
|
158
|
+
metadata[i] = { codecId: ref.codecId, name: ref.name, refs: ref.refs };
|
|
173
159
|
}
|
|
174
160
|
}
|
|
175
161
|
}
|
|
176
162
|
|
|
163
|
+
const aliasResolver = makeAliasResolver(plan.ast);
|
|
164
|
+
|
|
177
165
|
const tasks: Promise<unknown>[] = new Array(paramCount);
|
|
178
166
|
for (let i = 0; i < paramCount; i++) {
|
|
179
167
|
tasks[i] = encodeParamValue(
|
|
180
168
|
plan.params[i],
|
|
181
169
|
metadata[i] ?? NO_METADATA,
|
|
182
170
|
i,
|
|
183
|
-
registry,
|
|
184
171
|
ctx,
|
|
185
172
|
contractCodecs,
|
|
173
|
+
aliasResolver,
|
|
186
174
|
);
|
|
187
175
|
}
|
|
188
176
|
|
package/src/codecs/validation.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
2
|
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
3
3
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
4
|
-
import type { CodecRegistry } from '@prisma-next/sql-relational-core/ast';
|
|
5
4
|
import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
|
|
6
5
|
|
|
7
6
|
export function extractCodecIds(contract: Contract<SqlStorage>): Set<string> {
|
|
@@ -31,33 +30,15 @@ function extractCodecIdsFromColumns(contract: Contract<SqlStorage>): Map<string,
|
|
|
31
30
|
return codecIds;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
interface CodecLookupForValidation {
|
|
35
|
-
has(id: string): boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function adaptDescriptorRegistry(registry: CodecDescriptorRegistry): CodecLookupForValidation {
|
|
39
|
-
return { has: (id: string) => registry.descriptorFor(id) !== undefined };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isDescriptorRegistry(
|
|
43
|
-
registry: CodecRegistry | CodecDescriptorRegistry,
|
|
44
|
-
): registry is CodecDescriptorRegistry {
|
|
45
|
-
return 'descriptorFor' in registry;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
33
|
export function validateContractCodecMappings(
|
|
49
|
-
registry:
|
|
34
|
+
registry: CodecDescriptorRegistry,
|
|
50
35
|
contract: Contract<SqlStorage>,
|
|
51
36
|
): void {
|
|
52
|
-
const lookup: CodecLookupForValidation = isDescriptorRegistry(registry)
|
|
53
|
-
? adaptDescriptorRegistry(registry)
|
|
54
|
-
: registry;
|
|
55
|
-
|
|
56
37
|
const codecIds = extractCodecIdsFromColumns(contract);
|
|
57
38
|
const invalidCodecs: Array<{ table: string; column: string; codecId: string }> = [];
|
|
58
39
|
|
|
59
40
|
for (const [key, codecId] of codecIds.entries()) {
|
|
60
|
-
if (
|
|
41
|
+
if (registry.descriptorFor(codecId) === undefined) {
|
|
61
42
|
const parts = key.split('.');
|
|
62
43
|
const table = parts[0] ?? '';
|
|
63
44
|
const column = parts[1] ?? '';
|
|
@@ -80,7 +61,7 @@ export function validateContractCodecMappings(
|
|
|
80
61
|
}
|
|
81
62
|
|
|
82
63
|
export function validateCodecRegistryCompleteness(
|
|
83
|
-
registry:
|
|
64
|
+
registry: CodecDescriptorRegistry,
|
|
84
65
|
contract: Contract<SqlStorage>,
|
|
85
66
|
): void {
|
|
86
67
|
validateContractCodecMappings(registry, contract);
|
|
@@ -25,14 +25,17 @@ function hasAggregateWithoutGroupBy(ast: SelectAst): boolean {
|
|
|
25
25
|
return ast.projection.some((item) => item.expr.kind === 'aggregate');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function primaryTableFromAst(ast: SelectAst): string
|
|
28
|
+
function primaryTableFromAst(ast: SelectAst): string {
|
|
29
29
|
switch (ast.from.kind) {
|
|
30
30
|
case 'table-source':
|
|
31
31
|
return ast.from.name;
|
|
32
32
|
case 'derived-table-source':
|
|
33
33
|
return ast.from.alias;
|
|
34
|
+
// v8 ignore next 4
|
|
34
35
|
default:
|
|
35
|
-
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Unsupported source kind: ${(ast.from satisfies never as { kind: string }).kind}`,
|
|
38
|
+
);
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -41,17 +44,12 @@ function estimateRowsFromAst(
|
|
|
41
44
|
tableRows: Record<string, number>,
|
|
42
45
|
defaultTableRows: number,
|
|
43
46
|
hasAggregateWithoutGroup: boolean,
|
|
44
|
-
): number
|
|
47
|
+
): number {
|
|
45
48
|
if (hasAggregateWithoutGroup) {
|
|
46
49
|
return 1;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
const
|
|
50
|
-
if (!table) {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const tableEstimate = tableRows[table] ?? defaultTableRows;
|
|
52
|
+
const tableEstimate = tableRows[primaryTableFromAst(ast)] ?? defaultTableRows;
|
|
55
53
|
|
|
56
54
|
if (ast.limit !== undefined) {
|
|
57
55
|
return Math.min(ast.limit, tableEstimate);
|
|
@@ -136,31 +134,19 @@ export function budgets(options?: BudgetsOptions): SqlMiddleware {
|
|
|
136
134
|
const shouldBlock = rowSeverity === 'error' || ctx.mode === 'strict';
|
|
137
135
|
|
|
138
136
|
if (isUnbounded) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
estimatedRows: estimated,
|
|
144
|
-
maxRows,
|
|
145
|
-
}),
|
|
146
|
-
shouldBlock,
|
|
147
|
-
ctx,
|
|
148
|
-
);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
137
|
+
const details =
|
|
138
|
+
estimated >= maxRows
|
|
139
|
+
? { source: 'ast', estimatedRows: estimated, maxRows }
|
|
140
|
+
: { source: 'ast', maxRows };
|
|
152
141
|
emitBudgetViolation(
|
|
153
|
-
runtimeError('BUDGET.ROWS_EXCEEDED', 'Unbounded SELECT query exceeds budget',
|
|
154
|
-
source: 'ast',
|
|
155
|
-
maxRows,
|
|
156
|
-
}),
|
|
142
|
+
runtimeError('BUDGET.ROWS_EXCEEDED', 'Unbounded SELECT query exceeds budget', details),
|
|
157
143
|
shouldBlock,
|
|
158
144
|
ctx,
|
|
159
145
|
);
|
|
160
146
|
return;
|
|
161
147
|
}
|
|
162
148
|
|
|
163
|
-
if (estimated
|
|
149
|
+
if (estimated > maxRows) {
|
|
164
150
|
emitBudgetViolation(
|
|
165
151
|
runtimeError('BUDGET.ROWS_EXCEEDED', 'Estimated row count exceeds budget', {
|
|
166
152
|
source: 'ast',
|