@prisma-next/sql-runtime 0.5.0-dev.6 → 0.5.0-dev.60
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-BcX9wp4z.mjs +1640 -0
- package/dist/exports-BcX9wp4z.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-DkthtnOX.d.mts} +100 -25
- package/dist/index-DkthtnOX.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/test/utils.d.mts +6 -5
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +13 -6
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +13 -14
- package/src/codecs/decoding.ts +294 -173
- package/src/codecs/encoding.ts +162 -37
- package/src/codecs/validation.ts +22 -3
- package/src/content-hash.ts +44 -0
- package/src/exports/index.ts +12 -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 +26 -96
- 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 +438 -79
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +62 -47
- package/src/sql-runtime.ts +336 -113
- 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/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/sql-context.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Contract, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
|
|
2
|
+
import type { CodecDescriptor } from '@prisma-next/framework-components/codec';
|
|
3
|
+
import { synthesizeNonParameterizedDescriptor } from '@prisma-next/framework-components/codec';
|
|
2
4
|
import type { ComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
5
|
import { checkContractComponentRequirements } from '@prisma-next/framework-components/components';
|
|
4
6
|
import {
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
type RuntimeTargetInstance,
|
|
15
17
|
} from '@prisma-next/framework-components/execution';
|
|
16
18
|
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
17
|
-
import type { SqlStorage
|
|
19
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
18
20
|
import {
|
|
19
21
|
createSqlOperationRegistry,
|
|
20
22
|
type SqlOperationDescriptor,
|
|
@@ -22,46 +24,71 @@ import {
|
|
|
22
24
|
import type {
|
|
23
25
|
Adapter,
|
|
24
26
|
AnyQueryAst,
|
|
25
|
-
|
|
27
|
+
Codec,
|
|
26
28
|
CodecRegistry,
|
|
29
|
+
ContractCodecRegistry,
|
|
27
30
|
LoweredStatement,
|
|
31
|
+
SqlCodecInstanceContext,
|
|
28
32
|
SqlDriver,
|
|
29
33
|
} from '@prisma-next/sql-relational-core/ast';
|
|
30
34
|
import { createCodecRegistry } from '@prisma-next/sql-relational-core/ast';
|
|
31
35
|
import type {
|
|
32
36
|
AppliedMutationDefault,
|
|
37
|
+
CodecDescriptorRegistry,
|
|
33
38
|
ExecutionContext,
|
|
34
39
|
JsonSchemaValidateFn,
|
|
35
40
|
JsonSchemaValidatorRegistry,
|
|
36
41
|
MutationDefaultsOptions,
|
|
37
42
|
TypeHelperRegistry,
|
|
38
43
|
} from '@prisma-next/sql-relational-core/query-lane-context';
|
|
39
|
-
import { type as arktype } from 'arktype';
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Runtime parameterized codec descriptor.
|
|
43
|
-
* Provides validation schema and optional init hook for codecs that support type parameters.
|
|
44
|
-
* Used at runtime to validate typeParams and create type helpers.
|
|
45
47
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
+
* The unified `CodecDescriptor<P>` shape applied to parameterized codecs
|
|
49
|
+
* — `paramsSchema: StandardSchemaV1<P>` for JSON-boundary validation,
|
|
50
|
+
* `factory: (P) => (CodecInstanceContext) => Codec` for the curried higher-order codec.
|
|
51
|
+
* The factory is called once per `storage.types` instance (or once per
|
|
52
|
+
* inline-`typeParams` column); per-instance state lives in the closure.
|
|
53
|
+
*
|
|
54
|
+
* Codec-registry-unification spec § Decision.
|
|
48
55
|
*/
|
|
49
|
-
export type RuntimeParameterizedCodecDescriptor<
|
|
50
|
-
TParams = Record<string, unknown>,
|
|
51
|
-
THelper = unknown,
|
|
52
|
-
> = CodecParamsDescriptor<TParams, THelper>;
|
|
56
|
+
export type RuntimeParameterizedCodecDescriptor<P = Record<string, unknown>> = CodecDescriptor<P>;
|
|
53
57
|
|
|
54
58
|
export interface SqlStaticContributions {
|
|
55
59
|
readonly codecs: () => CodecRegistry;
|
|
56
60
|
// biome-ignore lint/suspicious/noExplicitAny: needed for covariance with concrete descriptor types
|
|
57
|
-
readonly parameterizedCodecs: () => ReadonlyArray<RuntimeParameterizedCodecDescriptor<any
|
|
61
|
+
readonly parameterizedCodecs: () => ReadonlyArray<RuntimeParameterizedCodecDescriptor<any>>;
|
|
58
62
|
readonly queryOperations?: () => ReadonlyArray<SqlOperationDescriptor>;
|
|
59
63
|
readonly mutationDefaultGenerators?: () => ReadonlyArray<RuntimeMutationDefaultGenerator>;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Scope across which a generator's value is constant.
|
|
68
|
+
*
|
|
69
|
+
* - `'field'` — one value per defaulting site (one column, one row).
|
|
70
|
+
* Cache strategy: no cache; call per defaulting site. Right for
|
|
71
|
+
* per-row identifiers (UUIDs, CUIDs, ULIDs, nanoid, ksuid).
|
|
72
|
+
* - `'row'` — one value across all defaulting sites of one row of one
|
|
73
|
+
* operation. Cache strategy: per-call cache keyed by `generatorId`.
|
|
74
|
+
* Right for correlation ids stamped into multiple columns of one row.
|
|
75
|
+
* - `'query'` — one value across all rows and columns of one ORM
|
|
76
|
+
* operation. Cache strategy: caller-provided cache keyed by
|
|
77
|
+
* `generatorId`. Right for `timestampNow` (a single timestamp per
|
|
78
|
+
* bulk insert/update).
|
|
79
|
+
*/
|
|
80
|
+
export type GeneratorStability = 'field' | 'row' | 'query';
|
|
81
|
+
|
|
62
82
|
export interface RuntimeMutationDefaultGenerator {
|
|
63
83
|
readonly id: string;
|
|
64
84
|
readonly generate: (params?: Record<string, unknown>) => unknown;
|
|
85
|
+
/**
|
|
86
|
+
* Scope across which the generator's value is constant. The framework
|
|
87
|
+
* derives the cache strategy from this declaration; generator authors
|
|
88
|
+
* never need to know about cache keys. See `GeneratorStability` for
|
|
89
|
+
* the per-value semantics.
|
|
90
|
+
*/
|
|
91
|
+
readonly stability: GeneratorStability;
|
|
65
92
|
}
|
|
66
93
|
|
|
67
94
|
export interface SqlRuntimeTargetDescriptor<
|
|
@@ -206,9 +233,16 @@ function validateTypeParams(
|
|
|
206
233
|
codecDescriptor: RuntimeParameterizedCodecDescriptor,
|
|
207
234
|
context: { typeName?: string; tableName?: string; columnName?: string },
|
|
208
235
|
): Record<string, unknown> {
|
|
209
|
-
const result = codecDescriptor.paramsSchema(typeParams);
|
|
210
|
-
if (result instanceof
|
|
211
|
-
|
|
236
|
+
const result = codecDescriptor.paramsSchema['~standard'].validate(typeParams);
|
|
237
|
+
if (result instanceof Promise) {
|
|
238
|
+
throw runtimeError(
|
|
239
|
+
'RUNTIME.TYPE_PARAMS_INVALID',
|
|
240
|
+
`paramsSchema for codec '${codecDescriptor.codecId}' returned a Promise; runtime validation requires a synchronous Standard Schema validator.`,
|
|
241
|
+
{ ...context, codecId: codecDescriptor.codecId, typeParams },
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (result.issues) {
|
|
245
|
+
const messages = result.issues.map((issue) => issue.message).join('; ');
|
|
212
246
|
const locationInfo = context.typeName
|
|
213
247
|
? `type '${context.typeName}'`
|
|
214
248
|
: `column '${context.tableName}.${context.columnName}'`;
|
|
@@ -218,7 +252,7 @@ function validateTypeParams(
|
|
|
218
252
|
{ ...context, codecId: codecDescriptor.codecId, typeParams },
|
|
219
253
|
);
|
|
220
254
|
}
|
|
221
|
-
return result as Record<string, unknown>;
|
|
255
|
+
return result.value as Record<string, unknown>;
|
|
222
256
|
}
|
|
223
257
|
|
|
224
258
|
function collectParameterizedCodecDescriptors(
|
|
@@ -242,32 +276,116 @@ function collectParameterizedCodecDescriptors(
|
|
|
242
276
|
return descriptors;
|
|
243
277
|
}
|
|
244
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Build the unified descriptor map. Combines parameterized descriptors
|
|
281
|
+
* (which already ship as `CodecDescriptor`s) with synthesized descriptors
|
|
282
|
+
* for non-parameterized codecs registered through the legacy `codecs:`
|
|
283
|
+
* slot. Codec ids that ship a parameterized descriptor take precedence —
|
|
284
|
+
* even when the legacy registry registers a representative codec under
|
|
285
|
+
* the same id, the parameterized descriptor is the authoritative source.
|
|
286
|
+
*
|
|
287
|
+
* Codec-registry-unification spec § Decision: every codec resolves
|
|
288
|
+
* through one descriptor map; reads are non-branching.
|
|
289
|
+
*/
|
|
290
|
+
function buildCodecDescriptorRegistry(
|
|
291
|
+
codecRegistry: CodecRegistry,
|
|
292
|
+
parameterizedDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
|
|
293
|
+
): CodecDescriptorRegistry {
|
|
294
|
+
type AnyDescriptor = CodecDescriptor<unknown>;
|
|
295
|
+
const byId = new Map<string, AnyDescriptor>();
|
|
296
|
+
const byTargetType = new Map<string, Array<AnyDescriptor>>();
|
|
297
|
+
|
|
298
|
+
function registerInIndices(descriptor: AnyDescriptor): void {
|
|
299
|
+
byId.set(descriptor.codecId, descriptor);
|
|
300
|
+
for (const targetType of descriptor.targetTypes) {
|
|
301
|
+
const list = byTargetType.get(targetType);
|
|
302
|
+
if (list) {
|
|
303
|
+
list.push(descriptor);
|
|
304
|
+
} else {
|
|
305
|
+
byTargetType.set(targetType, [descriptor]);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The descriptor map is heterogeneous in `P` — each codec id has its own
|
|
311
|
+
// params shape. The public `CodecDescriptorRegistry` interface widens to
|
|
312
|
+
// `CodecDescriptor<unknown>` and consumers narrow per codec id at the
|
|
313
|
+
// call site (the descriptor's `paramsSchema` validates JSON-sourced
|
|
314
|
+
// params before the factory ever sees them, so the runtime narrow is
|
|
315
|
+
// safe). The cast at registration goes through `unknown` because
|
|
316
|
+
// `CodecDescriptor<P>` is invariant in `P` (the `factory` and
|
|
317
|
+
// `renderOutputType` slots use `P` contravariantly).
|
|
318
|
+
for (const descriptor of parameterizedDescriptors.values()) {
|
|
319
|
+
registerInIndices(descriptor as unknown as AnyDescriptor);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const codec of codecRegistry.values()) {
|
|
323
|
+
if (byId.has(codec.id)) continue;
|
|
324
|
+
registerInIndices(synthesizeNonParameterizedDescriptor(codec) as unknown as AnyDescriptor);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
descriptorFor(codecId: string): AnyDescriptor | undefined {
|
|
329
|
+
return byId.get(codecId);
|
|
330
|
+
},
|
|
331
|
+
*values(): IterableIterator<AnyDescriptor> {
|
|
332
|
+
yield* byId.values();
|
|
333
|
+
},
|
|
334
|
+
byTargetType(targetType: string): readonly AnyDescriptor[] {
|
|
335
|
+
return byTargetType.get(targetType) ?? Object.freeze([]);
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function collectTypeRefSites(
|
|
341
|
+
storage: SqlStorage,
|
|
342
|
+
): Map<string, Array<{ readonly table: string; readonly column: string }>> {
|
|
343
|
+
const sites = new Map<string, Array<{ readonly table: string; readonly column: string }>>();
|
|
344
|
+
for (const [tableName, table] of Object.entries(storage.tables)) {
|
|
345
|
+
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
346
|
+
if (typeof column.typeRef !== 'string') continue;
|
|
347
|
+
const list = sites.get(column.typeRef);
|
|
348
|
+
const entry = { table: tableName, column: columnName };
|
|
349
|
+
if (list) {
|
|
350
|
+
list.push(entry);
|
|
351
|
+
} else {
|
|
352
|
+
sites.set(column.typeRef, [entry]);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return sites;
|
|
357
|
+
}
|
|
358
|
+
|
|
245
359
|
function initializeTypeHelpers(
|
|
246
|
-
|
|
360
|
+
storage: SqlStorage,
|
|
247
361
|
codecDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
|
|
248
362
|
): TypeHelperRegistry {
|
|
249
363
|
const helpers: TypeHelperRegistry = {};
|
|
364
|
+
const storageTypes = storage.types;
|
|
250
365
|
|
|
251
366
|
if (!storageTypes) {
|
|
252
367
|
return helpers;
|
|
253
368
|
}
|
|
254
369
|
|
|
370
|
+
const typeRefSites = collectTypeRefSites(storage);
|
|
371
|
+
|
|
255
372
|
for (const [typeName, typeInstance] of Object.entries(storageTypes)) {
|
|
256
373
|
const descriptor = codecDescriptors.get(typeInstance.codecId);
|
|
257
374
|
|
|
258
|
-
if (descriptor) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
if (descriptor.init) {
|
|
264
|
-
helpers[typeName] = descriptor.init(validatedParams);
|
|
265
|
-
} else {
|
|
266
|
-
helpers[typeName] = typeInstance;
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
375
|
+
if (!descriptor) {
|
|
376
|
+
// No parameterized descriptor for this codec id — store the raw
|
|
377
|
+
// type instance for callers that need typeParams metadata.
|
|
269
378
|
helpers[typeName] = typeInstance;
|
|
379
|
+
continue;
|
|
270
380
|
}
|
|
381
|
+
|
|
382
|
+
const validatedParams = validateTypeParams(typeInstance.typeParams, descriptor, {
|
|
383
|
+
typeName,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const usedAt = typeRefSites.get(typeName) ?? [];
|
|
387
|
+
const ctx: SqlCodecInstanceContext = { name: typeName, usedAt };
|
|
388
|
+
helpers[typeName] = descriptor.factory(validatedParams)(ctx);
|
|
271
389
|
}
|
|
272
390
|
|
|
273
391
|
return helpers;
|
|
@@ -290,67 +408,245 @@ function validateColumnTypeParams(
|
|
|
290
408
|
}
|
|
291
409
|
|
|
292
410
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
411
|
+
* View of a codec that exposes a per-instance JSON-schema `validate`
|
|
412
|
+
* function. Codecs declare this contract by including the
|
|
413
|
+
* `'json-validator'` `CodecTrait` in their `traits` array; the trait is
|
|
414
|
+
* the gate that lets `extractValidator` resolve from structurally-typed
|
|
415
|
+
* `unknown` to this typed view.
|
|
416
|
+
*/
|
|
417
|
+
type JsonValidatorCodec = {
|
|
418
|
+
readonly traits?: ReadonlyArray<unknown>;
|
|
419
|
+
readonly validate: JsonSchemaValidateFn;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
function hasJsonValidatorTrait(candidate: unknown): candidate is JsonValidatorCodec {
|
|
423
|
+
if (candidate === null || typeof candidate !== 'object') return false;
|
|
424
|
+
const traits = (candidate as { readonly traits?: unknown }).traits;
|
|
425
|
+
if (!Array.isArray(traits)) return false;
|
|
426
|
+
if (!traits.includes('json-validator')) return false;
|
|
427
|
+
const validate = (candidate as { readonly validate?: unknown }).validate;
|
|
428
|
+
return typeof validate === 'function';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function extractValidator(candidate: unknown): JsonSchemaValidateFn | undefined {
|
|
432
|
+
return hasJsonValidatorTrait(candidate) ? candidate.validate : undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function isResolvedCodec(candidate: unknown): candidate is Codec {
|
|
436
|
+
return (
|
|
437
|
+
candidate !== null &&
|
|
438
|
+
typeof candidate === 'object' &&
|
|
439
|
+
'id' in candidate &&
|
|
440
|
+
'decode' in candidate
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Walk the contract's `storage.tables[].columns[]` and resolve each
|
|
446
|
+
* column to a `Codec` through the unified descriptor map. Per-instance
|
|
447
|
+
* behavior:
|
|
295
448
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
449
|
+
* - **typeRef columns**: reuse the resolved codec materialized once by
|
|
450
|
+
* `initializeTypeHelpers` for the `storage.types` entry. Multiple
|
|
451
|
+
* columns sharing one typeRef share one codec instance.
|
|
452
|
+
* - **inline-typeParams columns**: call `descriptor.factory(typeParams)
|
|
453
|
+
* (ctx)` once per column (per-column anonymous instance).
|
|
454
|
+
* - **non-parameterized columns**: call `descriptor.factory()(ctx)`
|
|
455
|
+
* once. The synthesized descriptor's factory is constant — every call
|
|
456
|
+
* returns the same shared codec instance — so columns sharing a non-
|
|
457
|
+
* parameterized codec id share one resolved codec without explicit
|
|
458
|
+
* caching.
|
|
459
|
+
*
|
|
460
|
+
* Combines what `initializeTypeHelpers` (named-instance walk) and the
|
|
461
|
+
* old `buildJsonSchemaValidatorRegistry` (per-column walk) used to do
|
|
462
|
+
* separately: one walk over all columns, one resolved codec per column,
|
|
463
|
+
* one trait-gated validator extraction per column. The result drives
|
|
464
|
+
* both the dispatch registry (`ContractCodecRegistry.forColumn`) and the
|
|
465
|
+
* validator registry.
|
|
466
|
+
*
|
|
467
|
+
* Codec-registry-unification spec § AC-4: every column resolves through
|
|
468
|
+
* one descriptor map without branching on parameterization.
|
|
299
469
|
*/
|
|
300
|
-
function
|
|
470
|
+
function buildContractCodecRegistry(
|
|
301
471
|
contract: Contract<SqlStorage>,
|
|
472
|
+
codecDescriptors: CodecDescriptorRegistry,
|
|
473
|
+
legacyCodecRegistry: CodecRegistry,
|
|
302
474
|
types: TypeHelperRegistry,
|
|
303
|
-
|
|
304
|
-
):
|
|
475
|
+
parameterizedDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
|
|
476
|
+
): {
|
|
477
|
+
readonly registry: ContractCodecRegistry;
|
|
478
|
+
readonly jsonValidators: JsonSchemaValidatorRegistry | undefined;
|
|
479
|
+
} {
|
|
480
|
+
const byColumn = new Map<string, Codec>();
|
|
481
|
+
const byCodecId = new Map<string, Codec>();
|
|
482
|
+
// Codec ids whose `byCodecId` entry is ambiguous — multiple distinct
|
|
483
|
+
// resolved instances landed under the same parameterized codec id (e.g.
|
|
484
|
+
// `Vector<1024>` and `Vector<1536>` both registering under
|
|
485
|
+
// `pg/vector@1`). The encode-side `forCodecId` fallback rejects these
|
|
486
|
+
// ids so a DSL-param without a column ref cannot silently bind to the
|
|
487
|
+
// wrong instance. Retires when AC-5's `ParamRef.refs` plumbing lands
|
|
488
|
+
// (TML-2357).
|
|
489
|
+
const ambiguousCodecIds = new Set<string>();
|
|
305
490
|
const validators = new Map<string, JsonSchemaValidateFn>();
|
|
306
491
|
|
|
307
|
-
// Collect codec IDs that have init hooks (these produce { validate } helpers)
|
|
308
|
-
const codecIdsWithInit = new Set<string>();
|
|
309
|
-
for (const [codecId, descriptor] of codecDescriptors) {
|
|
310
|
-
if (descriptor.init) {
|
|
311
|
-
codecIdsWithInit.add(codecId);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (codecIdsWithInit.size === 0) {
|
|
316
|
-
return undefined;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
492
|
for (const [tableName, table] of Object.entries(contract.storage.tables)) {
|
|
320
493
|
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
494
|
+
const columnKey = `${tableName}.${columnName}`;
|
|
495
|
+
const descriptor = codecDescriptors.descriptorFor(column.codecId);
|
|
496
|
+
|
|
497
|
+
let resolvedCodec: Codec | undefined;
|
|
498
|
+
|
|
499
|
+
if (descriptor) {
|
|
500
|
+
const isParameterized = parameterizedDescriptors.has(column.codecId);
|
|
501
|
+
|
|
502
|
+
if (column.typeRef) {
|
|
503
|
+
// The named instance was already materialized once by
|
|
504
|
+
// `initializeTypeHelpers`; reuse it so multiple columns sharing
|
|
505
|
+
// the same typeRef share one codec instance (and any per-
|
|
506
|
+
// instance helper state on it).
|
|
507
|
+
const helper = types[column.typeRef];
|
|
508
|
+
if (isResolvedCodec(helper)) {
|
|
509
|
+
resolvedCodec = helper;
|
|
510
|
+
}
|
|
511
|
+
} else if (column.typeParams && isParameterized) {
|
|
512
|
+
const parameterizedDescriptor = parameterizedDescriptors.get(column.codecId);
|
|
513
|
+
if (parameterizedDescriptor) {
|
|
514
|
+
const validatedParams = validateTypeParams(column.typeParams, parameterizedDescriptor, {
|
|
515
|
+
tableName,
|
|
516
|
+
columnName,
|
|
517
|
+
});
|
|
518
|
+
const ctx: SqlCodecInstanceContext = {
|
|
519
|
+
name: `<anon:${tableName}.${columnName}>`,
|
|
520
|
+
usedAt: [{ table: tableName, column: columnName }],
|
|
521
|
+
};
|
|
522
|
+
resolvedCodec = parameterizedDescriptor.factory(validatedParams)(ctx);
|
|
523
|
+
}
|
|
524
|
+
} else if (!isParameterized) {
|
|
525
|
+
// Non-parameterized column. Cache the resolved codec by codec
|
|
526
|
+
// id — the synthesized descriptor's factory is constant for
|
|
527
|
+
// non-parameterized codecs, so columns sharing this codec id
|
|
528
|
+
// share one resolved instance.
|
|
529
|
+
let cached = byCodecId.get(column.codecId);
|
|
530
|
+
if (!cached) {
|
|
531
|
+
const ctx: SqlCodecInstanceContext = {
|
|
532
|
+
name: `<shared:${column.codecId}>`,
|
|
533
|
+
usedAt: [{ table: tableName, column: columnName }],
|
|
534
|
+
};
|
|
535
|
+
// `synthesizeNonParameterizedDescriptor` produces a
|
|
536
|
+
// `CodecDescriptor<void>` whose factory ignores its params
|
|
537
|
+
// and ctx; the runtime's `void` value is `undefined`. The
|
|
538
|
+
// structural cast goes through `unknown` to satisfy the
|
|
539
|
+
// heterogeneous-`P` registry boundary (the factory's
|
|
540
|
+
// declared `P` is `any` here; the consumer narrows per
|
|
541
|
+
// codec id). The cast narrows the descriptor's
|
|
542
|
+
// family-agnostic `CodecInstanceContext` slot to the SQL
|
|
543
|
+
// `SqlCodecInstanceContext` we pass at this call site —
|
|
544
|
+
// function-argument contravariance makes the narrow safe
|
|
545
|
+
// (a callee that accepts the base will also accept the
|
|
546
|
+
// SQL extension). Per spec § Non-functional constraints.
|
|
547
|
+
const voidFactory = descriptor.factory as unknown as (
|
|
548
|
+
params: undefined,
|
|
549
|
+
) => (ctx: SqlCodecInstanceContext) => Codec;
|
|
550
|
+
cached = voidFactory(undefined)(ctx);
|
|
551
|
+
byCodecId.set(column.codecId, cached);
|
|
552
|
+
}
|
|
553
|
+
resolvedCodec = cached;
|
|
330
554
|
}
|
|
331
|
-
|
|
555
|
+
// else: parameterized codec id with no typeRef and no typeParams
|
|
556
|
+
// — this is the legitimate "undimensioned" form for codecs that
|
|
557
|
+
// ship a no-params column variant alongside a parameterized one
|
|
558
|
+
// (e.g. pgvector's `vectorColumn` vs. `vector(N)`). Leave
|
|
559
|
+
// `resolvedCodec` undefined; encode/decode for this column flows
|
|
560
|
+
// through `forCodecId` (the AC-5-deferred carve-out documented
|
|
561
|
+
// in `relational-core/src/ast/codec-types.ts`). The fallback
|
|
562
|
+
// works for these cases because their wire format is
|
|
563
|
+
// params-independent (vector formats `[v1,v2,...]` regardless
|
|
564
|
+
// of declared length).
|
|
332
565
|
}
|
|
333
566
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
567
|
+
if (resolvedCodec) {
|
|
568
|
+
byColumn.set(columnKey, resolvedCodec);
|
|
569
|
+
const validate = extractValidator(resolvedCodec);
|
|
570
|
+
if (validate) {
|
|
571
|
+
validators.set(columnKey, validate);
|
|
572
|
+
}
|
|
573
|
+
const existing = byCodecId.get(column.codecId);
|
|
574
|
+
if (existing === undefined) {
|
|
575
|
+
byCodecId.set(column.codecId, resolvedCodec);
|
|
576
|
+
} else if (existing !== resolvedCodec && parameterizedDescriptors.has(column.codecId)) {
|
|
577
|
+
ambiguousCodecIds.add(column.codecId);
|
|
344
578
|
}
|
|
345
579
|
}
|
|
346
580
|
}
|
|
347
581
|
}
|
|
348
582
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
583
|
+
const registry: ContractCodecRegistry = {
|
|
584
|
+
forColumn(table, column) {
|
|
585
|
+
return byColumn.get(`${table}.${column}`);
|
|
586
|
+
},
|
|
587
|
+
forCodecId(codecId) {
|
|
588
|
+
// Codec-id-only fallback for sites without a column ref (encode-
|
|
589
|
+
// side DSL params whose `ParamRef.refs` isn't populated). Prefer
|
|
590
|
+
// the contract-walk-derived shared codec; fall back to the legacy
|
|
591
|
+
// `codecRegistry.get` for parameterized codec ids whose contracts
|
|
592
|
+
// don't have a typeRef/typeParams column the walk could resolve
|
|
593
|
+
// through. The legacy fallback retires once `ParamRef.refs` is
|
|
594
|
+
// threaded everywhere (TML-2357).
|
|
595
|
+
//
|
|
596
|
+
// Reject ambiguous parameterized fallbacks: if the contract walk
|
|
597
|
+
// resolved more than one distinct codec instance under this id
|
|
598
|
+
// (e.g. multiple vector dimensions, multiple arktype-json
|
|
599
|
+
// schemas), the codec-id-keyed lookup cannot honor the call site
|
|
600
|
+
// — fail fast rather than bind to whichever instance happened to
|
|
601
|
+
// land first.
|
|
602
|
+
if (ambiguousCodecIds.has(codecId)) {
|
|
603
|
+
throw runtimeError(
|
|
604
|
+
'RUNTIME.TYPE_PARAMS_INVALID',
|
|
605
|
+
`Codec '${codecId}' resolves to multiple parameterized instances; column-aware dispatch is required.`,
|
|
606
|
+
{ codecId },
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return byCodecId.get(codecId) ?? legacyCodecRegistry.get(codecId);
|
|
610
|
+
},
|
|
353
611
|
};
|
|
612
|
+
|
|
613
|
+
const jsonValidators: JsonSchemaValidatorRegistry | undefined =
|
|
614
|
+
validators.size > 0
|
|
615
|
+
? {
|
|
616
|
+
get: (key: string) => validators.get(key),
|
|
617
|
+
size: validators.size,
|
|
618
|
+
}
|
|
619
|
+
: undefined;
|
|
620
|
+
|
|
621
|
+
return { registry, jsonValidators };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function assertMutationDefaultGeneratorsAvailable(
|
|
625
|
+
contract: Contract<SqlStorage>,
|
|
626
|
+
generatorRegistry: ReadonlyMap<string, RuntimeMutationDefaultGenerator>,
|
|
627
|
+
): void {
|
|
628
|
+
const defaults = contract.execution?.mutations.defaults ?? [];
|
|
629
|
+
if (defaults.length === 0) return;
|
|
630
|
+
|
|
631
|
+
const missing = new Set<string>();
|
|
632
|
+
for (const mutationDefault of defaults) {
|
|
633
|
+
for (const phase of [mutationDefault.onCreate, mutationDefault.onUpdate]) {
|
|
634
|
+
if (!phase) continue;
|
|
635
|
+
if (phase.kind === 'generator' && !generatorRegistry.has(phase.id)) {
|
|
636
|
+
missing.add(phase.id);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (missing.size === 0) return;
|
|
642
|
+
|
|
643
|
+
const ids = Array.from(missing);
|
|
644
|
+
const idList = ids.map((id) => `'${id}'`).join(', ');
|
|
645
|
+
throw runtimeError(
|
|
646
|
+
'RUNTIME.MISSING_MUTATION_DEFAULT_GENERATOR',
|
|
647
|
+
`Contract requires mutation default generator(s) ${idList}, but no runtime component provides them.`,
|
|
648
|
+
{ ids },
|
|
649
|
+
);
|
|
354
650
|
}
|
|
355
651
|
|
|
356
652
|
function collectMutationDefaultGenerators(
|
|
@@ -414,8 +710,13 @@ function applyMutationDefaults(
|
|
|
414
710
|
return [];
|
|
415
711
|
}
|
|
416
712
|
|
|
713
|
+
const isEmptyUpdate = options.op === 'update' && Object.keys(options.values).length === 0;
|
|
714
|
+
|
|
417
715
|
const applied: AppliedMutationDefault[] = [];
|
|
418
716
|
const appliedColumns = new Set<string>();
|
|
717
|
+
// Fresh per-call cache for `stability: 'row'` generators — they share
|
|
718
|
+
// across columns of a single row but regenerate on the next call.
|
|
719
|
+
const rowCache = new Map<string, unknown>();
|
|
419
720
|
|
|
420
721
|
for (const mutationDefault of defaults) {
|
|
421
722
|
if (mutationDefault.ref.table !== options.table) {
|
|
@@ -428,6 +729,12 @@ function applyMutationDefaults(
|
|
|
428
729
|
continue;
|
|
429
730
|
}
|
|
430
731
|
|
|
732
|
+
// RD2: empty update payloads skip onUpdate defaults — no write means
|
|
733
|
+
// no `@updatedAt` advance.
|
|
734
|
+
if (isEmptyUpdate) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
431
738
|
const columnName = mutationDefault.ref.column;
|
|
432
739
|
if (Object.hasOwn(options.values, columnName) || appliedColumns.has(columnName)) {
|
|
433
740
|
continue;
|
|
@@ -435,7 +742,12 @@ function applyMutationDefaults(
|
|
|
435
742
|
|
|
436
743
|
applied.push({
|
|
437
744
|
column: columnName,
|
|
438
|
-
value:
|
|
745
|
+
value: resolveScopedValue(
|
|
746
|
+
defaultSpec,
|
|
747
|
+
generatorRegistry,
|
|
748
|
+
rowCache,
|
|
749
|
+
options.defaultValueCache,
|
|
750
|
+
),
|
|
439
751
|
});
|
|
440
752
|
appliedColumns.add(columnName);
|
|
441
753
|
}
|
|
@@ -443,6 +755,43 @@ function applyMutationDefaults(
|
|
|
443
755
|
return applied;
|
|
444
756
|
}
|
|
445
757
|
|
|
758
|
+
function resolveScopedValue(
|
|
759
|
+
spec: ExecutionMutationDefaultValue,
|
|
760
|
+
generatorRegistry: ReadonlyMap<string, RuntimeMutationDefaultGenerator>,
|
|
761
|
+
rowCache: Map<string, unknown>,
|
|
762
|
+
queryCache: Map<string, unknown> | undefined,
|
|
763
|
+
): unknown {
|
|
764
|
+
if (spec.kind !== 'generator') {
|
|
765
|
+
return computeExecutionDefaultValue(spec, generatorRegistry);
|
|
766
|
+
}
|
|
767
|
+
const generator = generatorRegistry.get(spec.id);
|
|
768
|
+
const cache = scopedCache(generator?.stability, rowCache, queryCache);
|
|
769
|
+
if (!cache) {
|
|
770
|
+
return computeExecutionDefaultValue(spec, generatorRegistry);
|
|
771
|
+
}
|
|
772
|
+
if (cache.has(spec.id)) {
|
|
773
|
+
return cache.get(spec.id);
|
|
774
|
+
}
|
|
775
|
+
const value = computeExecutionDefaultValue(spec, generatorRegistry);
|
|
776
|
+
cache.set(spec.id, value);
|
|
777
|
+
return value;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function scopedCache(
|
|
781
|
+
stability: GeneratorStability | undefined,
|
|
782
|
+
rowCache: Map<string, unknown>,
|
|
783
|
+
queryCache: Map<string, unknown> | undefined,
|
|
784
|
+
): Map<string, unknown> | undefined {
|
|
785
|
+
switch (stability) {
|
|
786
|
+
case 'row':
|
|
787
|
+
return rowCache;
|
|
788
|
+
case 'query':
|
|
789
|
+
return queryCache;
|
|
790
|
+
default:
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
446
795
|
export function createExecutionContext<
|
|
447
796
|
TContract extends Contract<SqlStorage> = Contract<SqlStorage>,
|
|
448
797
|
TTargetId extends string = string,
|
|
@@ -476,23 +825,33 @@ export function createExecutionContext<
|
|
|
476
825
|
}
|
|
477
826
|
|
|
478
827
|
const parameterizedCodecDescriptors = collectParameterizedCodecDescriptors(contributors);
|
|
828
|
+
const codecDescriptors = buildCodecDescriptorRegistry(
|
|
829
|
+
codecRegistry,
|
|
830
|
+
parameterizedCodecDescriptors,
|
|
831
|
+
);
|
|
479
832
|
const mutationDefaultGeneratorRegistry = collectMutationDefaultGenerators(contributors);
|
|
833
|
+
assertMutationDefaultGeneratorsAvailable(contract, mutationDefaultGeneratorRegistry);
|
|
480
834
|
|
|
481
835
|
if (parameterizedCodecDescriptors.size > 0) {
|
|
482
836
|
validateColumnTypeParams(contract.storage, parameterizedCodecDescriptors);
|
|
483
837
|
}
|
|
484
838
|
|
|
485
|
-
const types = initializeTypeHelpers(contract.storage
|
|
839
|
+
const types = initializeTypeHelpers(contract.storage, parameterizedCodecDescriptors);
|
|
486
840
|
|
|
487
|
-
const jsonSchemaValidators =
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
841
|
+
const { registry: contractCodecs, jsonValidators: jsonSchemaValidators } =
|
|
842
|
+
buildContractCodecRegistry(
|
|
843
|
+
contract,
|
|
844
|
+
codecDescriptors,
|
|
845
|
+
codecRegistry,
|
|
846
|
+
types,
|
|
847
|
+
parameterizedCodecDescriptors,
|
|
848
|
+
);
|
|
492
849
|
|
|
493
850
|
return {
|
|
494
851
|
contract,
|
|
495
852
|
codecs: codecRegistry,
|
|
853
|
+
contractCodecs,
|
|
854
|
+
codecDescriptors,
|
|
496
855
|
queryOperations: queryOperationRegistry,
|
|
497
856
|
types,
|
|
498
857
|
...(jsonSchemaValidators ? { jsonSchemaValidators } : {}),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { Contract
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type { ExecutionPlan } from '@prisma-next/framework-components/runtime';
|
|
2
3
|
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
3
|
-
import type { MarkerReader, RuntimeFamilyAdapter } from '@prisma-next/runtime-executor';
|
|
4
4
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
5
5
|
import type { AdapterProfile } from '@prisma-next/sql-relational-core/ast';
|
|
6
|
+
import type { MarkerReader, RuntimeFamilyAdapter } from './runtime-spi';
|
|
6
7
|
|
|
7
8
|
export class SqlFamilyAdapter<TContract extends Contract<SqlStorage>>
|
|
8
9
|
implements RuntimeFamilyAdapter<TContract>
|