@prisma-next/sql-runtime 0.5.0-dev.60 → 0.5.0-dev.62

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.
@@ -1,6 +1,5 @@
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
+ import type { AnyCodecDescriptor, CodecDescriptor } from '@prisma-next/framework-components/codec';
4
3
  import type { ComponentDescriptor } from '@prisma-next/framework-components/components';
5
4
  import { checkContractComponentRequirements } from '@prisma-next/framework-components/components';
6
5
  import {
@@ -25,19 +24,16 @@ import type {
25
24
  Adapter,
26
25
  AnyQueryAst,
27
26
  Codec,
28
- CodecRegistry,
29
27
  ContractCodecRegistry,
30
28
  LoweredStatement,
31
29
  SqlCodecInstanceContext,
32
30
  SqlDriver,
33
31
  } from '@prisma-next/sql-relational-core/ast';
34
- import { createCodecRegistry } from '@prisma-next/sql-relational-core/ast';
32
+ import { buildCodecDescriptorRegistry } from '@prisma-next/sql-relational-core/codec-descriptor-registry';
35
33
  import type {
36
34
  AppliedMutationDefault,
37
35
  CodecDescriptorRegistry,
38
36
  ExecutionContext,
39
- JsonSchemaValidateFn,
40
- JsonSchemaValidatorRegistry,
41
37
  MutationDefaultsOptions,
42
38
  TypeHelperRegistry,
43
39
  } from '@prisma-next/sql-relational-core/query-lane-context';
@@ -45,20 +41,17 @@ import type {
45
41
  /**
46
42
  * Runtime parameterized codec descriptor.
47
43
  *
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.
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.
53
45
  *
54
46
  * Codec-registry-unification spec § Decision.
55
47
  */
56
48
  export type RuntimeParameterizedCodecDescriptor<P = Record<string, unknown>> = CodecDescriptor<P>;
57
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
+ */
58
53
  export interface SqlStaticContributions {
59
- readonly codecs: () => CodecRegistry;
60
- // biome-ignore lint/suspicious/noExplicitAny: needed for covariance with concrete descriptor types
61
- readonly parameterizedCodecs: () => ReadonlyArray<RuntimeParameterizedCodecDescriptor<any>>;
54
+ readonly codecs: () => ReadonlyArray<AnyCodecDescriptor>;
62
55
  readonly queryOperations?: () => ReadonlyArray<SqlOperationDescriptor>;
63
56
  readonly mutationDefaultGenerators?: () => ReadonlyArray<RuntimeMutationDefaultGenerator>;
64
57
  }
@@ -66,16 +59,9 @@ export interface SqlStaticContributions {
66
59
  /**
67
60
  * Scope across which a generator's value is constant.
68
61
  *
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).
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).
79
65
  */
80
66
  export type GeneratorStability = 'field' | 'row' | 'query';
81
67
 
@@ -83,10 +69,7 @@ export interface RuntimeMutationDefaultGenerator {
83
69
  readonly id: string;
84
70
  readonly generate: (params?: Record<string, unknown>) => unknown;
85
71
  /**
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.
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.
90
73
  */
91
74
  readonly stability: GeneratorStability;
92
75
  }
@@ -149,10 +132,7 @@ export type SqlRuntimeAdapterInstance<TTargetId extends string = string> = Runti
149
132
  Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
150
133
 
151
134
  /**
152
- * NOTE: Binding type is intentionally erased to unknown at this shared runtime layer.
153
- * Target clients (for example `postgres()`) validate and construct the concrete binding
154
- * before calling `driver.connect(binding)`, which keeps runtime behavior safe today.
155
- * 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.
156
136
  */
157
137
  export type SqlRuntimeDriverInstance<TTargetId extends string = string> = RuntimeDriverInstance<
158
138
  'sql',
@@ -176,7 +156,7 @@ export function createSqlExecutionStack<TTargetId extends string>(options: {
176
156
  });
177
157
  }
178
158
 
179
- export type { ExecutionContext, JsonSchemaValidatorRegistry, TypeHelperRegistry };
159
+ export type { ExecutionContext, TypeHelperRegistry };
180
160
 
181
161
  export function assertExecutionStackContractRequirements(
182
162
  contract: Contract<SqlStorage>,
@@ -230,15 +210,15 @@ export function assertExecutionStackContractRequirements(
230
210
 
231
211
  function validateTypeParams(
232
212
  typeParams: Record<string, unknown>,
233
- codecDescriptor: RuntimeParameterizedCodecDescriptor,
213
+ descriptor: RuntimeParameterizedCodecDescriptor,
234
214
  context: { typeName?: string; tableName?: string; columnName?: string },
235
215
  ): Record<string, unknown> {
236
- const result = codecDescriptor.paramsSchema['~standard'].validate(typeParams);
216
+ const result = descriptor.paramsSchema['~standard'].validate(typeParams);
237
217
  if (result instanceof Promise) {
238
218
  throw runtimeError(
239
219
  '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 },
220
+ `paramsSchema for codec '${descriptor.codecId}' returned a Promise; runtime validation requires a synchronous Standard Schema validator.`,
221
+ { ...context, codecId: descriptor.codecId, typeParams },
242
222
  );
243
223
  }
244
224
  if (result.issues) {
@@ -248,93 +228,49 @@ function validateTypeParams(
248
228
  : `column '${context.tableName}.${context.columnName}'`;
249
229
  throw runtimeError(
250
230
  'RUNTIME.TYPE_PARAMS_INVALID',
251
- `Invalid typeParams for ${locationInfo} (codecId: ${codecDescriptor.codecId}): ${messages}`,
252
- { ...context, codecId: codecDescriptor.codecId, typeParams },
231
+ `Invalid typeParams for ${locationInfo} (codecId: ${descriptor.codecId}): ${messages}`,
232
+ { ...context, codecId: descriptor.codecId, typeParams },
253
233
  );
254
234
  }
255
235
  return result.value as Record<string, unknown>;
256
236
  }
257
237
 
258
- function collectParameterizedCodecDescriptors(
259
- contributors: ReadonlyArray<SqlStaticContributions>,
260
- ): Map<string, RuntimeParameterizedCodecDescriptor> {
261
- 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>();
262
250
 
263
251
  for (const contributor of contributors) {
264
- for (const descriptor of contributor.parameterizedCodecs()) {
265
- if (descriptors.has(descriptor.codecId)) {
252
+ for (const descriptor of contributor.codecs()) {
253
+ if (seen.has(descriptor.codecId)) {
266
254
  throw runtimeError(
267
- 'RUNTIME.DUPLICATE_PARAMETERIZED_CODEC',
268
- `Duplicate parameterized codec descriptor for codecId '${descriptor.codecId}'.`,
255
+ 'RUNTIME.DUPLICATE_CODEC',
256
+ `Duplicate codec descriptor for codecId '${descriptor.codecId}'.`,
269
257
  { codecId: descriptor.codecId },
270
258
  );
271
259
  }
272
- descriptors.set(descriptor.codecId, descriptor);
273
- }
274
- }
275
-
276
- return descriptors;
277
- }
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]);
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
+ );
306
269
  }
307
270
  }
308
271
  }
309
272
 
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
- };
273
+ return { all, parameterized };
338
274
  }
339
275
 
340
276
  function collectTypeRefSites(
@@ -373,8 +309,7 @@ function initializeTypeHelpers(
373
309
  const descriptor = codecDescriptors.get(typeInstance.codecId);
374
310
 
375
311
  if (!descriptor) {
376
- // No parameterized descriptor for this codec id — store the raw
377
- // type instance for callers that need typeParams metadata.
312
+ // No parameterized descriptor for this codec id — store the raw type instance for callers that need typeParams metadata.
378
313
  helpers[typeName] = typeInstance;
379
314
  continue;
380
315
  }
@@ -407,31 +342,6 @@ function validateColumnTypeParams(
407
342
  }
408
343
  }
409
344
 
410
- /**
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
345
  function isResolvedCodec(candidate: unknown): candidate is Codec {
436
346
  return (
437
347
  candidate !== null &&
@@ -442,52 +352,59 @@ function isResolvedCodec(candidate: unknown): candidate is Codec {
442
352
  }
443
353
 
444
354
  /**
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:
355
+ * Walk the contract's `storage.tables[].columns[]` and resolve each column to a `Codec` through the unified descriptor map. Per-instance behavior:
448
356
  *
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.
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.
459
360
  *
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.
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.
469
362
  */
470
363
  function buildContractCodecRegistry(
471
364
  contract: Contract<SqlStorage>,
472
365
  codecDescriptors: CodecDescriptorRegistry,
473
- legacyCodecRegistry: CodecRegistry,
474
366
  types: TypeHelperRegistry,
475
367
  parameterizedDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
476
- ): {
477
- readonly registry: ContractCodecRegistry;
478
- readonly jsonValidators: JsonSchemaValidatorRegistry | undefined;
479
- } {
368
+ ): ContractCodecRegistry {
480
369
  const byColumn = new Map<string, Codec>();
481
370
  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).
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.
489
373
  const ambiguousCodecIds = new Set<string>();
490
- const validators = new Map<string, JsonSchemaValidateFn>();
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));
387
+ }
388
+
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
+ }
407
+ }
491
408
 
492
409
  for (const [tableName, table] of Object.entries(contract.storage.tables)) {
493
410
  for (const [columnName, column] of Object.entries(table.columns)) {
@@ -500,10 +417,7 @@ function buildContractCodecRegistry(
500
417
  const isParameterized = parameterizedDescriptors.has(column.codecId);
501
418
 
502
419
  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).
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).
507
421
  const helper = types[column.typeRef];
508
422
  if (isResolvedCodec(helper)) {
509
423
  resolvedCodec = helper;
@@ -516,60 +430,30 @@ function buildContractCodecRegistry(
516
430
  columnName,
517
431
  });
518
432
  const ctx: SqlCodecInstanceContext = {
519
- name: `<anon:${tableName}.${columnName}>`,
433
+ name: `<col:${tableName}.${columnName}>`,
520
434
  usedAt: [{ table: tableName, column: columnName }],
521
435
  };
522
436
  resolvedCodec = parameterizedDescriptor.factory(validatedParams)(ctx);
523
437
  }
524
438
  } 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;
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);
554
450
  }
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).
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).
565
453
  }
566
454
 
567
455
  if (resolvedCodec) {
568
456
  byColumn.set(columnKey, resolvedCodec);
569
- const validate = extractValidator(resolvedCodec);
570
- if (validate) {
571
- validators.set(columnKey, validate);
572
- }
573
457
  const existing = byCodecId.get(column.codecId);
574
458
  if (existing === undefined) {
575
459
  byCodecId.set(column.codecId, resolvedCodec);
@@ -585,20 +469,10 @@ function buildContractCodecRegistry(
585
469
  return byColumn.get(`${table}.${column}`);
586
470
  },
587
471
  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).
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.
595
474
  //
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.
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.
602
476
  if (ambiguousCodecIds.has(codecId)) {
603
477
  throw runtimeError(
604
478
  'RUNTIME.TYPE_PARAMS_INVALID',
@@ -606,19 +480,11 @@ function buildContractCodecRegistry(
606
480
  { codecId },
607
481
  );
608
482
  }
609
- return byCodecId.get(codecId) ?? legacyCodecRegistry.get(codecId);
483
+ return byCodecId.get(codecId) ?? parameterizedRepresentatives.get(codecId);
610
484
  },
611
485
  };
612
486
 
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 };
487
+ return registry;
622
488
  }
623
489
 
624
490
  function assertMutationDefaultGeneratorsAvailable(
@@ -714,8 +580,7 @@ function applyMutationDefaults(
714
580
 
715
581
  const applied: AppliedMutationDefault[] = [];
716
582
  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.
583
+ // Fresh per-call cache for `stability: 'row'` generators — they share across columns of a single row but regenerate on the next call.
719
584
  const rowCache = new Map<string, unknown>();
720
585
 
721
586
  for (const mutationDefault of defaults) {
@@ -729,8 +594,7 @@ function applyMutationDefaults(
729
594
  continue;
730
595
  }
731
596
 
732
- // RD2: empty update payloads skip onUpdate defaults — no write means
733
- // no `@updatedAt` advance.
597
+ // RD2: empty update payloads skip onUpdate defaults — no write means no `@updatedAt` advance.
734
598
  if (isEmptyUpdate) {
735
599
  continue;
736
600
  }
@@ -803,19 +667,14 @@ export function createExecutionContext<
803
667
 
804
668
  assertExecutionStackContractRequirements(contract, stack);
805
669
 
806
- const codecRegistry = createCodecRegistry();
807
-
808
670
  const contributors: Array<SqlStaticContributions & ComponentDescriptor<string>> = [
809
671
  stack.target,
810
672
  stack.adapter,
811
673
  ...stack.extensionPacks,
812
674
  ];
813
675
 
814
- for (const contributor of contributors) {
815
- for (const c of contributor.codecs().values()) {
816
- codecRegistry.register(c);
817
- }
818
- }
676
+ const { all: allCodecDescriptors, parameterized: parameterizedCodecDescriptors } =
677
+ collectCodecDescriptors(contributors);
819
678
 
820
679
  const queryOperationRegistry = createSqlOperationRegistry();
821
680
  for (const contributor of contributors) {
@@ -824,11 +683,7 @@ export function createExecutionContext<
824
683
  }
825
684
  }
826
685
 
827
- const parameterizedCodecDescriptors = collectParameterizedCodecDescriptors(contributors);
828
- const codecDescriptors = buildCodecDescriptorRegistry(
829
- codecRegistry,
830
- parameterizedCodecDescriptors,
831
- );
686
+ const codecDescriptors = buildCodecDescriptorRegistry(allCodecDescriptors);
832
687
  const mutationDefaultGeneratorRegistry = collectMutationDefaultGenerators(contributors);
833
688
  assertMutationDefaultGeneratorsAvailable(contract, mutationDefaultGeneratorRegistry);
834
689
 
@@ -838,23 +693,19 @@ export function createExecutionContext<
838
693
 
839
694
  const types = initializeTypeHelpers(contract.storage, parameterizedCodecDescriptors);
840
695
 
841
- const { registry: contractCodecs, jsonValidators: jsonSchemaValidators } =
842
- buildContractCodecRegistry(
843
- contract,
844
- codecDescriptors,
845
- codecRegistry,
846
- types,
847
- parameterizedCodecDescriptors,
848
- );
696
+ const contractCodecs = buildContractCodecRegistry(
697
+ contract,
698
+ codecDescriptors,
699
+ types,
700
+ parameterizedCodecDescriptors,
701
+ );
849
702
 
850
703
  return {
851
704
  contract,
852
- codecs: codecRegistry,
853
705
  contractCodecs,
854
706
  codecDescriptors,
855
707
  queryOperations: queryOperationRegistry,
856
708
  types,
857
- ...(jsonSchemaValidators ? { jsonSchemaValidators } : {}),
858
709
  applyMutationDefaults: (options) =>
859
710
  applyMutationDefaults(contract, mutationDefaultGeneratorRegistry, options),
860
711
  };