@prisma-next/sql-runtime 0.5.0-dev.7 → 0.5.0-dev.70
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 +31 -22
- package/dist/exports-CXtbKm5q.mjs +1516 -0
- package/dist/exports-CXtbKm5q.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-C4Dz0JKE.d.mts} +116 -45
- package/dist/index-C4Dz0JKE.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -3
- package/dist/test/utils.d.mts +38 -33
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +107 -56
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +18 -19
- package/src/codecs/alias-resolver.ts +34 -0
- package/src/codecs/decoding.ts +263 -176
- package/src/codecs/encoding.ts +151 -38
- package/src/codecs/validation.ts +4 -4
- package/src/content-hash.ts +44 -0
- package/src/exports/index.ts +13 -7
- package/src/fingerprint.ts +22 -0
- package/src/guardrails/raw.ts +165 -0
- package/src/lower-sql-plan.ts +3 -3
- package/src/marker.ts +75 -0
- package/src/middleware/before-compile-chain.ts +1 -0
- package/src/middleware/budgets.ts +36 -120
- package/src/middleware/lints.ts +3 -3
- package/src/middleware/sql-middleware.ts +6 -5
- package/src/runtime-spi.ts +44 -0
- package/src/sql-context.ts +315 -105
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +89 -51
- package/src/sql-runtime.ts +305 -144
- package/dist/exports-BQZSVXXt.mjs +0 -981
- package/dist/exports-BQZSVXXt.mjs.map +0 -1
- package/dist/index-yb51L_1h.d.mts.map +0 -1
- package/src/codecs/json-schema-validation.ts +0 -61
- 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/src/codecs/decoding.ts
CHANGED
|
@@ -1,221 +1,308 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
checkAborted,
|
|
3
|
+
isRuntimeError,
|
|
4
|
+
raceAgainstAbort,
|
|
5
|
+
runtimeError,
|
|
6
|
+
} from '@prisma-next/framework-components/runtime';
|
|
7
|
+
import type {
|
|
8
|
+
AnyQueryAst,
|
|
9
|
+
Codec,
|
|
10
|
+
ContractCodecRegistry,
|
|
11
|
+
ProjectionItem,
|
|
12
|
+
SqlCodecCallContext,
|
|
13
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
14
|
+
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
15
|
+
import { makeAliasResolver } from './alias-resolver';
|
|
5
16
|
|
|
6
|
-
|
|
7
|
-
alias: string,
|
|
8
|
-
plan: ExecutionPlan,
|
|
9
|
-
registry: CodecRegistry,
|
|
10
|
-
): Codec | null {
|
|
11
|
-
const planCodecId = plan.meta.annotations?.codecs?.[alias] as string | undefined;
|
|
12
|
-
if (planCodecId) {
|
|
13
|
-
const codec = registry.get(planCodecId);
|
|
14
|
-
if (codec) {
|
|
15
|
-
return codec;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
17
|
+
type ColumnRef = { table: string; column: string };
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
interface DecodeContext {
|
|
20
|
+
readonly aliases: ReadonlyArray<string> | undefined;
|
|
21
|
+
readonly codecs: ReadonlyMap<string, Codec>;
|
|
22
|
+
readonly columnRefs: ReadonlyMap<string, ColumnRef>;
|
|
23
|
+
readonly includeAliases: ReadonlySet<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const WIRE_PREVIEW_LIMIT = 100;
|
|
27
|
+
const EMPTY_INCLUDE_ALIASES: ReadonlySet<string> = new Set<string>();
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
function isAstBackedPlan(
|
|
30
|
+
plan: SqlExecutionPlan,
|
|
31
|
+
): plan is SqlExecutionPlan & { readonly ast: AnyQueryAst } {
|
|
32
|
+
return plan.ast !== undefined;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
function projectionListFromAst(ast: AnyQueryAst): ReadonlyArray<ProjectionItem> | undefined {
|
|
36
|
+
if (ast.kind === 'select') {
|
|
37
|
+
return ast.projection;
|
|
38
|
+
}
|
|
39
|
+
return ast.returning;
|
|
40
|
+
}
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
43
|
+
* Resolve the per-cell codec for a projection item.
|
|
44
|
+
*
|
|
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.).
|
|
46
|
+
*
|
|
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.
|
|
37
54
|
*/
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
function resolveProjectionCodec(
|
|
56
|
+
item: ProjectionItem,
|
|
57
|
+
contractCodecs: ContractCodecRegistry | undefined,
|
|
58
|
+
aliasResolver: (alias: string) => string,
|
|
59
|
+
): Codec | undefined {
|
|
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
|
+
}
|
|
45
69
|
}
|
|
46
|
-
|
|
70
|
+
if (item.codecId) {
|
|
71
|
+
return contractCodecs?.forCodecId(item.codecId);
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
47
74
|
}
|
|
48
75
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
function buildDecodeContext(
|
|
77
|
+
plan: SqlExecutionPlan,
|
|
78
|
+
contractCodecs: ContractCodecRegistry | undefined,
|
|
79
|
+
): DecodeContext {
|
|
80
|
+
if (!isAstBackedPlan(plan)) {
|
|
81
|
+
return {
|
|
82
|
+
aliases: undefined,
|
|
83
|
+
codecs: new Map(),
|
|
84
|
+
columnRefs: new Map(),
|
|
85
|
+
includeAliases: EMPTY_INCLUDE_ALIASES,
|
|
86
|
+
};
|
|
52
87
|
}
|
|
53
88
|
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
return
|
|
89
|
+
const projection = projectionListFromAst(plan.ast);
|
|
90
|
+
if (!projection) {
|
|
91
|
+
return {
|
|
92
|
+
aliases: undefined,
|
|
93
|
+
codecs: new Map(),
|
|
94
|
+
columnRefs: new Map(),
|
|
95
|
+
includeAliases: EMPTY_INCLUDE_ALIASES,
|
|
96
|
+
};
|
|
57
97
|
}
|
|
58
98
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
99
|
+
const aliases: string[] = [];
|
|
100
|
+
const codecs = new Map<string, Codec>();
|
|
101
|
+
const columnRefs = new Map<string, ColumnRef>();
|
|
102
|
+
const includeAliases = new Set<string>();
|
|
103
|
+
const aliasResolver = makeAliasResolver(plan.ast);
|
|
104
|
+
|
|
105
|
+
for (const item of projection) {
|
|
106
|
+
aliases.push(item.alias);
|
|
107
|
+
|
|
108
|
+
const codec = resolveProjectionCodec(item, contractCodecs, aliasResolver);
|
|
109
|
+
if (codec) {
|
|
110
|
+
codecs.set(item.alias, codec);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (item.expr.kind === 'column-ref') {
|
|
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
|
+
});
|
|
123
|
+
} else if (item.expr.kind === 'subquery' || item.expr.kind === 'json-array-agg') {
|
|
124
|
+
includeAliases.add(item.alias);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { aliases, codecs, columnRefs, includeAliases };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function previewWireValue(wireValue: unknown): string {
|
|
132
|
+
if (typeof wireValue === 'string') {
|
|
133
|
+
return wireValue.length > WIRE_PREVIEW_LIMIT
|
|
134
|
+
? `${wireValue.substring(0, WIRE_PREVIEW_LIMIT)}...`
|
|
135
|
+
: wireValue;
|
|
136
|
+
}
|
|
137
|
+
return String(wireValue).substring(0, WIRE_PREVIEW_LIMIT);
|
|
63
138
|
}
|
|
64
139
|
|
|
65
|
-
function
|
|
140
|
+
function wrapDecodeFailure(
|
|
141
|
+
error: unknown,
|
|
66
142
|
alias: string,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
143
|
+
ref: ColumnRef | undefined,
|
|
144
|
+
codec: Codec,
|
|
145
|
+
wireValue: unknown,
|
|
146
|
+
): never {
|
|
147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
148
|
+
const target = ref ? `${ref.table}.${ref.column}` : alias;
|
|
149
|
+
const wrapped = runtimeError(
|
|
150
|
+
'RUNTIME.DECODE_FAILED',
|
|
151
|
+
`Failed to decode column ${target} with codec '${codec.id}': ${message}`,
|
|
152
|
+
{
|
|
153
|
+
...(ref ? { table: ref.table, column: ref.column } : { alias }),
|
|
154
|
+
codec: codec.id,
|
|
155
|
+
wirePreview: previewWireValue(wireValue),
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
wrapped.cause = error;
|
|
159
|
+
throw wrapped;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function wrapIncludeAggregateFailure(error: unknown, alias: string, wireValue: unknown): never {
|
|
163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
+
const wrapped = runtimeError(
|
|
165
|
+
'RUNTIME.DECODE_FAILED',
|
|
166
|
+
`Failed to parse JSON array for include alias '${alias}': ${message}`,
|
|
167
|
+
{
|
|
168
|
+
alias,
|
|
169
|
+
wirePreview: previewWireValue(wireValue),
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
wrapped.cause = error;
|
|
173
|
+
throw wrapped;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function decodeIncludeAggregate(alias: string, wireValue: unknown): unknown {
|
|
177
|
+
if (wireValue === null || wireValue === undefined) {
|
|
178
|
+
return [];
|
|
76
179
|
}
|
|
77
180
|
|
|
78
|
-
|
|
181
|
+
try {
|
|
182
|
+
let parsed: unknown;
|
|
183
|
+
if (typeof wireValue === 'string') {
|
|
184
|
+
parsed = JSON.parse(wireValue);
|
|
185
|
+
} else if (Array.isArray(wireValue)) {
|
|
186
|
+
parsed = wireValue;
|
|
187
|
+
} else {
|
|
188
|
+
parsed = JSON.parse(String(wireValue));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!Array.isArray(parsed)) {
|
|
192
|
+
throw new Error(`Expected array for include alias '${alias}', got ${typeof parsed}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return parsed;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
wrapIncludeAggregateFailure(error, alias, wireValue);
|
|
198
|
+
}
|
|
79
199
|
}
|
|
80
200
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
):
|
|
87
|
-
|
|
88
|
-
|
|
201
|
+
/**
|
|
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.
|
|
204
|
+
*
|
|
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.
|
|
207
|
+
*/
|
|
208
|
+
async function decodeField(
|
|
209
|
+
alias: string,
|
|
210
|
+
wireValue: unknown,
|
|
211
|
+
decodeCtx: DecodeContext,
|
|
212
|
+
rowCtx: SqlCodecCallContext,
|
|
213
|
+
): Promise<unknown> {
|
|
214
|
+
if (wireValue === null) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const codec = decodeCtx.codecs.get(alias);
|
|
219
|
+
if (!codec) {
|
|
220
|
+
return wireValue;
|
|
221
|
+
}
|
|
89
222
|
|
|
90
|
-
|
|
91
|
-
const fallbackColumnRefIndex =
|
|
92
|
-
jsonValidators && (!projection || Array.isArray(projection)) ? buildColumnRefIndex(plan) : null;
|
|
223
|
+
const ref = decodeCtx.columnRefs.get(alias);
|
|
93
224
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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`.
|
|
227
|
+
let cellCtx: SqlCodecCallContext;
|
|
228
|
+
if (ref) {
|
|
229
|
+
cellCtx = { ...rowCtx, column: { table: ref.table, name: ref.column } };
|
|
99
230
|
} else {
|
|
100
|
-
|
|
231
|
+
const { column: _drop, ...rowCtxWithoutColumn } = rowCtx;
|
|
232
|
+
cellCtx = rowCtxWithoutColumn;
|
|
101
233
|
}
|
|
102
234
|
|
|
103
|
-
|
|
104
|
-
|
|
235
|
+
try {
|
|
236
|
+
return await codec.decode(wireValue, cellCtx);
|
|
237
|
+
} catch (error) {
|
|
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;
|
|
241
|
+
}
|
|
242
|
+
wrapDecodeFailure(error, alias, ref, codec, wireValue);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
105
245
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
246
|
+
/**
|
|
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`.
|
|
248
|
+
*
|
|
249
|
+
* When `rowCtx.signal` is provided:
|
|
250
|
+
*
|
|
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).
|
|
254
|
+
*/
|
|
255
|
+
export async function decodeRow(
|
|
256
|
+
row: Record<string, unknown>,
|
|
257
|
+
plan: SqlExecutionPlan,
|
|
258
|
+
rowCtx: SqlCodecCallContext,
|
|
259
|
+
contractCodecs?: ContractCodecRegistry,
|
|
260
|
+
): Promise<Record<string, unknown>> {
|
|
261
|
+
checkAborted(rowCtx, 'decode');
|
|
262
|
+
const signal = rowCtx.signal;
|
|
110
263
|
|
|
111
|
-
|
|
112
|
-
if (wireValue === null || wireValue === undefined) {
|
|
113
|
-
decoded[alias] = [];
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
264
|
+
const decodeCtx = buildDecodeContext(plan, contractCodecs);
|
|
116
265
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 = {
|
|
266
|
+
const aliases = decodeCtx.aliases ?? Object.keys(row);
|
|
267
|
+
|
|
268
|
+
if (decodeCtx.aliases !== undefined) {
|
|
269
|
+
for (const alias of decodeCtx.aliases) {
|
|
270
|
+
if (!Object.hasOwn(row, alias)) {
|
|
271
|
+
throw runtimeError('RUNTIME.DECODE_FAILED', `Row missing projection alias "${alias}"`, {
|
|
145
272
|
alias,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
: String(wireValue).substring(0, 100),
|
|
150
|
-
};
|
|
151
|
-
throw decodeError;
|
|
273
|
+
expectedAliases: decodeCtx.aliases,
|
|
274
|
+
presentKeys: Object.keys(row),
|
|
275
|
+
});
|
|
152
276
|
}
|
|
153
|
-
continue;
|
|
154
277
|
}
|
|
278
|
+
}
|
|
155
279
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
280
|
+
const tasks: Promise<unknown>[] = [];
|
|
281
|
+
const includeIndices: { index: number; alias: string; value: unknown }[] = [];
|
|
160
282
|
|
|
161
|
-
|
|
283
|
+
for (let i = 0; i < aliases.length; i++) {
|
|
284
|
+
const alias = aliases[i] as string;
|
|
285
|
+
const wireValue = row[alias];
|
|
162
286
|
|
|
163
|
-
if (
|
|
164
|
-
|
|
287
|
+
if (decodeCtx.includeAliases.has(alias)) {
|
|
288
|
+
includeIndices.push({ index: i, alias, value: wireValue });
|
|
289
|
+
tasks.push(Promise.resolve(undefined));
|
|
165
290
|
continue;
|
|
166
291
|
}
|
|
167
292
|
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
}
|
|
293
|
+
tasks.push(decodeField(alias, wireValue, decodeCtx, rowCtx));
|
|
294
|
+
}
|
|
185
295
|
|
|
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
|
-
}
|
|
296
|
+
const settled = await raceAgainstAbort(Promise.all(tasks), signal, 'decode');
|
|
196
297
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
code: string;
|
|
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
|
-
}
|
|
298
|
+
// Include aggregates are decoded synchronously after concurrent codec dispatch settles, so any decode failures upstream propagate first.
|
|
299
|
+
for (const entry of includeIndices) {
|
|
300
|
+
settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value);
|
|
218
301
|
}
|
|
219
302
|
|
|
303
|
+
const decoded: Record<string, unknown> = {};
|
|
304
|
+
for (let i = 0; i < aliases.length; i++) {
|
|
305
|
+
decoded[aliases[i] as string] = settled[i];
|
|
306
|
+
}
|
|
220
307
|
return decoded;
|
|
221
308
|
}
|