@prisma-next/sql-runtime 0.5.0-dev.7 → 0.5.0-dev.71

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 (48) hide show
  1. package/README.md +31 -22
  2. package/dist/exports-CXtbKm5q.mjs +1516 -0
  3. package/dist/exports-CXtbKm5q.mjs.map +1 -0
  4. package/dist/{index-yb51L_1h.d.mts → index-C4Dz0JKE.d.mts} +116 -45
  5. package/dist/index-C4Dz0JKE.d.mts.map +1 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +2 -3
  8. package/dist/test/utils.d.mts +38 -33
  9. package/dist/test/utils.d.mts.map +1 -1
  10. package/dist/test/utils.mjs +107 -56
  11. package/dist/test/utils.mjs.map +1 -1
  12. package/package.json +17 -18
  13. package/src/codecs/alias-resolver.ts +34 -0
  14. package/src/codecs/decoding.ts +263 -176
  15. package/src/codecs/encoding.ts +151 -38
  16. package/src/codecs/validation.ts +4 -4
  17. package/src/content-hash.ts +44 -0
  18. package/src/exports/index.ts +13 -7
  19. package/src/fingerprint.ts +22 -0
  20. package/src/guardrails/raw.ts +165 -0
  21. package/src/lower-sql-plan.ts +3 -3
  22. package/src/marker.ts +75 -0
  23. package/src/middleware/before-compile-chain.ts +1 -0
  24. package/src/middleware/budgets.ts +36 -120
  25. package/src/middleware/lints.ts +3 -3
  26. package/src/middleware/sql-middleware.ts +6 -5
  27. package/src/runtime-spi.ts +44 -0
  28. package/src/sql-context.ts +315 -105
  29. package/src/sql-family-adapter.ts +3 -2
  30. package/src/sql-marker.ts +89 -51
  31. package/src/sql-runtime.ts +305 -144
  32. package/dist/exports-BQZSVXXt.mjs +0 -981
  33. package/dist/exports-BQZSVXXt.mjs.map +0 -1
  34. package/dist/index-yb51L_1h.d.mts.map +0 -1
  35. package/src/codecs/json-schema-validation.ts +0 -61
  36. package/test/async-iterable-result.test.ts +0 -141
  37. package/test/before-compile-chain.test.ts +0 -223
  38. package/test/budgets.test.ts +0 -431
  39. package/test/context.types.test-d.ts +0 -68
  40. package/test/execution-stack.test.ts +0 -161
  41. package/test/json-schema-validation.test.ts +0 -571
  42. package/test/lints.test.ts +0 -160
  43. package/test/mutation-default-generators.test.ts +0 -254
  44. package/test/parameterized-types.test.ts +0 -529
  45. package/test/sql-context.test.ts +0 -384
  46. package/test/sql-family-adapter.test.ts +0 -103
  47. package/test/sql-runtime.test.ts +0 -792
  48. package/test/utils.ts +0 -297
@@ -1,4 +1,5 @@
1
1
  import type { Contract, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
2
+ import type { AnyCodecDescriptor, CodecDescriptor } from '@prisma-next/framework-components/codec';
2
3
  import type { ComponentDescriptor } from '@prisma-next/framework-components/components';
3
4
  import { checkContractComponentRequirements } from '@prisma-next/framework-components/components';
4
5
  import {
@@ -14,7 +15,7 @@ import {
14
15
  type RuntimeTargetInstance,
15
16
  } from '@prisma-next/framework-components/execution';
16
17
  import { runtimeError } from '@prisma-next/framework-components/runtime';
17
- import type { SqlStorage, StorageTypeInstance } from '@prisma-next/sql-contract/types';
18
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
18
19
  import {
19
20
  createSqlOperationRegistry,
20
21
  type SqlOperationDescriptor,
@@ -22,46 +23,55 @@ import {
22
23
  import type {
23
24
  Adapter,
24
25
  AnyQueryAst,
25
- CodecParamsDescriptor,
26
- CodecRegistry,
26
+ Codec,
27
+ ContractCodecRegistry,
27
28
  LoweredStatement,
29
+ SqlCodecInstanceContext,
28
30
  SqlDriver,
29
31
  } from '@prisma-next/sql-relational-core/ast';
30
- import { createCodecRegistry } from '@prisma-next/sql-relational-core/ast';
32
+ import { buildCodecDescriptorRegistry } from '@prisma-next/sql-relational-core/codec-descriptor-registry';
31
33
  import type {
32
34
  AppliedMutationDefault,
35
+ CodecDescriptorRegistry,
33
36
  ExecutionContext,
34
- JsonSchemaValidateFn,
35
- JsonSchemaValidatorRegistry,
36
37
  MutationDefaultsOptions,
37
38
  TypeHelperRegistry,
38
39
  } from '@prisma-next/sql-relational-core/query-lane-context';
39
- import { type as arktype } from 'arktype';
40
40
 
41
41
  /**
42
42
  * 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
43
  *
46
- * This is a type alias for `CodecParamsDescriptor` from the AST layer,
47
- * which is the shared definition used by both adapter and runtime.
44
+ * The unified `CodecDescriptor<P>` shape applied to parameterized codecs — `paramsSchema: StandardSchemaV1<P>` for JSON-boundary validation, `factory: (P) => (CodecInstanceContext) => Codec` for the curried higher-order codec. The factory is called once per `storage.types` instance (or once per inline-`typeParams` column); per-instance state lives in the closure.
45
+ *
46
+ * Codec-registry-unification spec § Decision.
48
47
  */
49
- export type RuntimeParameterizedCodecDescriptor<
50
- TParams = Record<string, unknown>,
51
- THelper = unknown,
52
- > = CodecParamsDescriptor<TParams, THelper>;
48
+ export type RuntimeParameterizedCodecDescriptor<P = Record<string, unknown>> = CodecDescriptor<P>;
53
49
 
50
+ /**
51
+ * Contributor protocol for SQL components (target, adapter, extension pack). The unified `codecs:` slot returns the full {@link CodecDescriptor} list — non-parameterized and parameterized descriptors live side-by-side in the same array. The framework dispatches every codec id through the unified descriptor map without branching on parameterization.
52
+ */
54
53
  export interface SqlStaticContributions {
55
- readonly codecs: () => CodecRegistry;
56
- // biome-ignore lint/suspicious/noExplicitAny: needed for covariance with concrete descriptor types
57
- readonly parameterizedCodecs: () => ReadonlyArray<RuntimeParameterizedCodecDescriptor<any, any>>;
54
+ readonly codecs: () => ReadonlyArray<AnyCodecDescriptor>;
58
55
  readonly queryOperations?: () => ReadonlyArray<SqlOperationDescriptor>;
59
56
  readonly mutationDefaultGenerators?: () => ReadonlyArray<RuntimeMutationDefaultGenerator>;
60
57
  }
61
58
 
59
+ /**
60
+ * Scope across which a generator's value is constant.
61
+ *
62
+ * - `'field'` — one value per defaulting site (one column, one row). Cache strategy: no cache; call per defaulting site. Right for per-row identifiers (UUIDs, CUIDs, ULIDs, nanoid, ksuid).
63
+ * - `'row'` — one value across all defaulting sites of one row of one operation. Cache strategy: per-call cache keyed by `generatorId`. Right for correlation ids stamped into multiple columns of one row.
64
+ * - `'query'` — one value across all rows and columns of one ORM operation. Cache strategy: caller-provided cache keyed by `generatorId`. Right for `timestampNow` (a single timestamp per bulk insert/update).
65
+ */
66
+ export type GeneratorStability = 'field' | 'row' | 'query';
67
+
62
68
  export interface RuntimeMutationDefaultGenerator {
63
69
  readonly id: string;
64
70
  readonly generate: (params?: Record<string, unknown>) => unknown;
71
+ /**
72
+ * Scope across which the generator's value is constant. The framework derives the cache strategy from this declaration; generator authors never need to know about cache keys. See `GeneratorStability` for the per-value semantics.
73
+ */
74
+ readonly stability: GeneratorStability;
65
75
  }
66
76
 
67
77
  export interface SqlRuntimeTargetDescriptor<
@@ -122,10 +132,7 @@ export type SqlRuntimeAdapterInstance<TTargetId extends string = string> = Runti
122
132
  Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
123
133
 
124
134
  /**
125
- * NOTE: Binding type is intentionally erased to unknown at this shared runtime layer.
126
- * Target clients (for example `postgres()`) validate and construct the concrete binding
127
- * before calling `driver.connect(binding)`, which keeps runtime behavior safe today.
128
- * A future follow-up can preserve TBinding through stack/context generics end-to-end.
135
+ * NOTE: Binding type is intentionally erased to unknown at this shared runtime layer. Target clients (for example `postgres()`) validate and construct the concrete binding before calling `driver.connect(binding)`, which keeps runtime behavior safe today. A future follow-up can preserve TBinding through stack/context generics end-to-end.
129
136
  */
130
137
  export type SqlRuntimeDriverInstance<TTargetId extends string = string> = RuntimeDriverInstance<
131
138
  'sql',
@@ -149,7 +156,7 @@ export function createSqlExecutionStack<TTargetId extends string>(options: {
149
156
  });
150
157
  }
151
158
 
152
- export type { ExecutionContext, JsonSchemaValidatorRegistry, TypeHelperRegistry };
159
+ export type { ExecutionContext, TypeHelperRegistry };
153
160
 
154
161
  export function assertExecutionStackContractRequirements(
155
162
  contract: Contract<SqlStorage>,
@@ -203,71 +210,117 @@ export function assertExecutionStackContractRequirements(
203
210
 
204
211
  function validateTypeParams(
205
212
  typeParams: Record<string, unknown>,
206
- codecDescriptor: RuntimeParameterizedCodecDescriptor,
213
+ descriptor: RuntimeParameterizedCodecDescriptor,
207
214
  context: { typeName?: string; tableName?: string; columnName?: string },
208
215
  ): 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('; ');
216
+ const result = descriptor.paramsSchema['~standard'].validate(typeParams);
217
+ if (result instanceof Promise) {
218
+ throw runtimeError(
219
+ 'RUNTIME.TYPE_PARAMS_INVALID',
220
+ `paramsSchema for codec '${descriptor.codecId}' returned a Promise; runtime validation requires a synchronous Standard Schema validator.`,
221
+ { ...context, codecId: descriptor.codecId, typeParams },
222
+ );
223
+ }
224
+ if (result.issues) {
225
+ const messages = result.issues.map((issue) => issue.message).join('; ');
212
226
  const locationInfo = context.typeName
213
227
  ? `type '${context.typeName}'`
214
228
  : `column '${context.tableName}.${context.columnName}'`;
215
229
  throw runtimeError(
216
230
  'RUNTIME.TYPE_PARAMS_INVALID',
217
- `Invalid typeParams for ${locationInfo} (codecId: ${codecDescriptor.codecId}): ${messages}`,
218
- { ...context, codecId: codecDescriptor.codecId, typeParams },
231
+ `Invalid typeParams for ${locationInfo} (codecId: ${descriptor.codecId}): ${messages}`,
232
+ { ...context, codecId: descriptor.codecId, typeParams },
219
233
  );
220
234
  }
221
- return result as Record<string, unknown>;
235
+ return result.value as Record<string, unknown>;
222
236
  }
223
237
 
224
- function collectParameterizedCodecDescriptors(
225
- contributors: ReadonlyArray<SqlStaticContributions>,
226
- ): Map<string, RuntimeParameterizedCodecDescriptor> {
227
- const descriptors = new Map<string, RuntimeParameterizedCodecDescriptor>();
238
+ /**
239
+ * Collect every {@link CodecDescriptor} contributed by the SQL stack and partition into "parameterized" vs "non-parameterized" via the descriptor's own {@link CodecDescriptorImpl.isParameterized} getter. The getter is the canonical discriminator — a `paramsSchema` identity check would misroute any descriptor that doesn't reuse the exact `voidParamsSchema` singleton (e.g. a non-parameterized codec authoring its own no-op schema).
240
+ *
241
+ * The unified descriptor list collapses the legacy split (a separate slot used to register parameterized codecs) — every codec id resolves through the same map (codec-registry-unification spec § Decision).
242
+ */
243
+ function collectCodecDescriptors(contributors: ReadonlyArray<SqlStaticContributions>): {
244
+ readonly all: ReadonlyArray<AnyCodecDescriptor>;
245
+ readonly parameterized: Map<string, RuntimeParameterizedCodecDescriptor>;
246
+ } {
247
+ const all: AnyCodecDescriptor[] = [];
248
+ const parameterized = new Map<string, RuntimeParameterizedCodecDescriptor>();
249
+ const seen = new Set<string>();
228
250
 
229
251
  for (const contributor of contributors) {
230
- for (const descriptor of contributor.parameterizedCodecs()) {
231
- if (descriptors.has(descriptor.codecId)) {
252
+ for (const descriptor of contributor.codecs()) {
253
+ if (seen.has(descriptor.codecId)) {
232
254
  throw runtimeError(
233
- 'RUNTIME.DUPLICATE_PARAMETERIZED_CODEC',
234
- `Duplicate parameterized codec descriptor for codecId '${descriptor.codecId}'.`,
255
+ 'RUNTIME.DUPLICATE_CODEC',
256
+ `Duplicate codec descriptor for codecId '${descriptor.codecId}'.`,
235
257
  { codecId: descriptor.codecId },
236
258
  );
237
259
  }
238
- descriptors.set(descriptor.codecId, descriptor);
260
+ seen.add(descriptor.codecId);
261
+ all.push(descriptor);
262
+
263
+ if (descriptor.isParameterized) {
264
+ // Cast widens the descriptor's heterogeneous `P` to the runtime alias surface; consumers narrow per codec id at the dispatch site, where the descriptor's own `paramsSchema` validates JSON-sourced params before the factory ever sees them.
265
+ parameterized.set(
266
+ descriptor.codecId,
267
+ descriptor as unknown as RuntimeParameterizedCodecDescriptor,
268
+ );
269
+ }
239
270
  }
240
271
  }
241
272
 
242
- return descriptors;
273
+ return { all, parameterized };
274
+ }
275
+
276
+ function collectTypeRefSites(
277
+ storage: SqlStorage,
278
+ ): Map<string, Array<{ readonly table: string; readonly column: string }>> {
279
+ const sites = new Map<string, Array<{ readonly table: string; readonly column: string }>>();
280
+ for (const [tableName, table] of Object.entries(storage.tables)) {
281
+ for (const [columnName, column] of Object.entries(table.columns)) {
282
+ if (typeof column.typeRef !== 'string') continue;
283
+ const list = sites.get(column.typeRef);
284
+ const entry = { table: tableName, column: columnName };
285
+ if (list) {
286
+ list.push(entry);
287
+ } else {
288
+ sites.set(column.typeRef, [entry]);
289
+ }
290
+ }
291
+ }
292
+ return sites;
243
293
  }
244
294
 
245
295
  function initializeTypeHelpers(
246
- storageTypes: Record<string, StorageTypeInstance> | undefined,
296
+ storage: SqlStorage,
247
297
  codecDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
248
298
  ): TypeHelperRegistry {
249
299
  const helpers: TypeHelperRegistry = {};
300
+ const storageTypes = storage.types;
250
301
 
251
302
  if (!storageTypes) {
252
303
  return helpers;
253
304
  }
254
305
 
306
+ const typeRefSites = collectTypeRefSites(storage);
307
+
255
308
  for (const [typeName, typeInstance] of Object.entries(storageTypes)) {
256
309
  const descriptor = codecDescriptors.get(typeInstance.codecId);
257
310
 
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 {
311
+ if (!descriptor) {
312
+ // No parameterized descriptor for this codec id — store the raw type instance for callers that need typeParams metadata.
269
313
  helpers[typeName] = typeInstance;
314
+ continue;
270
315
  }
316
+
317
+ const validatedParams = validateTypeParams(typeInstance.typeParams, descriptor, {
318
+ typeName,
319
+ });
320
+
321
+ const usedAt = typeRefSites.get(typeName) ?? [];
322
+ const ctx: SqlCodecInstanceContext = { name: typeName, usedAt };
323
+ helpers[typeName] = descriptor.factory(validatedParams)(ctx);
271
324
  }
272
325
 
273
326
  return helpers;
@@ -289,68 +342,177 @@ function validateColumnTypeParams(
289
342
  }
290
343
  }
291
344
 
345
+ function isResolvedCodec(candidate: unknown): candidate is Codec {
346
+ return (
347
+ candidate !== null &&
348
+ typeof candidate === 'object' &&
349
+ 'id' in candidate &&
350
+ 'decode' in candidate
351
+ );
352
+ }
353
+
292
354
  /**
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 }`.
355
+ * Walk the contract's `storage.tables[].columns[]` and resolve each column to a `Codec` through the unified descriptor map. Per-instance behavior:
356
+ *
357
+ * - **typeRef columns**: reuse the resolved codec materialized once by `initializeTypeHelpers` for the `storage.types` entry. Multiple columns sharing one typeRef share one codec instance.
358
+ * - **inline-typeParams columns**: call `descriptor.factory(typeParams) (ctx)` once per column (per-column anonymous instance).
359
+ * - **non-parameterized columns**: call `descriptor.factory()(ctx)` once. The synthesized descriptor's factory is constant — every call returns the same shared codec instance — so columns sharing a non-parameterized codec id share one resolved codec without explicit caching.
295
360
  *
296
- * Handles both:
297
- * - Inline `typeParams.schema` on columns
298
- * - `typeRef` → `storage.types[ref]` with init hook results already in `types` registry
361
+ * Codec-registry-unification spec § AC-4: every column resolves through one descriptor map without branching on parameterization. JSON-Schema validation, when required, lives inside the resolved codec's `decode` body (see `arktype-json`'s `ArktypeJsonCodecClass`); the framework no longer maintains a parallel validator registry.
299
362
  */
300
- function buildJsonSchemaValidatorRegistry(
363
+ function buildContractCodecRegistry(
301
364
  contract: Contract<SqlStorage>,
365
+ codecDescriptors: CodecDescriptorRegistry,
302
366
  types: TypeHelperRegistry,
303
- codecDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
304
- ): JsonSchemaValidatorRegistry | undefined {
305
- const validators = new Map<string, JsonSchemaValidateFn>();
306
-
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
- }
367
+ parameterizedDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
368
+ ): ContractCodecRegistry {
369
+ const byColumn = new Map<string, Codec>();
370
+ const byCodecId = new Map<string, Codec>();
371
+ // Codec ids whose `byCodecId` entry is ambiguous — multiple distinct resolved instances landed under the same parameterized codec id (e.g. `Vector<1024>` and `Vector<1536>` both registering under `pg/vector@1`). The refs-less `forCodecId` fallback rejects these ids so a DSL-param without a column ref cannot silently bind to the wrong instance. The validator pass enforces refs on every parameterized `ParamRef`, so this
372
+ // branch is reachable only as a defensive guard for non-parameterized columns whose `byCodecId` entry is unique by construction.
373
+ const ambiguousCodecIds = new Set<string>();
374
+
375
+ // Pre-populate `byCodecId` with non-parameterized descriptor instances. Refs-less encode/decode call sites (computed projections without a column ref, transient builder ParamRefs) resolve through `forCodecId(id)` and need a representative instance for codec ids that no contract column declares. Non-parameterized descriptors' factories are constant — every call yields the same shared codec — so a single materialization
376
+ // is correct.
377
+ for (const descriptor of codecDescriptors.values()) {
378
+ if (descriptor.isParameterized) continue;
379
+ const ctx: SqlCodecInstanceContext = {
380
+ name: `<shared:${descriptor.codecId}>`,
381
+ usedAt: [],
382
+ };
383
+ const voidFactory = descriptor.factory as unknown as (
384
+ params: undefined,
385
+ ) => (ctx: SqlCodecInstanceContext) => Codec;
386
+ byCodecId.set(descriptor.codecId, voidFactory(undefined)(ctx));
313
387
  }
314
388
 
315
- if (codecIdsWithInit.size === 0) {
316
- return undefined;
389
+ // Representative instances for parameterized descriptors whose factory tolerates `factory(undefined)` (e.g. pgvector — the factory ignores its params and returns the same shared codec). Used as the last-resort fallback in `forCodecId` for refs-less call sites whose codec id has no contract column the walk could resolve through (e.g. `cosineSimilarity(col, [literal])` builds an inline `ParamRef` without column refs).
390
+ // Stored separately so column-bound walk results don't trip the ambiguity check, and consulted only when `byCodecId` has no column-bound entry. Descriptors whose factory needs real params (arktype-json) raise and are skipped — the per-column dispatch path materializes those lazily when refs are populated.
391
+ const parameterizedRepresentatives = new Map<string, Codec>();
392
+ for (const descriptor of codecDescriptors.values()) {
393
+ if (!descriptor.isParameterized) continue;
394
+ const ctx: SqlCodecInstanceContext = {
395
+ name: `<shared:${descriptor.codecId}>`,
396
+ usedAt: [],
397
+ };
398
+ // Call `factory` *as a method on the descriptor* — `descriptor.factory(undefined)` — rather than detaching it into a local. Several descriptors implement `factory` as a class method whose body returns an arrow that captures `this` (`return () => new SomeCodec(this);`), and detaching loses that binding so the codec ends up with an `undefined` descriptor and `codec.id` throws.
399
+ const factory = descriptor.factory.bind(descriptor) as unknown as (
400
+ params: unknown,
401
+ ) => (ctx: SqlCodecInstanceContext) => Codec;
402
+ try {
403
+ parameterizedRepresentatives.set(descriptor.codecId, factory(undefined)(ctx));
404
+ } catch {
405
+ // Parameterized descriptor whose factory requires real params; refs-less fallback for this codec id is unavailable.
406
+ }
317
407
  }
318
408
 
319
409
  for (const [tableName, table] of Object.entries(contract.storage.tables)) {
320
410
  for (const [columnName, column] of Object.entries(table.columns)) {
321
- if (!codecIdsWithInit.has(column.codecId)) continue;
411
+ const columnKey = `${tableName}.${columnName}`;
412
+ const descriptor = codecDescriptors.descriptorFor(column.codecId);
322
413
 
323
- const key = `${tableName}.${columnName}`;
414
+ let resolvedCodec: Codec | undefined;
324
415
 
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);
416
+ if (descriptor) {
417
+ const isParameterized = parameterizedDescriptors.has(column.codecId);
418
+
419
+ if (column.typeRef) {
420
+ // The named instance was already materialized once by `initializeTypeHelpers`; reuse it so multiple columns sharing the same typeRef share one codec instance (and any per-instance helper state on it).
421
+ const helper = types[column.typeRef];
422
+ if (isResolvedCodec(helper)) {
423
+ resolvedCodec = helper;
424
+ }
425
+ } else if (column.typeParams && isParameterized) {
426
+ const parameterizedDescriptor = parameterizedDescriptors.get(column.codecId);
427
+ if (parameterizedDescriptor) {
428
+ const validatedParams = validateTypeParams(column.typeParams, parameterizedDescriptor, {
429
+ tableName,
430
+ columnName,
431
+ });
432
+ const ctx: SqlCodecInstanceContext = {
433
+ name: `<col:${tableName}.${columnName}>`,
434
+ usedAt: [{ table: tableName, column: columnName }],
435
+ };
436
+ resolvedCodec = parameterizedDescriptor.factory(validatedParams)(ctx);
437
+ }
438
+ } else if (!isParameterized) {
439
+ // Non-parameterized column: materialize a fresh codec instance per `forColumn(table, column)` entry with a column-specific `SqlCodecInstanceContext`. The pre-populated `byCodecId` representative (built with the synthetic `<shared:codecId>` context and empty `usedAt`) is reserved for `forCodecId()` refs-less fallbacks; reusing it for column-bound dispatch would erase per-column diagnostics for any descriptor whose factory reads `CodecInstanceContext`.
440
+ const ctx: SqlCodecInstanceContext = {
441
+ name: `<col:${tableName}.${columnName}>`,
442
+ usedAt: [{ table: tableName, column: columnName }],
443
+ };
444
+ // The descriptor's `P` is `void` for non-parameterized codecs; the runtime's `void` value is `undefined`. The cast narrows the descriptor's family-agnostic `CodecInstanceContext` slot to the SQL `SqlCodecInstanceContext` we pass at this call site — function-argument contravariance makes the narrow safe.
445
+ // `bind` preserves the `this`-on-descriptor invariant — several descriptors implement `factory` as a class method whose body returns an arrow that captures `this`; detaching loses the binding and produces a codec whose `descriptor` is `undefined`.
446
+ const voidFactory = descriptor.factory.bind(descriptor) as unknown as (
447
+ params: undefined,
448
+ ) => (ctx: SqlCodecInstanceContext) => Codec;
449
+ resolvedCodec = voidFactory(undefined)(ctx);
330
450
  }
331
- continue;
451
+ // else: parameterized codec id with no typeRef and no typeParams — this is the legitimate "undimensioned" form for codecs that ship a no-params column variant alongside a parameterized one (e.g. pgvector's `vectorColumn` vs. `vector(N)`). Leave `resolvedCodec` undefined; encode/decode for this column flows through `forCodecId`. The fallback works for these cases because their wire format is params-independent (vector
452
+ // formats `[v1,v2,...]` regardless of declared length).
332
453
  }
333
454
 
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
- }
455
+ if (resolvedCodec) {
456
+ byColumn.set(columnKey, resolvedCodec);
457
+ const existing = byCodecId.get(column.codecId);
458
+ if (existing === undefined) {
459
+ byCodecId.set(column.codecId, resolvedCodec);
460
+ } else if (existing !== resolvedCodec && parameterizedDescriptors.has(column.codecId)) {
461
+ ambiguousCodecIds.add(column.codecId);
344
462
  }
345
463
  }
346
464
  }
347
465
  }
348
466
 
349
- if (validators.size === 0) return undefined;
350
- return {
351
- get: (key: string) => validators.get(key),
352
- size: validators.size,
467
+ const registry: ContractCodecRegistry = {
468
+ forColumn(table, column) {
469
+ return byColumn.get(`${table}.${column}`);
470
+ },
471
+ forCodecId(codecId) {
472
+ // Codec-id-only fallback for refs-less sites. The validator pass (`validateParamRefRefs`) enforces refs on every parameterized `ParamRef` before encode, so this path is only legitimately reachable for non-parameterized codec ids. The map is pre-populated with every non-parameterized descriptor's canonical instance and overlaid with column-resolved instances from the contract walk; parameterized codec ids without a
473
+ // typeRef/typeParams-bound column never reach this map.
474
+ //
475
+ // Reject ambiguous parameterized fallbacks: if the contract walk resolved more than one distinct codec instance under this id (e.g. multiple vector dimensions, multiple arktype-json schemas), the codec-id-keyed lookup cannot honor the call site — fail fast rather than bind to whichever instance happened to land first.
476
+ if (ambiguousCodecIds.has(codecId)) {
477
+ throw runtimeError(
478
+ 'RUNTIME.TYPE_PARAMS_INVALID',
479
+ `Codec '${codecId}' resolves to multiple parameterized instances; column-aware dispatch is required.`,
480
+ { codecId },
481
+ );
482
+ }
483
+ return byCodecId.get(codecId) ?? parameterizedRepresentatives.get(codecId);
484
+ },
353
485
  };
486
+
487
+ return registry;
488
+ }
489
+
490
+ function assertMutationDefaultGeneratorsAvailable(
491
+ contract: Contract<SqlStorage>,
492
+ generatorRegistry: ReadonlyMap<string, RuntimeMutationDefaultGenerator>,
493
+ ): void {
494
+ const defaults = contract.execution?.mutations.defaults ?? [];
495
+ if (defaults.length === 0) return;
496
+
497
+ const missing = new Set<string>();
498
+ for (const mutationDefault of defaults) {
499
+ for (const phase of [mutationDefault.onCreate, mutationDefault.onUpdate]) {
500
+ if (!phase) continue;
501
+ if (phase.kind === 'generator' && !generatorRegistry.has(phase.id)) {
502
+ missing.add(phase.id);
503
+ }
504
+ }
505
+ }
506
+
507
+ if (missing.size === 0) return;
508
+
509
+ const ids = Array.from(missing);
510
+ const idList = ids.map((id) => `'${id}'`).join(', ');
511
+ throw runtimeError(
512
+ 'RUNTIME.MISSING_MUTATION_DEFAULT_GENERATOR',
513
+ `Contract requires mutation default generator(s) ${idList}, but no runtime component provides them.`,
514
+ { ids },
515
+ );
354
516
  }
355
517
 
356
518
  function collectMutationDefaultGenerators(
@@ -414,8 +576,12 @@ function applyMutationDefaults(
414
576
  return [];
415
577
  }
416
578
 
579
+ const isEmptyUpdate = options.op === 'update' && Object.keys(options.values).length === 0;
580
+
417
581
  const applied: AppliedMutationDefault[] = [];
418
582
  const appliedColumns = new Set<string>();
583
+ // Fresh per-call cache for `stability: 'row'` generators — they share across columns of a single row but regenerate on the next call.
584
+ const rowCache = new Map<string, unknown>();
419
585
 
420
586
  for (const mutationDefault of defaults) {
421
587
  if (mutationDefault.ref.table !== options.table) {
@@ -428,6 +594,11 @@ function applyMutationDefaults(
428
594
  continue;
429
595
  }
430
596
 
597
+ // RD2: empty update payloads skip onUpdate defaults — no write means no `@updatedAt` advance.
598
+ if (isEmptyUpdate) {
599
+ continue;
600
+ }
601
+
431
602
  const columnName = mutationDefault.ref.column;
432
603
  if (Object.hasOwn(options.values, columnName) || appliedColumns.has(columnName)) {
433
604
  continue;
@@ -435,7 +606,12 @@ function applyMutationDefaults(
435
606
 
436
607
  applied.push({
437
608
  column: columnName,
438
- value: computeExecutionDefaultValue(defaultSpec, generatorRegistry),
609
+ value: resolveScopedValue(
610
+ defaultSpec,
611
+ generatorRegistry,
612
+ rowCache,
613
+ options.defaultValueCache,
614
+ ),
439
615
  });
440
616
  appliedColumns.add(columnName);
441
617
  }
@@ -443,6 +619,43 @@ function applyMutationDefaults(
443
619
  return applied;
444
620
  }
445
621
 
622
+ function resolveScopedValue(
623
+ spec: ExecutionMutationDefaultValue,
624
+ generatorRegistry: ReadonlyMap<string, RuntimeMutationDefaultGenerator>,
625
+ rowCache: Map<string, unknown>,
626
+ queryCache: Map<string, unknown> | undefined,
627
+ ): unknown {
628
+ if (spec.kind !== 'generator') {
629
+ return computeExecutionDefaultValue(spec, generatorRegistry);
630
+ }
631
+ const generator = generatorRegistry.get(spec.id);
632
+ const cache = scopedCache(generator?.stability, rowCache, queryCache);
633
+ if (!cache) {
634
+ return computeExecutionDefaultValue(spec, generatorRegistry);
635
+ }
636
+ if (cache.has(spec.id)) {
637
+ return cache.get(spec.id);
638
+ }
639
+ const value = computeExecutionDefaultValue(spec, generatorRegistry);
640
+ cache.set(spec.id, value);
641
+ return value;
642
+ }
643
+
644
+ function scopedCache(
645
+ stability: GeneratorStability | undefined,
646
+ rowCache: Map<string, unknown>,
647
+ queryCache: Map<string, unknown> | undefined,
648
+ ): Map<string, unknown> | undefined {
649
+ switch (stability) {
650
+ case 'row':
651
+ return rowCache;
652
+ case 'query':
653
+ return queryCache;
654
+ default:
655
+ return undefined;
656
+ }
657
+ }
658
+
446
659
  export function createExecutionContext<
447
660
  TContract extends Contract<SqlStorage> = Contract<SqlStorage>,
448
661
  TTargetId extends string = string,
@@ -454,19 +667,14 @@ export function createExecutionContext<
454
667
 
455
668
  assertExecutionStackContractRequirements(contract, stack);
456
669
 
457
- const codecRegistry = createCodecRegistry();
458
-
459
670
  const contributors: Array<SqlStaticContributions & ComponentDescriptor<string>> = [
460
671
  stack.target,
461
672
  stack.adapter,
462
673
  ...stack.extensionPacks,
463
674
  ];
464
675
 
465
- for (const contributor of contributors) {
466
- for (const c of contributor.codecs().values()) {
467
- codecRegistry.register(c);
468
- }
469
- }
676
+ const { all: allCodecDescriptors, parameterized: parameterizedCodecDescriptors } =
677
+ collectCodecDescriptors(contributors);
470
678
 
471
679
  const queryOperationRegistry = createSqlOperationRegistry();
472
680
  for (const contributor of contributors) {
@@ -475,27 +683,29 @@ export function createExecutionContext<
475
683
  }
476
684
  }
477
685
 
478
- const parameterizedCodecDescriptors = collectParameterizedCodecDescriptors(contributors);
686
+ const codecDescriptors = buildCodecDescriptorRegistry(allCodecDescriptors);
479
687
  const mutationDefaultGeneratorRegistry = collectMutationDefaultGenerators(contributors);
688
+ assertMutationDefaultGeneratorsAvailable(contract, mutationDefaultGeneratorRegistry);
480
689
 
481
690
  if (parameterizedCodecDescriptors.size > 0) {
482
691
  validateColumnTypeParams(contract.storage, parameterizedCodecDescriptors);
483
692
  }
484
693
 
485
- const types = initializeTypeHelpers(contract.storage.types, parameterizedCodecDescriptors);
694
+ const types = initializeTypeHelpers(contract.storage, parameterizedCodecDescriptors);
486
695
 
487
- const jsonSchemaValidators = buildJsonSchemaValidatorRegistry(
696
+ const contractCodecs = buildContractCodecRegistry(
488
697
  contract,
698
+ codecDescriptors,
489
699
  types,
490
700
  parameterizedCodecDescriptors,
491
701
  );
492
702
 
493
703
  return {
494
704
  contract,
495
- codecs: codecRegistry,
705
+ contractCodecs,
706
+ codecDescriptors,
496
707
  queryOperations: queryOperationRegistry,
497
708
  types,
498
- ...(jsonSchemaValidators ? { jsonSchemaValidators } : {}),
499
709
  applyMutationDefaults: (options) =>
500
710
  applyMutationDefaults(contract, mutationDefaultGeneratorRegistry, options),
501
711
  };
@@ -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>