@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.
Files changed (46) hide show
  1. package/README.md +31 -22
  2. package/dist/exports-BcX9wp4z.mjs +1640 -0
  3. package/dist/exports-BcX9wp4z.mjs.map +1 -0
  4. package/dist/{index-yb51L_1h.d.mts → index-DkthtnOX.d.mts} +100 -25
  5. package/dist/index-DkthtnOX.d.mts.map +1 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +2 -2
  8. package/dist/test/utils.d.mts +6 -5
  9. package/dist/test/utils.d.mts.map +1 -1
  10. package/dist/test/utils.mjs +13 -6
  11. package/dist/test/utils.mjs.map +1 -1
  12. package/package.json +13 -14
  13. package/src/codecs/decoding.ts +294 -173
  14. package/src/codecs/encoding.ts +162 -37
  15. package/src/codecs/validation.ts +22 -3
  16. package/src/content-hash.ts +44 -0
  17. package/src/exports/index.ts +12 -7
  18. package/src/fingerprint.ts +22 -0
  19. package/src/guardrails/raw.ts +165 -0
  20. package/src/lower-sql-plan.ts +3 -3
  21. package/src/marker.ts +75 -0
  22. package/src/middleware/before-compile-chain.ts +1 -0
  23. package/src/middleware/budgets.ts +26 -96
  24. package/src/middleware/lints.ts +3 -3
  25. package/src/middleware/sql-middleware.ts +6 -5
  26. package/src/runtime-spi.ts +44 -0
  27. package/src/sql-context.ts +438 -79
  28. package/src/sql-family-adapter.ts +3 -2
  29. package/src/sql-marker.ts +62 -47
  30. package/src/sql-runtime.ts +336 -113
  31. package/dist/exports-BQZSVXXt.mjs +0 -981
  32. package/dist/exports-BQZSVXXt.mjs.map +0 -1
  33. package/dist/index-yb51L_1h.d.mts.map +0 -1
  34. package/test/async-iterable-result.test.ts +0 -141
  35. package/test/before-compile-chain.test.ts +0 -223
  36. package/test/budgets.test.ts +0 -431
  37. package/test/context.types.test-d.ts +0 -68
  38. package/test/execution-stack.test.ts +0 -161
  39. package/test/json-schema-validation.test.ts +0 -571
  40. package/test/lints.test.ts +0 -160
  41. package/test/mutation-default-generators.test.ts +0 -254
  42. package/test/parameterized-types.test.ts +0 -529
  43. package/test/sql-context.test.ts +0 -384
  44. package/test/sql-family-adapter.test.ts +0 -103
  45. package/test/sql-runtime.test.ts +0 -792
  46. package/test/utils.ts +0 -297
@@ -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, StorageTypeInstance } from '@prisma-next/sql-contract/types';
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
- CodecParamsDescriptor,
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
- * This is a type alias for `CodecParamsDescriptor` from the AST layer,
47
- * which is the shared definition used by both adapter and runtime.
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, 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 arktype.errors) {
211
- const messages = result.map((p: { message: string }) => p.message).join('; ');
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
- storageTypes: Record<string, StorageTypeInstance> | undefined,
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
- const validatedParams = validateTypeParams(typeInstance.typeParams, descriptor, {
260
- typeName,
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
- * Builds a registry of compiled JSON Schema validators by scanning the contract
294
- * for columns whose codec descriptor provides an `init` hook returning `{ validate }`.
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
- * Handles both:
297
- * - Inline `typeParams.schema` on columns
298
- * - `typeRef` `storage.types[ref]` with init hook results already in `types` registry
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 buildJsonSchemaValidatorRegistry(
470
+ function buildContractCodecRegistry(
301
471
  contract: Contract<SqlStorage>,
472
+ codecDescriptors: CodecDescriptorRegistry,
473
+ legacyCodecRegistry: CodecRegistry,
302
474
  types: TypeHelperRegistry,
303
- codecDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
304
- ): JsonSchemaValidatorRegistry | undefined {
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
- if (!codecIdsWithInit.has(column.codecId)) continue;
322
-
323
- const key = `${tableName}.${columnName}`;
324
-
325
- // Case 1: column references a named type → validator already compiled via init hook
326
- if (column.typeRef) {
327
- const helper = types[column.typeRef] as { validate?: JsonSchemaValidateFn } | undefined;
328
- if (helper?.validate) {
329
- validators.set(key, helper.validate);
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
- continue;
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
- // Case 2: inline typeParams with schema → compile via init hook
335
- if (column.typeParams) {
336
- const descriptor = codecDescriptors.get(column.codecId);
337
- if (descriptor?.init) {
338
- const helper = descriptor.init(column.typeParams) as
339
- | { validate?: JsonSchemaValidateFn }
340
- | undefined;
341
- if (helper?.validate) {
342
- validators.set(key, helper.validate);
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
- if (validators.size === 0) return undefined;
350
- return {
351
- get: (key: string) => validators.get(key),
352
- size: validators.size,
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: computeExecutionDefaultValue(defaultSpec, generatorRegistry),
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.types, parameterizedCodecDescriptors);
839
+ const types = initializeTypeHelpers(contract.storage, parameterizedCodecDescriptors);
486
840
 
487
- const jsonSchemaValidators = buildJsonSchemaValidatorRegistry(
488
- contract,
489
- types,
490
- parameterizedCodecDescriptors,
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, ExecutionPlan } from '@prisma-next/contract/types';
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>