@prisma-next/sql-runtime 0.5.0-dev.3 → 0.5.0-dev.31

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 (45) hide show
  1. package/README.md +29 -21
  2. package/dist/exports-CrHMfIKo.mjs +1564 -0
  3. package/dist/exports-CrHMfIKo.mjs.map +1 -0
  4. package/dist/{index-yb51L_1h.d.mts → index-_dXSGeho.d.mts} +78 -25
  5. package/dist/index-_dXSGeho.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 +11 -5
  11. package/dist/test/utils.mjs.map +1 -1
  12. package/package.json +11 -13
  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/exports/index.ts +11 -7
  17. package/src/fingerprint.ts +22 -0
  18. package/src/guardrails/raw.ts +165 -0
  19. package/src/lower-sql-plan.ts +3 -3
  20. package/src/marker.ts +75 -0
  21. package/src/middleware/before-compile-chain.ts +1 -0
  22. package/src/middleware/budgets.ts +26 -96
  23. package/src/middleware/lints.ts +3 -3
  24. package/src/middleware/sql-middleware.ts +6 -5
  25. package/src/runtime-spi.ts +44 -0
  26. package/src/sql-context.ts +332 -78
  27. package/src/sql-family-adapter.ts +3 -2
  28. package/src/sql-marker.ts +62 -47
  29. package/src/sql-runtime.ts +332 -113
  30. package/dist/exports-BQZSVXXt.mjs +0 -981
  31. package/dist/exports-BQZSVXXt.mjs.map +0 -1
  32. package/dist/index-yb51L_1h.d.mts.map +0 -1
  33. package/test/async-iterable-result.test.ts +0 -141
  34. package/test/before-compile-chain.test.ts +0 -223
  35. package/test/budgets.test.ts +0 -431
  36. package/test/context.types.test-d.ts +0 -68
  37. package/test/execution-stack.test.ts +0 -161
  38. package/test/json-schema-validation.test.ts +0 -571
  39. package/test/lints.test.ts +0 -160
  40. package/test/mutation-default-generators.test.ts +0 -254
  41. package/test/parameterized-types.test.ts +0 -529
  42. package/test/sql-context.test.ts +0 -384
  43. package/test/sql-family-adapter.test.ts +0 -103
  44. package/test/sql-runtime.test.ts +0 -792
  45. 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,39 +24,41 @@ 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
  }
@@ -206,9 +210,16 @@ function validateTypeParams(
206
210
  codecDescriptor: RuntimeParameterizedCodecDescriptor,
207
211
  context: { typeName?: string; tableName?: string; columnName?: string },
208
212
  ): 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('; ');
213
+ const result = codecDescriptor.paramsSchema['~standard'].validate(typeParams);
214
+ if (result instanceof Promise) {
215
+ throw runtimeError(
216
+ 'RUNTIME.TYPE_PARAMS_INVALID',
217
+ `paramsSchema for codec '${codecDescriptor.codecId}' returned a Promise; runtime validation requires a synchronous Standard Schema validator.`,
218
+ { ...context, codecId: codecDescriptor.codecId, typeParams },
219
+ );
220
+ }
221
+ if (result.issues) {
222
+ const messages = result.issues.map((issue) => issue.message).join('; ');
212
223
  const locationInfo = context.typeName
213
224
  ? `type '${context.typeName}'`
214
225
  : `column '${context.tableName}.${context.columnName}'`;
@@ -218,7 +229,7 @@ function validateTypeParams(
218
229
  { ...context, codecId: codecDescriptor.codecId, typeParams },
219
230
  );
220
231
  }
221
- return result as Record<string, unknown>;
232
+ return result.value as Record<string, unknown>;
222
233
  }
223
234
 
224
235
  function collectParameterizedCodecDescriptors(
@@ -242,32 +253,116 @@ function collectParameterizedCodecDescriptors(
242
253
  return descriptors;
243
254
  }
244
255
 
256
+ /**
257
+ * Build the unified descriptor map. Combines parameterized descriptors
258
+ * (which already ship as `CodecDescriptor`s) with synthesized descriptors
259
+ * for non-parameterized codecs registered through the legacy `codecs:`
260
+ * slot. Codec ids that ship a parameterized descriptor take precedence —
261
+ * even when the legacy registry registers a representative codec under
262
+ * the same id, the parameterized descriptor is the authoritative source.
263
+ *
264
+ * Codec-registry-unification spec § Decision: every codec resolves
265
+ * through one descriptor map; reads are non-branching.
266
+ */
267
+ function buildCodecDescriptorRegistry(
268
+ codecRegistry: CodecRegistry,
269
+ parameterizedDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
270
+ ): CodecDescriptorRegistry {
271
+ type AnyDescriptor = CodecDescriptor<unknown>;
272
+ const byId = new Map<string, AnyDescriptor>();
273
+ const byTargetType = new Map<string, Array<AnyDescriptor>>();
274
+
275
+ function registerInIndices(descriptor: AnyDescriptor): void {
276
+ byId.set(descriptor.codecId, descriptor);
277
+ for (const targetType of descriptor.targetTypes) {
278
+ const list = byTargetType.get(targetType);
279
+ if (list) {
280
+ list.push(descriptor);
281
+ } else {
282
+ byTargetType.set(targetType, [descriptor]);
283
+ }
284
+ }
285
+ }
286
+
287
+ // The descriptor map is heterogeneous in `P` — each codec id has its own
288
+ // params shape. The public `CodecDescriptorRegistry` interface widens to
289
+ // `CodecDescriptor<unknown>` and consumers narrow per codec id at the
290
+ // call site (the descriptor's `paramsSchema` validates JSON-sourced
291
+ // params before the factory ever sees them, so the runtime narrow is
292
+ // safe). The cast at registration goes through `unknown` because
293
+ // `CodecDescriptor<P>` is invariant in `P` (the `factory` and
294
+ // `renderOutputType` slots use `P` contravariantly).
295
+ for (const descriptor of parameterizedDescriptors.values()) {
296
+ registerInIndices(descriptor as unknown as AnyDescriptor);
297
+ }
298
+
299
+ for (const codec of codecRegistry.values()) {
300
+ if (byId.has(codec.id)) continue;
301
+ registerInIndices(synthesizeNonParameterizedDescriptor(codec) as unknown as AnyDescriptor);
302
+ }
303
+
304
+ return {
305
+ descriptorFor(codecId: string): AnyDescriptor | undefined {
306
+ return byId.get(codecId);
307
+ },
308
+ *values(): IterableIterator<AnyDescriptor> {
309
+ yield* byId.values();
310
+ },
311
+ byTargetType(targetType: string): readonly AnyDescriptor[] {
312
+ return byTargetType.get(targetType) ?? Object.freeze([]);
313
+ },
314
+ };
315
+ }
316
+
317
+ function collectTypeRefSites(
318
+ storage: SqlStorage,
319
+ ): Map<string, Array<{ readonly table: string; readonly column: string }>> {
320
+ const sites = new Map<string, Array<{ readonly table: string; readonly column: string }>>();
321
+ for (const [tableName, table] of Object.entries(storage.tables)) {
322
+ for (const [columnName, column] of Object.entries(table.columns)) {
323
+ if (typeof column.typeRef !== 'string') continue;
324
+ const list = sites.get(column.typeRef);
325
+ const entry = { table: tableName, column: columnName };
326
+ if (list) {
327
+ list.push(entry);
328
+ } else {
329
+ sites.set(column.typeRef, [entry]);
330
+ }
331
+ }
332
+ }
333
+ return sites;
334
+ }
335
+
245
336
  function initializeTypeHelpers(
246
- storageTypes: Record<string, StorageTypeInstance> | undefined,
337
+ storage: SqlStorage,
247
338
  codecDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
248
339
  ): TypeHelperRegistry {
249
340
  const helpers: TypeHelperRegistry = {};
341
+ const storageTypes = storage.types;
250
342
 
251
343
  if (!storageTypes) {
252
344
  return helpers;
253
345
  }
254
346
 
347
+ const typeRefSites = collectTypeRefSites(storage);
348
+
255
349
  for (const [typeName, typeInstance] of Object.entries(storageTypes)) {
256
350
  const descriptor = codecDescriptors.get(typeInstance.codecId);
257
351
 
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 {
352
+ if (!descriptor) {
353
+ // No parameterized descriptor for this codec id — store the raw
354
+ // type instance for callers that need typeParams metadata.
269
355
  helpers[typeName] = typeInstance;
356
+ continue;
270
357
  }
358
+
359
+ const validatedParams = validateTypeParams(typeInstance.typeParams, descriptor, {
360
+ typeName,
361
+ });
362
+
363
+ const usedAt = typeRefSites.get(typeName) ?? [];
364
+ const ctx: SqlCodecInstanceContext = { name: typeName, usedAt };
365
+ helpers[typeName] = descriptor.factory(validatedParams)(ctx);
271
366
  }
272
367
 
273
368
  return helpers;
@@ -290,67 +385,217 @@ function validateColumnTypeParams(
290
385
  }
291
386
 
292
387
  /**
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 }`.
388
+ * View of a codec that exposes a per-instance JSON-schema `validate`
389
+ * function. Codecs declare this contract by including the
390
+ * `'json-validator'` `CodecTrait` in their `traits` array; the trait is
391
+ * the gate that lets `extractValidator` resolve from structurally-typed
392
+ * `unknown` to this typed view.
393
+ */
394
+ type JsonValidatorCodec = {
395
+ readonly traits?: ReadonlyArray<unknown>;
396
+ readonly validate: JsonSchemaValidateFn;
397
+ };
398
+
399
+ function hasJsonValidatorTrait(candidate: unknown): candidate is JsonValidatorCodec {
400
+ if (candidate === null || typeof candidate !== 'object') return false;
401
+ const traits = (candidate as { readonly traits?: unknown }).traits;
402
+ if (!Array.isArray(traits)) return false;
403
+ if (!traits.includes('json-validator')) return false;
404
+ const validate = (candidate as { readonly validate?: unknown }).validate;
405
+ return typeof validate === 'function';
406
+ }
407
+
408
+ function extractValidator(candidate: unknown): JsonSchemaValidateFn | undefined {
409
+ return hasJsonValidatorTrait(candidate) ? candidate.validate : undefined;
410
+ }
411
+
412
+ function isResolvedCodec(candidate: unknown): candidate is Codec {
413
+ return (
414
+ candidate !== null &&
415
+ typeof candidate === 'object' &&
416
+ 'id' in candidate &&
417
+ 'decode' in candidate
418
+ );
419
+ }
420
+
421
+ /**
422
+ * Walk the contract's `storage.tables[].columns[]` and resolve each
423
+ * column to a `Codec` through the unified descriptor map. Per-instance
424
+ * behavior:
425
+ *
426
+ * - **typeRef columns**: reuse the resolved codec materialized once by
427
+ * `initializeTypeHelpers` for the `storage.types` entry. Multiple
428
+ * columns sharing one typeRef share one codec instance.
429
+ * - **inline-typeParams columns**: call `descriptor.factory(typeParams)
430
+ * (ctx)` once per column (per-column anonymous instance).
431
+ * - **non-parameterized columns**: call `descriptor.factory()(ctx)`
432
+ * once. The synthesized descriptor's factory is constant — every call
433
+ * returns the same shared codec instance — so columns sharing a non-
434
+ * parameterized codec id share one resolved codec without explicit
435
+ * caching.
295
436
  *
296
- * Handles both:
297
- * - Inline `typeParams.schema` on columns
298
- * - `typeRef` `storage.types[ref]` with init hook results already in `types` registry
437
+ * Combines what `initializeTypeHelpers` (named-instance walk) and the
438
+ * old `buildJsonSchemaValidatorRegistry` (per-column walk) used to do
439
+ * separately: one walk over all columns, one resolved codec per column,
440
+ * one trait-gated validator extraction per column. The result drives
441
+ * both the dispatch registry (`ContractCodecRegistry.forColumn`) and the
442
+ * validator registry.
443
+ *
444
+ * Codec-registry-unification spec § AC-4: every column resolves through
445
+ * one descriptor map without branching on parameterization.
299
446
  */
300
- function buildJsonSchemaValidatorRegistry(
447
+ function buildContractCodecRegistry(
301
448
  contract: Contract<SqlStorage>,
449
+ codecDescriptors: CodecDescriptorRegistry,
450
+ legacyCodecRegistry: CodecRegistry,
302
451
  types: TypeHelperRegistry,
303
- codecDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
304
- ): JsonSchemaValidatorRegistry | undefined {
452
+ parameterizedDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
453
+ ): {
454
+ readonly registry: ContractCodecRegistry;
455
+ readonly jsonValidators: JsonSchemaValidatorRegistry | undefined;
456
+ } {
457
+ const byColumn = new Map<string, Codec>();
458
+ const byCodecId = new Map<string, Codec>();
459
+ // Codec ids whose `byCodecId` entry is ambiguous — multiple distinct
460
+ // resolved instances landed under the same parameterized codec id (e.g.
461
+ // `Vector<1024>` and `Vector<1536>` both registering under
462
+ // `pg/vector@1`). The encode-side `forCodecId` fallback rejects these
463
+ // ids so a DSL-param without a column ref cannot silently bind to the
464
+ // wrong instance. Retires when AC-5's `ParamRef.refs` plumbing lands
465
+ // (TML-2357).
466
+ const ambiguousCodecIds = new Set<string>();
305
467
  const validators = new Map<string, JsonSchemaValidateFn>();
306
468
 
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
469
  for (const [tableName, table] of Object.entries(contract.storage.tables)) {
320
470
  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);
471
+ const columnKey = `${tableName}.${columnName}`;
472
+ const descriptor = codecDescriptors.descriptorFor(column.codecId);
473
+
474
+ let resolvedCodec: Codec | undefined;
475
+
476
+ if (descriptor) {
477
+ const isParameterized = parameterizedDescriptors.has(column.codecId);
478
+
479
+ if (column.typeRef) {
480
+ // The named instance was already materialized once by
481
+ // `initializeTypeHelpers`; reuse it so multiple columns sharing
482
+ // the same typeRef share one codec instance (and any per-
483
+ // instance helper state on it).
484
+ const helper = types[column.typeRef];
485
+ if (isResolvedCodec(helper)) {
486
+ resolvedCodec = helper;
487
+ }
488
+ } else if (column.typeParams && isParameterized) {
489
+ const parameterizedDescriptor = parameterizedDescriptors.get(column.codecId);
490
+ if (parameterizedDescriptor) {
491
+ const validatedParams = validateTypeParams(column.typeParams, parameterizedDescriptor, {
492
+ tableName,
493
+ columnName,
494
+ });
495
+ const ctx: SqlCodecInstanceContext = {
496
+ name: `<anon:${tableName}.${columnName}>`,
497
+ usedAt: [{ table: tableName, column: columnName }],
498
+ };
499
+ resolvedCodec = parameterizedDescriptor.factory(validatedParams)(ctx);
500
+ }
501
+ } else if (!isParameterized) {
502
+ // Non-parameterized column. Cache the resolved codec by codec
503
+ // id — the synthesized descriptor's factory is constant for
504
+ // non-parameterized codecs, so columns sharing this codec id
505
+ // share one resolved instance.
506
+ let cached = byCodecId.get(column.codecId);
507
+ if (!cached) {
508
+ const ctx: SqlCodecInstanceContext = {
509
+ name: `<shared:${column.codecId}>`,
510
+ usedAt: [{ table: tableName, column: columnName }],
511
+ };
512
+ // `synthesizeNonParameterizedDescriptor` produces a
513
+ // `CodecDescriptor<void>` whose factory ignores its params
514
+ // and ctx; the runtime's `void` value is `undefined`. The
515
+ // structural cast goes through `unknown` to satisfy the
516
+ // heterogeneous-`P` registry boundary (the factory's
517
+ // declared `P` is `any` here; the consumer narrows per
518
+ // codec id). The cast narrows the descriptor's
519
+ // family-agnostic `CodecInstanceContext` slot to the SQL
520
+ // `SqlCodecInstanceContext` we pass at this call site —
521
+ // function-argument contravariance makes the narrow safe
522
+ // (a callee that accepts the base will also accept the
523
+ // SQL extension). Per spec § Non-functional constraints.
524
+ const voidFactory = descriptor.factory as unknown as (
525
+ params: undefined,
526
+ ) => (ctx: SqlCodecInstanceContext) => Codec;
527
+ cached = voidFactory(undefined)(ctx);
528
+ byCodecId.set(column.codecId, cached);
529
+ }
530
+ resolvedCodec = cached;
330
531
  }
331
- continue;
532
+ // else: parameterized codec id with no typeRef and no typeParams
533
+ // — this is the legitimate "undimensioned" form for codecs that
534
+ // ship a no-params column variant alongside a parameterized one
535
+ // (e.g. pgvector's `vectorColumn` vs. `vector(N)`). Leave
536
+ // `resolvedCodec` undefined; encode/decode for this column flows
537
+ // through `forCodecId` (the AC-5-deferred carve-out documented
538
+ // in `relational-core/src/ast/codec-types.ts`). The fallback
539
+ // works for these cases because their wire format is
540
+ // params-independent (vector formats `[v1,v2,...]` regardless
541
+ // of declared length).
332
542
  }
333
543
 
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
- }
544
+ if (resolvedCodec) {
545
+ byColumn.set(columnKey, resolvedCodec);
546
+ const validate = extractValidator(resolvedCodec);
547
+ if (validate) {
548
+ validators.set(columnKey, validate);
549
+ }
550
+ const existing = byCodecId.get(column.codecId);
551
+ if (existing === undefined) {
552
+ byCodecId.set(column.codecId, resolvedCodec);
553
+ } else if (existing !== resolvedCodec && parameterizedDescriptors.has(column.codecId)) {
554
+ ambiguousCodecIds.add(column.codecId);
344
555
  }
345
556
  }
346
557
  }
347
558
  }
348
559
 
349
- if (validators.size === 0) return undefined;
350
- return {
351
- get: (key: string) => validators.get(key),
352
- size: validators.size,
560
+ const registry: ContractCodecRegistry = {
561
+ forColumn(table, column) {
562
+ return byColumn.get(`${table}.${column}`);
563
+ },
564
+ forCodecId(codecId) {
565
+ // Codec-id-only fallback for sites without a column ref (encode-
566
+ // side DSL params whose `ParamRef.refs` isn't populated). Prefer
567
+ // the contract-walk-derived shared codec; fall back to the legacy
568
+ // `codecRegistry.get` for parameterized codec ids whose contracts
569
+ // don't have a typeRef/typeParams column the walk could resolve
570
+ // through. The legacy fallback retires once `ParamRef.refs` is
571
+ // threaded everywhere (TML-2357).
572
+ //
573
+ // Reject ambiguous parameterized fallbacks: if the contract walk
574
+ // resolved more than one distinct codec instance under this id
575
+ // (e.g. multiple vector dimensions, multiple arktype-json
576
+ // schemas), the codec-id-keyed lookup cannot honor the call site
577
+ // — fail fast rather than bind to whichever instance happened to
578
+ // land first.
579
+ if (ambiguousCodecIds.has(codecId)) {
580
+ throw runtimeError(
581
+ 'RUNTIME.TYPE_PARAMS_INVALID',
582
+ `Codec '${codecId}' resolves to multiple parameterized instances; column-aware dispatch is required.`,
583
+ { codecId },
584
+ );
585
+ }
586
+ return byCodecId.get(codecId) ?? legacyCodecRegistry.get(codecId);
587
+ },
353
588
  };
589
+
590
+ const jsonValidators: JsonSchemaValidatorRegistry | undefined =
591
+ validators.size > 0
592
+ ? {
593
+ get: (key: string) => validators.get(key),
594
+ size: validators.size,
595
+ }
596
+ : undefined;
597
+
598
+ return { registry, jsonValidators };
354
599
  }
355
600
 
356
601
  function collectMutationDefaultGenerators(
@@ -476,23 +721,32 @@ export function createExecutionContext<
476
721
  }
477
722
 
478
723
  const parameterizedCodecDescriptors = collectParameterizedCodecDescriptors(contributors);
724
+ const codecDescriptors = buildCodecDescriptorRegistry(
725
+ codecRegistry,
726
+ parameterizedCodecDescriptors,
727
+ );
479
728
  const mutationDefaultGeneratorRegistry = collectMutationDefaultGenerators(contributors);
480
729
 
481
730
  if (parameterizedCodecDescriptors.size > 0) {
482
731
  validateColumnTypeParams(contract.storage, parameterizedCodecDescriptors);
483
732
  }
484
733
 
485
- const types = initializeTypeHelpers(contract.storage.types, parameterizedCodecDescriptors);
734
+ const types = initializeTypeHelpers(contract.storage, parameterizedCodecDescriptors);
486
735
 
487
- const jsonSchemaValidators = buildJsonSchemaValidatorRegistry(
488
- contract,
489
- types,
490
- parameterizedCodecDescriptors,
491
- );
736
+ const { registry: contractCodecs, jsonValidators: jsonSchemaValidators } =
737
+ buildContractCodecRegistry(
738
+ contract,
739
+ codecDescriptors,
740
+ codecRegistry,
741
+ types,
742
+ parameterizedCodecDescriptors,
743
+ );
492
744
 
493
745
  return {
494
746
  contract,
495
747
  codecs: codecRegistry,
748
+ contractCodecs,
749
+ codecDescriptors,
496
750
  queryOperations: queryOperationRegistry,
497
751
  types,
498
752
  ...(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>
package/src/sql-marker.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MarkerStatement } from '@prisma-next/runtime-executor';
1
+ import type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
2
2
 
3
3
  export interface SqlStatement {
4
4
  readonly sql: string;
@@ -12,6 +12,15 @@ export interface WriteMarkerInput {
12
12
  readonly canonicalVersion?: number;
13
13
  readonly appTag?: string;
14
14
  readonly meta?: Record<string, unknown>;
15
+ /**
16
+ * Applied-invariants set on the marker.
17
+ *
18
+ * - `undefined` → existing column left untouched. Sign and
19
+ * verify-database paths use this; they don't accumulate invariants.
20
+ * - explicit value (including `[]`) → column overwritten with
21
+ * exactly that value.
22
+ */
23
+ readonly invariants?: readonly string[];
15
24
  }
16
25
 
17
26
  export const ensureSchemaStatement: SqlStatement = {
@@ -28,7 +37,8 @@ export const ensureTableStatement: SqlStatement = {
28
37
  canonical_version int,
29
38
  updated_at timestamptz not null default now(),
30
39
  app_tag text,
31
- meta jsonb not null default '{}'
40
+ meta jsonb not null default '{}',
41
+ invariants text[] not null default '{}'
32
42
  )`,
33
43
  params: [],
34
44
  };
@@ -42,7 +52,8 @@ export function readContractMarker(): MarkerStatement {
42
52
  canonical_version,
43
53
  updated_at,
44
54
  app_tag,
45
- meta
55
+ meta,
56
+ invariants
46
57
  from prisma_contract.marker
47
58
  where id = $1`,
48
59
  params: [1],
@@ -54,52 +65,56 @@ export interface WriteContractMarkerStatements {
54
65
  readonly update: SqlStatement;
55
66
  }
56
67
 
57
- export function writeContractMarker(input: WriteMarkerInput): WriteContractMarkerStatements {
58
- const baseParams: readonly unknown[] = [
59
- 1,
60
- input.storageHash,
61
- input.profileHash,
62
- input.contractJson ?? null,
63
- input.canonicalVersion ?? null,
64
- input.appTag ?? null,
65
- JSON.stringify(input.meta ?? {}),
68
+ /**
69
+ * Variable columns that participate in INSERT/UPDATE alongside the
70
+ * always-on `id = $1` and `updated_at = now()`. Each column declares
71
+ * its name, optional cast type, and parameter value; the placeholder
72
+ * (`$N`) is computed positionally below — adding or reordering a
73
+ * column doesn't desync indices. `invariants` only appears when the
74
+ * caller supplies it — see `WriteMarkerInput.invariants`.
75
+ */
76
+ function markerColumns(
77
+ input: WriteMarkerInput,
78
+ ): ReadonlyArray<{ readonly name: string; readonly type?: string; readonly param: unknown }> {
79
+ return [
80
+ { name: 'core_hash', param: input.storageHash },
81
+ { name: 'profile_hash', param: input.profileHash },
82
+ { name: 'contract_json', type: 'jsonb', param: input.contractJson ?? null },
83
+ { name: 'canonical_version', param: input.canonicalVersion ?? null },
84
+ { name: 'app_tag', param: input.appTag ?? null },
85
+ { name: 'meta', type: 'jsonb', param: JSON.stringify(input.meta ?? {}) },
86
+ ...(input.invariants !== undefined
87
+ ? [{ name: 'invariants' as const, type: 'text[]' as const, param: input.invariants }]
88
+ : []),
66
89
  ];
90
+ }
67
91
 
68
- const insert: SqlStatement = {
69
- sql: `insert into prisma_contract.marker (
70
- id,
71
- core_hash,
72
- profile_hash,
73
- contract_json,
74
- canonical_version,
75
- updated_at,
76
- app_tag,
77
- meta
78
- ) values (
79
- $1,
80
- $2,
81
- $3,
82
- $4::jsonb,
83
- $5,
84
- now(),
85
- $6,
86
- $7::jsonb
87
- )`,
88
- params: baseParams,
89
- };
92
+ export function writeContractMarker(input: WriteMarkerInput): WriteContractMarkerStatements {
93
+ const cols = markerColumns(input);
94
+ // $1 is reserved for `id`; subsequent positions follow the order of cols.
95
+ const placed = cols.map((c, i) => ({
96
+ name: c.name,
97
+ expr: c.type ? `$${i + 2}::${c.type}` : `$${i + 2}`,
98
+ param: c.param,
99
+ }));
100
+ const params: readonly unknown[] = [1, ...placed.map((c) => c.param)];
90
101
 
91
- const update: SqlStatement = {
92
- sql: `update prisma_contract.marker set
93
- core_hash = $2,
94
- profile_hash = $3,
95
- contract_json = $4::jsonb,
96
- canonical_version = $5,
97
- updated_at = now(),
98
- app_tag = $6,
99
- meta = $7::jsonb
100
- where id = $1`,
101
- params: baseParams,
102
- };
102
+ // `updated_at = now()` is a SQL literal with no parameter slot, so it
103
+ // sits outside `placed` and is appended directly to each statement.
104
+ const insertColumns = ['id', ...placed.map((c) => c.name), 'updated_at'].join(', ');
105
+ const insertValues = ['$1', ...placed.map((c) => c.expr), 'now()'].join(', ');
106
+ const setClauses = [...placed.map((c) => `${c.name} = ${c.expr}`), 'updated_at = now()'].join(
107
+ ', ',
108
+ );
103
109
 
104
- return { insert, update };
110
+ return {
111
+ insert: {
112
+ sql: `insert into prisma_contract.marker (${insertColumns}) values (${insertValues})`,
113
+ params,
114
+ },
115
+ update: {
116
+ sql: `update prisma_contract.marker set ${setClauses} where id = $1`,
117
+ params,
118
+ },
119
+ };
105
120
  }