@prisma-next/sql-contract-ts 0.12.0 → 0.13.0-dev.10

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,4 +1,5 @@
1
1
  import type { ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
2
+ import type { ExtensionPackRef } from '@prisma-next/framework-components/components';
2
3
  import {
3
4
  isPostgresEnumStorageEntry,
4
5
  type PostgresEnumStorageEntry,
@@ -35,6 +36,7 @@ import {
35
36
  emitTypedCrossModelFallbackWarnings,
36
37
  emitTypedNamedTypeFallbackWarnings,
37
38
  } from './contract-warnings';
39
+ import { isEnumTypeHandle } from './enum-type';
38
40
 
39
41
  type RuntimeModel = ContractModelBuilder<
40
42
  string | undefined,
@@ -84,6 +86,13 @@ function resolveFieldDescriptor(
84
86
  }
85
87
 
86
88
  if ('typeRef' in fieldState && fieldState.typeRef) {
89
+ if (isEnumTypeHandle(fieldState.typeRef)) {
90
+ return {
91
+ codecId: fieldState.typeRef.codecId,
92
+ nativeType: fieldState.typeRef.nativeType,
93
+ };
94
+ }
95
+
87
96
  const typeRef =
88
97
  typeof fieldState.typeRef === 'string'
89
98
  ? fieldState.typeRef
@@ -253,6 +262,41 @@ function resolveRelationForeignKeys(
253
262
  }
254
263
 
255
264
  const targetModelName = resolveRelationModelName(relation.toModel);
265
+
266
+ // F-relfk: cross-space relations carry a spaceId; skip the local spec lookup
267
+ // and include cross-space coordinates so resolveForeignKeyNodes routes the FK
268
+ // through the cross-space path.
269
+ if (relation.spaceId !== undefined) {
270
+ const fields = normalizeRelationFieldNames(relation.from);
271
+ const targetFields = normalizeRelationFieldNames(relation.to);
272
+ assertRelationFieldArity({
273
+ modelName: spec.modelName,
274
+ relationName,
275
+ leftLabel: 'source',
276
+ leftFields: fields,
277
+ rightLabel: 'target',
278
+ rightFields: targetFields,
279
+ });
280
+
281
+ foreignKeys.push({
282
+ kind: 'fk',
283
+ fields,
284
+ targetModel: targetModelName,
285
+ targetFields,
286
+ targetSpaceId: relation.spaceId,
287
+ ...(relation.namespaceId !== undefined ? { targetNamespaceId: relation.namespaceId } : {}),
288
+ ...(relation.tableName !== undefined ? { targetTableName: relation.tableName } : {}),
289
+ ...(relation.sql.fk.name ? { name: relation.sql.fk.name } : {}),
290
+ ...(relation.sql.fk.onDelete ? { onDelete: relation.sql.fk.onDelete } : {}),
291
+ ...(relation.sql.fk.onUpdate ? { onUpdate: relation.sql.fk.onUpdate } : {}),
292
+ ...(relation.sql.fk.constraint !== undefined
293
+ ? { constraint: relation.sql.fk.constraint }
294
+ : {}),
295
+ ...(relation.sql.fk.index !== undefined ? { index: relation.sql.fk.index } : {}),
296
+ });
297
+ continue;
298
+ }
299
+
256
300
  if (!allSpecs.has(targetModelName)) {
257
301
  throw new Error(
258
302
  `Relation "${spec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
@@ -308,17 +352,12 @@ function lowerBelongsToRelation(
308
352
  relation: Extract<RelationState, { kind: 'belongsTo' }>,
309
353
  currentSpec: RuntimeModelSpec,
310
354
  allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
355
+ extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
311
356
  ): RelationNode {
312
357
  const targetModelName = resolveRelationModelName(relation.toModel);
313
- const targetSpec = allSpecs.get(targetModelName);
314
- if (!targetSpec) {
315
- throw new Error(
316
- `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
317
- );
318
- }
319
-
320
358
  const fromFields = normalizeRelationFieldNames(relation.from);
321
359
  const toFields = normalizeRelationFieldNames(relation.to);
360
+
322
361
  assertRelationFieldArity({
323
362
  modelName: currentSpec.modelName,
324
363
  relationName,
@@ -328,6 +367,48 @@ function lowerBelongsToRelation(
328
367
  rightFields: toFields,
329
368
  });
330
369
 
370
+ // Cross-space path: the target lives in a different contract space.
371
+ // Resolve from the brand carried on the BelongsToRelation instead of
372
+ // requiring a local model spec — matching how the FK lowering works.
373
+ if (relation.spaceId !== undefined) {
374
+ assertKnownExtensionPack(
375
+ extensionPacks,
376
+ relation.spaceId,
377
+ `Relation "${currentSpec.modelName}.${relationName}"`,
378
+ );
379
+ const targetTable = relation.tableName ?? targetModelName.toLowerCase();
380
+ const parentColumns = mapFieldNamesToColumnNames(
381
+ currentSpec.modelName,
382
+ fromFields,
383
+ currentSpec.fieldToColumn,
384
+ );
385
+ // For cross-space relations, the `to` field names map directly to column
386
+ // names because we have no fieldToColumn map for the remote model.
387
+ // (The brand carries the table name; field→column resolution on the remote
388
+ // side is deferred to the planner which has access to the remote contract.)
389
+ return {
390
+ fieldName: relationName,
391
+ toModel: targetModelName,
392
+ toTable: targetTable,
393
+ cardinality: 'N:1',
394
+ spaceId: relation.spaceId,
395
+ ...(relation.namespaceId !== undefined ? { namespaceId: relation.namespaceId } : {}),
396
+ on: {
397
+ parentTable: currentSpec.tableName,
398
+ parentColumns,
399
+ childTable: targetTable,
400
+ childColumns: toFields,
401
+ },
402
+ };
403
+ }
404
+
405
+ const targetSpec = allSpecs.get(targetModelName);
406
+ if (!targetSpec) {
407
+ throw new Error(
408
+ `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
409
+ );
410
+ }
411
+
331
412
  return {
332
413
  fieldName: relationName,
333
414
  toModel: targetModelName,
@@ -472,9 +553,10 @@ function resolveRelationNode(
472
553
  relation: RelationState,
473
554
  currentSpec: RuntimeModelSpec,
474
555
  allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
556
+ extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
475
557
  ): RelationNode {
476
558
  if (relation.kind === 'belongsTo') {
477
- return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs);
559
+ return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs, extensionPacks);
478
560
  }
479
561
 
480
562
  if (relation.kind === 'hasMany' || relation.kind === 'hasOne') {
@@ -484,7 +566,7 @@ function resolveRelationNode(
484
566
  return lowerManyToManyRelation(relationName, relation, currentSpec, allSpecs);
485
567
  }
486
568
 
487
- function lowerForeignKeyNode(
569
+ function lowerLocalForeignKeyNode(
488
570
  spec: RuntimeModelSpec,
489
571
  targetSpec: RuntimeModelSpec,
490
572
  foreignKey: {
@@ -516,11 +598,74 @@ function lowerForeignKeyNode(
516
598
  };
517
599
  }
518
600
 
601
+ function lowerCrossSpaceForeignKeyNode(
602
+ spec: RuntimeModelSpec,
603
+ foreignKey: {
604
+ readonly fields: readonly string[];
605
+ readonly targetFields: readonly string[];
606
+ readonly targetModel: string;
607
+ readonly targetSpaceId: string;
608
+ readonly targetNamespaceId?: string;
609
+ readonly targetTableName?: string;
610
+ readonly name?: string | undefined;
611
+ readonly onDelete?: ForeignKeyConstraint['onDelete'] | undefined;
612
+ readonly onUpdate?: ForeignKeyConstraint['onUpdate'] | undefined;
613
+ readonly constraint?: boolean | undefined;
614
+ readonly index?: boolean | undefined;
615
+ },
616
+ ): ForeignKeyNode {
617
+ return {
618
+ columns: mapFieldNamesToColumnNames(spec.modelName, foreignKey.fields, spec.fieldToColumn),
619
+ references: {
620
+ model: foreignKey.targetModel,
621
+ table: foreignKey.targetTableName ?? foreignKey.targetModel.toLowerCase(),
622
+ columns: foreignKey.targetFields,
623
+ ...(foreignKey.targetNamespaceId !== undefined
624
+ ? { namespaceId: foreignKey.targetNamespaceId }
625
+ : {}),
626
+ spaceId: foreignKey.targetSpaceId,
627
+ },
628
+ ...(foreignKey.name ? { name: foreignKey.name } : {}),
629
+ ...(foreignKey.onDelete ? { onDelete: foreignKey.onDelete } : {}),
630
+ ...(foreignKey.onUpdate ? { onUpdate: foreignKey.onUpdate } : {}),
631
+ ...(foreignKey.constraint !== undefined ? { constraint: foreignKey.constraint } : {}),
632
+ ...(foreignKey.index !== undefined ? { index: foreignKey.index } : {}),
633
+ };
634
+ }
635
+
636
+ function assertKnownExtensionPack(
637
+ extensionPacks: Record<string, ExtensionPackRef<'sql', string>> | undefined,
638
+ spaceId: string,
639
+ context: string,
640
+ ): void {
641
+ if (extensionPacks !== undefined && Object.hasOwn(extensionPacks, spaceId)) {
642
+ return;
643
+ }
644
+ throw new Error(
645
+ `${context} references contract space "${spaceId}" but "${spaceId}" is not declared in extensionPacks. Add the pack to extensionPacks.`,
646
+ );
647
+ }
648
+
519
649
  function resolveForeignKeyNodes(
520
650
  spec: RuntimeModelSpec,
521
651
  allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
652
+ extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
522
653
  ): readonly ForeignKeyNode[] {
523
654
  const relationForeignKeys = resolveRelationForeignKeys(spec, allSpecs).map((foreignKey) => {
655
+ // F-relfk: relation-derived FKs for cross-space targets carry targetSpaceId;
656
+ // route them through the cross-space path, just like explicit sql() FKs.
657
+ if (foreignKey.targetSpaceId !== undefined) {
658
+ assertKnownExtensionPack(
659
+ extensionPacks,
660
+ foreignKey.targetSpaceId,
661
+ `Relation-derived foreign key on "${spec.modelName}"`,
662
+ );
663
+ return lowerCrossSpaceForeignKeyNode(spec, {
664
+ ...foreignKey,
665
+ targetSpaceId: foreignKey.targetSpaceId,
666
+ });
667
+ }
668
+
524
669
  const targetSpec = allSpecs.get(foreignKey.targetModel);
525
670
  if (!targetSpec) {
526
671
  throw new Error(
@@ -528,10 +673,22 @@ function resolveForeignKeyNodes(
528
673
  );
529
674
  }
530
675
 
531
- return lowerForeignKeyNode(spec, targetSpec, foreignKey);
676
+ return lowerLocalForeignKeyNode(spec, targetSpec, foreignKey);
532
677
  });
533
678
 
534
679
  const sqlForeignKeys = (spec.sqlSpec?.foreignKeys ?? []).map((foreignKey) => {
680
+ if (foreignKey.targetSpaceId !== undefined) {
681
+ assertKnownExtensionPack(
682
+ extensionPacks,
683
+ foreignKey.targetSpaceId,
684
+ `Foreign key on "${spec.modelName}"`,
685
+ );
686
+ return lowerCrossSpaceForeignKeyNode(spec, {
687
+ ...foreignKey,
688
+ targetSpaceId: foreignKey.targetSpaceId,
689
+ });
690
+ }
691
+
535
692
  const targetSpec = allSpecs.get(foreignKey.targetModel);
536
693
  if (!targetSpec) {
537
694
  throw new Error(
@@ -539,7 +696,7 @@ function resolveForeignKeyNodes(
539
696
  );
540
697
  }
541
698
 
542
- return lowerForeignKeyNode(spec, targetSpec, foreignKey);
699
+ return lowerLocalForeignKeyNode(spec, targetSpec, foreignKey);
543
700
  });
544
701
 
545
702
  return [...relationForeignKeys, ...sqlForeignKeys];
@@ -550,6 +707,7 @@ function resolveModelNode(
550
707
  allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
551
708
  storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
552
709
  storageTypeReverseLookup: ReadonlyMap<StorageTypeInstance | PostgresEnumStorageEntry, string>,
710
+ extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
553
711
  ): ModelNode {
554
712
  const fields: FieldNode[] = [];
555
713
 
@@ -567,6 +725,11 @@ function resolveModelNode(
567
725
  throw new Error(`Column name resolution failed for "${spec.modelName}.${fieldName}"`);
568
726
  }
569
727
 
728
+ const enumHandle =
729
+ 'typeRef' in fieldState && isEnumTypeHandle(fieldState.typeRef)
730
+ ? fieldState.typeRef
731
+ : undefined;
732
+
570
733
  fields.push({
571
734
  fieldName,
572
735
  columnName,
@@ -574,6 +737,7 @@ function resolveModelNode(
574
737
  nullable: fieldState.nullable,
575
738
  ...(fieldState.default ? { default: fieldState.default } : {}),
576
739
  ...(fieldState.executionDefaults ? { executionDefaults: fieldState.executionDefaults } : {}),
740
+ ...(enumHandle !== undefined ? { enumTypeHandle: enumHandle } : {}),
577
741
  });
578
742
  }
579
743
 
@@ -588,9 +752,9 @@ function resolveModelNode(
588
752
  ...ifDefined('type', index.type),
589
753
  ...ifDefined('options', index.options),
590
754
  })) satisfies readonly IndexNode[];
591
- const foreignKeys = resolveForeignKeyNodes(spec, allSpecs);
755
+ const foreignKeys = resolveForeignKeyNodes(spec, allSpecs, extensionPacks);
592
756
  const relations = Object.entries(spec.relations).map(([relationName, relationBuilder]) =>
593
- resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs),
757
+ resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs, extensionPacks),
594
758
  );
595
759
 
596
760
  return {
@@ -614,6 +778,7 @@ function resolveModelNode(
614
778
  ...(indexes.length > 0 ? { indexes } : {}),
615
779
  ...(foreignKeys.length > 0 ? { foreignKeys } : {}),
616
780
  ...(relations.length > 0 ? { relations } : {}),
781
+ ...ifDefined('control', spec.sqlSpec?.control),
617
782
  };
618
783
  }
619
784
 
@@ -687,7 +852,10 @@ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection
687
852
  };
688
853
  }
689
854
 
690
- function lowerModels(collection: RuntimeCollection): readonly ModelNode[] {
855
+ function lowerModels(
856
+ collection: RuntimeCollection,
857
+ extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
858
+ ): readonly ModelNode[] {
691
859
  emitTypedCrossModelFallbackWarnings(collection);
692
860
 
693
861
  const storageTypeReverseLookup = buildStorageTypeReverseLookup(collection.storageTypes);
@@ -697,16 +865,18 @@ function lowerModels(collection: RuntimeCollection): readonly ModelNode[] {
697
865
  collection.modelSpecs,
698
866
  collection.storageTypes,
699
867
  storageTypeReverseLookup,
868
+ extensionPacks,
700
869
  ),
701
870
  );
702
871
  }
703
872
 
704
873
  export function buildContractDefinition(definition: ContractInput): ContractDefinition {
705
874
  const collection = collectRuntimeModelSpecs(definition);
706
- const models = lowerModels(collection);
875
+ const models = lowerModels(collection, definition.extensionPacks);
707
876
 
708
877
  return {
709
878
  target: definition.target,
879
+ ...ifDefined('defaultControlPolicy', definition.defaultControlPolicy),
710
880
  ...(definition.extensionPacks ? { extensionPacks: definition.extensionPacks } : {}),
711
881
  ...(definition.storageHash ? { storageHash: definition.storageHash } : {}),
712
882
  ...(definition.foreignKeyDefaults ? { foreignKeyDefaults: definition.foreignKeyDefaults } : {}),
@@ -715,6 +885,9 @@ export function buildContractDefinition(definition: ContractInput): ContractDefi
715
885
  : {}),
716
886
  ...(definition.namespaces ? { namespaces: definition.namespaces } : {}),
717
887
  ...(definition.createNamespace ? { createNamespace: definition.createNamespace } : {}),
888
+ ...(definition.enums && Object.keys(definition.enums).length > 0
889
+ ? { enums: definition.enums }
890
+ : {}),
718
891
  models,
719
892
  };
720
893
  }
@@ -6,17 +6,18 @@ import type {
6
6
  StorageHashBase,
7
7
  } from '@prisma-next/contract/types';
8
8
  import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
9
+ import type { StorageType } from '@prisma-next/framework-components/ir';
9
10
  import type { IndexTypeRegistration } from '@prisma-next/sql-contract/index-types';
10
11
  import type {
11
12
  ContractWithTypeMaps,
12
13
  Index,
13
- PostgresEnumStorageEntry,
14
14
  ReferentialAction,
15
15
  StorageTypeInstance,
16
16
  TypeMaps,
17
17
  } from '@prisma-next/sql-contract/types';
18
18
  import type { UnionToIntersection } from './authoring-type-utils';
19
19
  import type { AttributeStageIdFieldNames, FieldStateOf, ScalarFieldBuilder } from './contract-dsl';
20
+ import type { EnumTypeHandle } from './enum-type';
20
21
 
21
22
  export type ExtractCodecTypesFromPack<P> = P extends { __codecTypes?: infer C }
22
23
  ? C extends Record<string, { output: unknown }>
@@ -144,10 +145,7 @@ type DefinitionNamespaces<Definition> = Definition extends {
144
145
  type DefinitionTypes<Definition> = Definition extends {
145
146
  readonly types?: unknown;
146
147
  }
147
- ? Present<Definition['types']> extends Record<
148
- string,
149
- StorageTypeInstance | PostgresEnumStorageEntry
150
- >
148
+ ? Present<Definition['types']> extends Record<string, StorageType>
151
149
  ? Present<Definition['types']>
152
150
  : Record<never, never>
153
151
  : Record<never, never>;
@@ -311,10 +309,7 @@ type DescriptorTypeRef<Descriptor> = Descriptor extends {
311
309
  ? TypeRef
312
310
  : undefined;
313
311
 
314
- type LookupNamedStorageTypeKeyByValue<
315
- Definition,
316
- TypeRef extends StorageTypeInstance | PostgresEnumStorageEntry,
317
- > = {
312
+ type LookupNamedStorageTypeKeyByValue<Definition, TypeRef extends StorageType> = {
318
313
  [TypeName in keyof DefinitionTypes<Definition> & string]: [TypeRef] extends [
319
314
  DefinitionTypes<Definition>[TypeName],
320
315
  ]
@@ -326,7 +321,7 @@ type LookupNamedStorageTypeKeyByValue<
326
321
 
327
322
  type ResolveNamedStorageTypeKey<Definition, TypeRef> = TypeRef extends string
328
323
  ? TypeRef
329
- : TypeRef extends StorageTypeInstance | PostgresEnumStorageEntry
324
+ : TypeRef extends StorageType
330
325
  ? [LookupNamedStorageTypeKeyByValue<Definition, TypeRef>] extends [never]
331
326
  ? string
332
327
  : LookupNamedStorageTypeKeyByValue<Definition, TypeRef>
@@ -339,17 +334,39 @@ type ResolveNamedStorageType<Definition, TypeRef> =
339
334
  : StorageTypeInstance
340
335
  : StorageTypeInstance;
341
336
 
342
- type ResolveFieldDescriptor<Definition, FieldState> = [FieldDescriptorOf<FieldState>] extends [
343
- never,
344
- ]
345
- ? ResolveNamedStorageType<Definition, FieldTypeRefOf<FieldState>>
346
- : FieldDescriptorOf<FieldState>;
337
+ // An enum-typed field carries its `EnumTypeHandle` (an object with a `codecId`
338
+ // and `nativeType`, but no `kind`) as the field's `typeRef`. It is neither a
339
+ // string nor a registered `StorageType`, so the named-type lookup cannot reach
340
+ // it; `EnumFieldHandle` short-circuits the resolvers to read codec + native type
341
+ // straight off the handle, with no column type-ref (the enum name is carried
342
+ // elsewhere). The `[...] extends [never]` guard excludes plain column fields,
343
+ // whose `typeRef` is `never`.
344
+ type EnumFieldHandle<FieldState> = [FieldTypeRefOf<FieldState>] extends [never]
345
+ ? never
346
+ : FieldTypeRefOf<FieldState> extends EnumTypeHandle
347
+ ? FieldTypeRefOf<FieldState>
348
+ : never;
347
349
 
348
- type ResolveFieldColumnTypeRef<Definition, FieldState> = [FieldTypeRefOf<FieldState>] extends [
350
+ type EnumHandleDescriptor<Handle> = Handle extends {
351
+ readonly codecId: infer CodecId extends string;
352
+ readonly nativeType: infer NativeType extends string;
353
+ }
354
+ ? { readonly codecId: CodecId; readonly nativeType: NativeType }
355
+ : never;
356
+
357
+ type ResolveFieldDescriptor<Definition, FieldState> = [EnumFieldHandle<FieldState>] extends [never]
358
+ ? [FieldDescriptorOf<FieldState>] extends [never]
359
+ ? ResolveNamedStorageType<Definition, FieldTypeRefOf<FieldState>>
360
+ : FieldDescriptorOf<FieldState>
361
+ : EnumHandleDescriptor<EnumFieldHandle<FieldState>>;
362
+
363
+ type ResolveFieldColumnTypeRef<Definition, FieldState> = [EnumFieldHandle<FieldState>] extends [
349
364
  never,
350
365
  ]
351
- ? DescriptorTypeRef<FieldDescriptorOf<FieldState>>
352
- : ResolveNamedStorageTypeKey<Definition, FieldTypeRefOf<FieldState>>;
366
+ ? [FieldTypeRefOf<FieldState>] extends [never]
367
+ ? DescriptorTypeRef<FieldDescriptorOf<FieldState>>
368
+ : ResolveNamedStorageTypeKey<Definition, FieldTypeRefOf<FieldState>>
369
+ : undefined;
353
370
 
354
371
  type ResolveFieldColumnTypeParams<Definition, FieldState> = [
355
372
  ResolveFieldColumnTypeRef<Definition, FieldState>,
@@ -540,6 +557,7 @@ type BuiltStorageTables<Definition> = {
540
557
  };
541
558
  readonly target: {
542
559
  readonly namespaceId: NamespaceId;
560
+ readonly spaceId?: string;
543
561
  readonly tableName: string;
544
562
  readonly columns: readonly string[];
545
563
  };
@@ -563,10 +581,41 @@ type BuiltStorageTables<Definition> = {
563
581
  : Record<string, never>);
564
582
  };
565
583
 
584
+ type DefinitionEnums<Definition> = Definition extends {
585
+ readonly enums?: infer E;
586
+ }
587
+ ? Present<E> extends Record<string, EnumTypeHandle>
588
+ ? // A bare `Record<string, EnumTypeHandle>` (no literal keys) is the
589
+ // widened default for a contract authored without enums; treat it as
590
+ // empty so `db.enums` carries only literally-authored enums.
591
+ string extends keyof Present<E>
592
+ ? Record<never, never>
593
+ : Present<E>
594
+ : Record<never, never>
595
+ : Record<never, never>;
596
+
597
+ type EnumHandleAccessorType<Handle> =
598
+ Handle extends EnumTypeHandle<infer _Name, infer Values, infer Names, infer MembersMap>
599
+ ? {
600
+ readonly values: Values;
601
+ readonly names: Names;
602
+ readonly members: MembersMap;
603
+ has(v: Values[number]): boolean;
604
+ nameOf(v: Values[number]): string | undefined;
605
+ ordinalOf(v: Values[number]): number;
606
+ }
607
+ : never;
608
+
609
+ type BuiltEnumAccessors<Definition> = {
610
+ readonly [K in keyof DefinitionEnums<Definition>]: EnumHandleAccessorType<
611
+ DefinitionEnums<Definition>[K]
612
+ >;
613
+ };
614
+
566
615
  type BuiltDocumentScopedTypes<Definition> = {
567
- readonly [K in keyof DefinitionTypes<Definition> as DefinitionTypes<Definition>[K] extends PostgresEnumStorageEntry
568
- ? never
569
- : K]: DefinitionTypes<Definition>[K];
616
+ readonly [K in keyof DefinitionTypes<Definition> as DefinitionTypes<Definition>[K] extends StorageTypeInstance
617
+ ? K
618
+ : never]: DefinitionTypes<Definition>[K];
570
619
  };
571
620
 
572
621
  type BuiltDomain<Definition> =
@@ -586,19 +635,20 @@ type BuiltStorage<Definition> = {
586
635
  readonly types?: BuiltDocumentScopedTypes<Definition>;
587
636
  // The primary namespace key is target-specific: Postgres uses `public` (the
588
637
  // default schema), all other SQL targets use `__unbound__`. The namespace
589
- // carries the narrowed tables shape so downstream DSL surfaces keep
638
+ // carries the narrowed `entries.table` shape so downstream DSL surfaces keep
590
639
  // literal-keyed access without an optional-narrowing dance. The shape is
591
640
  // described inline (rather than intersecting with `SqlStorage['namespaces']`)
592
641
  // so its `Readonly<Record<string, Namespace>>` index signature doesn't
593
- // collapse `keyof tables` to `string`. The literal object is still
594
- // structurally assignable to `SqlStorage['namespaces']` because every value
595
- // satisfies the framework `Namespace` interface.
642
+ // collapse slot keys to `string`. The literal object is still structurally
643
+ // assignable to `SqlStorage['namespaces']` because every value satisfies the
644
+ // framework `Namespace` interface.
596
645
  readonly namespaces: {
597
646
  readonly [K in DefaultStorageNamespaceId<Definition>]: {
598
647
  readonly id: K;
599
648
  readonly kind: string;
600
- readonly tables: BuiltStorageTables<Definition>;
601
- readonly enum?: Readonly<Record<string, PostgresEnumStorageEntry>>;
649
+ readonly entries: {
650
+ readonly table: BuiltStorageTables<Definition>;
651
+ };
602
652
  };
603
653
  } & {
604
654
  readonly [Ns in Exclude<
@@ -607,35 +657,60 @@ type BuiltStorage<Definition> = {
607
657
  >]: {
608
658
  readonly id: Ns;
609
659
  readonly kind: string;
610
- readonly tables: Record<never, never>;
611
- readonly enum?: Readonly<Record<string, PostgresEnumStorageEntry>>;
660
+ readonly entries: {
661
+ readonly table: Record<never, never>;
662
+ };
612
663
  };
613
664
  };
614
665
  };
615
666
 
616
- type FieldOutputType<
667
+ // The enum value union for an enum-typed field, or `never` for a non-enum
668
+ // field. The field's `typeRef` carries the authored `EnumTypeHandle`, whose
669
+ // `Values` tuple preserves the literal member values (text or numeric).
670
+ type EnumValueUnion<FieldState> = [FieldTypeRefOf<FieldState>] extends [
671
+ EnumTypeHandle<string, infer Values>,
672
+ ]
673
+ ? readonly unknown[] extends Values
674
+ ? never
675
+ : Values[number]
676
+ : never;
677
+
678
+ // The codec's `output` / `input` JS type for a field's column, before
679
+ // nullability. `unknown` when the codec is not in the definition's codec map.
680
+ type CodecChannelType<
617
681
  Definition,
618
682
  ModelName extends ModelNames<Definition>,
619
683
  FieldName extends ModelFieldNames<Definition, ModelName>,
684
+ Channel extends 'output' | 'input',
685
+ > = ModelStorageColumn<Definition, ModelName, FieldName>['codecId'] extends infer Id extends
686
+ keyof CodecTypesFromDefinition<Definition>
687
+ ? CodecTypesFromDefinition<Definition>[Id] extends { readonly [K in Channel]: infer T }
688
+ ? T
689
+ : unknown
690
+ : unknown;
691
+
692
+ // A field's read/write JS type: the enum value union when the field is
693
+ // enum-typed, otherwise the codec channel type, with column nullability applied.
694
+ type FieldChannelType<
695
+ Definition,
696
+ ModelName extends ModelNames<Definition>,
697
+ FieldName extends ModelFieldNames<Definition, ModelName>,
698
+ Channel extends 'output' | 'input',
620
699
  > =
621
- ModelStorageColumn<Definition, ModelName, FieldName> extends infer Col
622
- ? Col extends { readonly codecId: infer Id extends string }
623
- ? Id extends keyof CodecTypesFromDefinition<Definition>
624
- ? CodecTypesFromDefinition<Definition>[Id] extends { readonly output: infer O }
625
- ? Col extends { readonly nullable: true }
626
- ? O | null
627
- : O
628
- : unknown
629
- : unknown
630
- : unknown
631
- : unknown;
632
-
633
- type FieldOutputTypes<Definition> = {
700
+ | ([EnumValueUnion<ModelFieldState<Definition, ModelName, FieldName>>] extends [never]
701
+ ? CodecChannelType<Definition, ModelName, FieldName, Channel>
702
+ : EnumValueUnion<ModelFieldState<Definition, ModelName, FieldName>>)
703
+ | (FieldNullableOf<ModelFieldState<Definition, ModelName, FieldName>> extends true
704
+ ? null
705
+ : never);
706
+
707
+ type FieldChannelTypes<Definition, Channel extends 'output' | 'input'> = {
634
708
  readonly [ModelName in ModelNames<Definition>]: {
635
- readonly [FieldName in ModelFieldNames<Definition, ModelName>]: FieldOutputType<
709
+ readonly [FieldName in ModelFieldNames<Definition, ModelName>]: FieldChannelType<
636
710
  Definition,
637
711
  ModelName,
638
- FieldName
712
+ FieldName,
713
+ Channel
639
714
  >;
640
715
  };
641
716
  };
@@ -649,11 +724,12 @@ export type SqlContractResult<Definition> = ContractWithTypeMaps<
649
724
  ? Record<string, never>
650
725
  : DefinitionExtensionPacks<Definition>;
651
726
  readonly capabilities: DerivedCapabilities<Definition>;
727
+ readonly enumAccessors: BuiltEnumAccessors<Definition>;
652
728
  },
653
729
  TypeMaps<
654
730
  CodecTypesFromDefinition<Definition>,
655
731
  Record<string, never>,
656
- Record<string, never>,
657
- FieldOutputTypes<Definition>
732
+ FieldChannelTypes<Definition, 'output'>,
733
+ FieldChannelTypes<Definition, 'input'>
658
734
  >
659
735
  >;