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

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 (60) hide show
  1. package/dist/canonicalization-hooks.d.mts.map +1 -1
  2. package/dist/canonicalization-hooks.mjs +10 -11
  3. package/dist/canonicalization-hooks.mjs.map +1 -1
  4. package/dist/factories.d.mts +4 -2
  5. package/dist/factories.d.mts.map +1 -1
  6. package/dist/factories.mjs +3 -2
  7. package/dist/factories.mjs.map +1 -1
  8. package/dist/foreign-key-BATxB95l.d.mts +121 -0
  9. package/dist/foreign-key-BATxB95l.d.mts.map +1 -0
  10. package/dist/index-type-validation.d.mts +2 -2
  11. package/dist/index-type-validation.mjs +1 -1
  12. package/dist/index-type-validation.mjs.map +1 -1
  13. package/dist/{index-types-B1cf5N0F.d.mts → index-types-Czsyu7Iw.d.mts} +1 -1
  14. package/dist/{index-types-B1cf5N0F.d.mts.map → index-types-Czsyu7Iw.d.mts.map} +1 -1
  15. package/dist/index-types.d.mts +1 -1
  16. package/dist/pack-types.d.mts +1 -1
  17. package/dist/referential-action-sql.d.mts +12 -0
  18. package/dist/referential-action-sql.d.mts.map +1 -0
  19. package/dist/referential-action-sql.mjs +17 -0
  20. package/dist/referential-action-sql.mjs.map +1 -0
  21. package/dist/resolve-storage-table.d.mts +20 -0
  22. package/dist/resolve-storage-table.d.mts.map +1 -0
  23. package/dist/resolve-storage-table.mjs +42 -0
  24. package/dist/resolve-storage-table.mjs.map +1 -0
  25. package/dist/sql-storage-CXf9xjAL.d.mts +314 -0
  26. package/dist/sql-storage-CXf9xjAL.d.mts.map +1 -0
  27. package/dist/types-DEnWD3xB.d.mts +208 -0
  28. package/dist/types-DEnWD3xB.d.mts.map +1 -0
  29. package/dist/{types-DPkj4y3_.mjs → types-DqhaAjCH.mjs} +109 -28
  30. package/dist/types-DqhaAjCH.mjs.map +1 -0
  31. package/dist/types.d.mts +4 -2
  32. package/dist/types.mjs +2 -2
  33. package/dist/validators.d.mts +51 -14
  34. package/dist/validators.d.mts.map +1 -1
  35. package/dist/validators.mjs +116 -32
  36. package/dist/validators.mjs.map +1 -1
  37. package/package.json +11 -9
  38. package/src/canonicalization-hooks.ts +5 -6
  39. package/src/exports/referential-action-sql.ts +1 -0
  40. package/src/exports/resolve-storage-table.ts +1 -0
  41. package/src/exports/types.ts +6 -0
  42. package/src/factories.ts +2 -1
  43. package/src/index-type-validation.ts +1 -1
  44. package/src/ir/build-sql-namespace.ts +33 -19
  45. package/src/ir/check-constraint.ts +42 -0
  46. package/src/ir/foreign-key-reference.ts +23 -0
  47. package/src/ir/postgres-enum-storage-entry.ts +2 -0
  48. package/src/ir/sql-storage.ts +53 -50
  49. package/src/ir/sql-unbound-namespace.ts +11 -4
  50. package/src/ir/storage-column.ts +7 -1
  51. package/src/ir/storage-table.ts +10 -0
  52. package/src/ir/storage-type-instance.ts +5 -3
  53. package/src/ir/storage-value-set.ts +42 -0
  54. package/src/referential-action-sql.ts +14 -0
  55. package/src/resolve-storage-table.ts +61 -0
  56. package/src/types.ts +13 -0
  57. package/src/validators.ts +156 -46
  58. package/dist/types-ChlHcJCu.d.mts +0 -508
  59. package/dist/types-ChlHcJCu.d.mts.map +0 -1
  60. package/dist/types-DPkj4y3_.mjs.map +0 -1
@@ -1,4 +1,5 @@
1
1
  export type {
2
+ CheckConstraintInput,
2
3
  CodecTypesOf,
3
4
  ContractWithTypeMaps,
4
5
  ExtractCodecTypes,
@@ -21,6 +22,7 @@ export type {
21
22
  QueryOperationTypesOf,
22
23
  ReferentialAction,
23
24
  ResolveCodecTypes,
25
+ SqlControlDriverInstance,
24
26
  SqlModelFieldStorage,
25
27
  SqlModelStorage,
26
28
  SqlNamespaceTablesInput,
@@ -31,6 +33,7 @@ export type {
31
33
  StorageTableInput,
32
34
  StorageTypeInstance,
33
35
  StorageTypeInstanceInput,
36
+ StorageValueSetInput,
34
37
  TypeMaps,
35
38
  TypeMapsPhantomKey,
36
39
  UniqueConstraintInput,
@@ -39,6 +42,7 @@ export {
39
42
  applyFkDefaults,
40
43
  buildSqlNamespace,
41
44
  buildSqlNamespaceMap,
45
+ CheckConstraint,
42
46
  CODEC_INSTANCE_KIND,
43
47
  DEFAULT_FK_CONSTRAINT,
44
48
  DEFAULT_FK_INDEX,
@@ -54,6 +58,8 @@ export {
54
58
  SqlUnboundNamespace,
55
59
  StorageColumn,
56
60
  StorageTable,
61
+ StorageValueSet,
62
+ storageTableAt,
57
63
  toStorageTypeInstance,
58
64
  UniqueConstraint,
59
65
  } from '../types';
package/src/factories.ts CHANGED
@@ -72,12 +72,13 @@ export function model(
72
72
  tableName: string,
73
73
  fields: Record<string, SqlModelFieldStorage>,
74
74
  relations: Record<string, unknown> = {},
75
+ namespaceId: string = UNBOUND_NAMESPACE_ID,
75
76
  ): {
76
77
  storage: SqlModelStorage;
77
78
  fields: Record<string, { readonly nullable: boolean; readonly type: ScalarFieldType }>;
78
79
  relations: Record<string, unknown>;
79
80
  } {
80
- const storage: SqlModelStorage = { table: tableName, fields };
81
+ const storage: SqlModelStorage = { table: tableName, namespaceId, fields };
81
82
  const domainFields = Object.fromEntries(
82
83
  Object.entries(fields).map(([name, field]) => [
83
84
  name,
@@ -9,7 +9,7 @@ export function validateIndexTypes(
9
9
  indexTypeRegistry: IndexTypeRegistry,
10
10
  ): void {
11
11
  for (const [namespaceId, ns] of Object.entries(contract.storage.namespaces)) {
12
- for (const [tableName, rawTable] of Object.entries(ns.tables)) {
12
+ for (const [tableName, rawTable] of Object.entries(ns.entries.table)) {
13
13
  const table = rawTable as StorageTable;
14
14
  for (const index of table.indexes) {
15
15
  if (index.type === undefined && index.options !== undefined) {
@@ -5,10 +5,10 @@ import {
5
5
  UNBOUND_NAMESPACE_ID,
6
6
  } from '@prisma-next/framework-components/ir';
7
7
  import { blindCast, castAs } from '@prisma-next/utils/casts';
8
- import type { PostgresEnumStorageEntry } from './postgres-enum-storage-entry';
9
8
  import type { SqlNamespace, SqlNamespaceTablesInput } from './sql-storage';
10
9
  import { SqlUnboundNamespace } from './sql-unbound-namespace';
11
10
  import { StorageTable } from './storage-table';
11
+ import { StorageValueSet } from './storage-value-set';
12
12
 
13
13
  const SQL_NAMESPACE_KIND = 'sql-namespace' as const;
14
14
 
@@ -25,15 +25,18 @@ function isMaterializedSqlNamespace(ns: Namespace | SqlNamespaceTablesInput): ns
25
25
 
26
26
  class SqlBoundNamespace extends NamespaceBase {
27
27
  declare readonly kind: string;
28
- declare readonly enum?: Readonly<Record<string, PostgresEnumStorageEntry>>;
29
28
 
30
29
  readonly id: string;
31
- readonly tables: Readonly<Record<string, StorageTable>>;
30
+ readonly entries: Readonly<{
31
+ readonly table: Readonly<Record<string, StorageTable>>;
32
+ readonly valueSet?: Readonly<Record<string, StorageValueSet>>;
33
+ }>;
32
34
 
33
35
  static fromTablesInput(input: SqlNamespaceTablesInput): SqlNamespace {
34
- const tableCount = Object.keys(input.tables ?? {}).length;
35
- const enumCount = Object.keys(input.enum ?? {}).length;
36
- if (input.id === UNBOUND_NAMESPACE_ID && tableCount === 0 && enumCount === 0) {
36
+ const tableCount = Object.keys(input.entries.table).length;
37
+ const hasValueSets =
38
+ input.entries.valueSet !== undefined && Object.keys(input.entries.valueSet).length > 0;
39
+ if (input.id === UNBOUND_NAMESPACE_ID && tableCount === 0 && !hasValueSets) {
37
40
  return castAs<SqlNamespace>(SqlUnboundNamespace.instance);
38
41
  }
39
42
  return castAs<SqlNamespace>(new SqlBoundNamespace(input));
@@ -42,21 +45,20 @@ class SqlBoundNamespace extends NamespaceBase {
42
45
  private constructor(input: SqlNamespaceTablesInput) {
43
46
  super();
44
47
  this.id = input.id;
45
- this.tables = Object.freeze(
48
+ const table = Object.freeze(
46
49
  Object.fromEntries(
47
- Object.entries(input.tables ?? {}).map(([name, t]) => [
48
- name,
49
- t instanceof StorageTable ? t : new StorageTable(t),
50
- ]),
50
+ Object.entries(input.entries.table).map(([k, v]) => [k, new StorageTable(v)]),
51
51
  ),
52
52
  );
53
- if (input.enum !== undefined && Object.keys(input.enum).length > 0) {
54
- Object.defineProperty(this, 'enum', {
55
- value: Object.freeze({ ...input.enum }),
56
- writable: false,
57
- enumerable: true,
58
- configurable: false,
59
- });
53
+ if (input.entries.valueSet !== undefined) {
54
+ const valueSet = Object.freeze(
55
+ Object.fromEntries(
56
+ Object.entries(input.entries.valueSet).map(([k, v]) => [k, new StorageValueSet(v)]),
57
+ ),
58
+ );
59
+ this.entries = Object.freeze({ table, valueSet });
60
+ } else {
61
+ this.entries = Object.freeze({ table });
60
62
  }
61
63
  Object.defineProperty(this, 'kind', {
62
64
  value: SQL_NAMESPACE_KIND,
@@ -66,6 +68,13 @@ class SqlBoundNamespace extends NamespaceBase {
66
68
  });
67
69
  freezeNode(this);
68
70
  }
71
+
72
+ qualifyTable(tableName: string): string {
73
+ if (this.id === UNBOUND_NAMESPACE_ID) {
74
+ return `"${tableName}"`;
75
+ }
76
+ return `"${this.id}"."${tableName}"`;
77
+ }
69
78
  }
70
79
 
71
80
  export function buildSqlNamespace(input: SqlNamespaceTablesInput): SqlNamespace {
@@ -83,7 +92,12 @@ export function buildSqlNamespaceMap(
83
92
  SqlNamespace,
84
93
  'a materialised SQL-family namespace entry in a namespace map is a SqlNamespace'
85
94
  >(ns)
86
- : SqlBoundNamespace.fromTablesInput(ns),
95
+ : SqlBoundNamespace.fromTablesInput(
96
+ blindCast<
97
+ SqlNamespaceTablesInput,
98
+ 'non-materialized SQL namespace map entry is a SqlNamespaceTablesInput'
99
+ >(ns),
100
+ ),
87
101
  ]),
88
102
  );
89
103
  }
@@ -0,0 +1,42 @@
1
+ import type { ValueSetRef } from '@prisma-next/contract/types';
2
+ import { freezeNode } from '@prisma-next/framework-components/ir';
3
+ import { SqlNode } from './sql-node';
4
+
5
+ /**
6
+ * Hydration / construction input shape for {@link CheckConstraint}.
7
+ * Mirrors the on-disk storage JSON envelope so the serializer hydration
8
+ * walker can hand a validated literal straight to `new`.
9
+ */
10
+ export interface CheckConstraintInput {
11
+ readonly name: string;
12
+ readonly column: string;
13
+ readonly valueSet: ValueSetRef;
14
+ }
15
+
16
+ /**
17
+ * SQL Contract IR node for a table-level check constraint that restricts
18
+ * a column to the permitted values of a value-set.
19
+ *
20
+ * The constraint is **structured** (names a column and a value-set
21
+ * reference), not a raw SQL expression. Each target renders its own DDL
22
+ * from the structured form, keeping the contract target-agnostic.
23
+ *
24
+ * Construction is idempotent: passing an existing `CheckConstraint`
25
+ * instance as input produces a new instance with identical fields.
26
+ * The constructor does not use `instanceof` for input discrimination —
27
+ * it reads plain named properties, which is sufficient since
28
+ * `CheckConstraintInput` is a structural type.
29
+ */
30
+ export class CheckConstraint extends SqlNode {
31
+ readonly name: string;
32
+ readonly column: string;
33
+ readonly valueSet: ValueSetRef;
34
+
35
+ constructor(input: CheckConstraintInput) {
36
+ super();
37
+ this.name = input.name;
38
+ this.column = input.column;
39
+ this.valueSet = input.valueSet;
40
+ freezeNode(this);
41
+ }
42
+ }
@@ -2,16 +2,37 @@ import { asNamespaceId, type NamespaceId } from '@prisma-next/contract/types';
2
2
  import { freezeNode } from '@prisma-next/framework-components/ir';
3
3
  import { SqlNode } from './sql-node';
4
4
 
5
+ /**
6
+ * Input for a foreign-key reference (one side of a foreign-key declaration).
7
+ *
8
+ * When `spaceId` is absent the reference is local — the referenced table lives
9
+ * in the same contract-space. When `spaceId` is present the reference is
10
+ * cross-space — the referenced table lives in a different contract-space
11
+ * identified by `spaceId`.
12
+ *
13
+ * Presence-based discrimination keeps local FK JSON byte-identical to
14
+ * contracts authored before cross-space support was added.
15
+ */
5
16
  export interface ForeignKeyReferenceInput {
6
17
  readonly namespaceId: string;
7
18
  readonly tableName: string;
8
19
  readonly columns: readonly string[];
20
+ readonly spaceId?: string;
9
21
  }
10
22
 
11
23
  /**
12
24
  * SQL Contract IR node for one side (source or target) of a foreign-key
13
25
  * declaration. Carries the full coordinate: namespace, table, and columns.
14
26
  *
27
+ * Cross-space discrimination is based on `spaceId` presence: absent means
28
+ * local (same contract-space); present means cross-space (the referenced
29
+ * table lives in the contract-space identified by `spaceId`).
30
+ *
31
+ * For local references `spaceId` is absent from JSON, keeping the serialized
32
+ * shape byte-identical to contracts authored before cross-space support was
33
+ * added. For cross-space references `spaceId` appears in JSON so round-trips
34
+ * are lossless.
35
+ *
15
36
  * Use `UNBOUND_NAMESPACE_ID` from `@prisma-next/framework-components/ir`
16
37
  * as the sentinel `namespaceId` for single-namespace (unbound) references.
17
38
  */
@@ -19,12 +40,14 @@ export class ForeignKeyReference extends SqlNode {
19
40
  readonly namespaceId: NamespaceId;
20
41
  readonly tableName: string;
21
42
  readonly columns: readonly string[];
43
+ declare readonly spaceId?: string;
22
44
 
23
45
  constructor(input: ForeignKeyReferenceInput) {
24
46
  super();
25
47
  this.namespaceId = asNamespaceId(input.namespaceId);
26
48
  this.tableName = input.tableName;
27
49
  this.columns = input.columns;
50
+ if (input.spaceId !== undefined) this.spaceId = input.spaceId;
28
51
  freezeNode(this);
29
52
  }
30
53
  }
@@ -1,3 +1,4 @@
1
+ import type { ControlPolicy } from '@prisma-next/contract/types';
1
2
  import type { StorageType } from '@prisma-next/framework-components/ir';
2
3
 
3
4
  /**
@@ -42,6 +43,7 @@ export interface PostgresEnumStorageEntry extends StorageType {
42
43
  * present on raw JSON envelopes).
43
44
  */
44
45
  readonly codecId: string;
46
+ readonly control?: ControlPolicy;
45
47
  }
46
48
 
47
49
  /**
@@ -1,32 +1,30 @@
1
1
  import type { StorageHashBase } from '@prisma-next/contract/types';
2
2
  import { freezeNode, type Namespace, type Storage } from '@prisma-next/framework-components/ir';
3
- import {
4
- isPostgresEnumStorageEntry,
5
- type PostgresEnumStorageEntry,
6
- } from './postgres-enum-storage-entry';
7
3
  import { SqlNode } from './sql-node';
8
4
  import type { StorageTable, StorageTableInput } from './storage-table';
9
5
  import {
10
6
  isStorageTypeInstance,
11
7
  type StorageTypeInstance,
12
8
  type StorageTypeInstanceInput,
9
+ toStorageTypeInstance,
13
10
  } from './storage-type-instance';
11
+ import type { StorageValueSet, StorageValueSetInput } from './storage-value-set';
14
12
 
15
13
  /**
16
14
  * Polymorphic value type for document-scoped `SqlStorage.types` entries
17
- * (codec aliases / parameterised native type registrations). Postgres
18
- * native enum registrations live under
19
- * `storage.namespaces[namespaceId].enum` instead.
15
+ * (codec aliases / parameterised native type registrations).
16
+ *
17
+ * Postgres native enum registrations live under the postgres-specific
18
+ * `entries.type` slot on `PostgresSchema` (target layer), not here.
20
19
  */
21
- export type SqlStorageTypeEntry =
22
- | StorageTypeInstance
23
- | StorageTypeInstanceInput
24
- | PostgresEnumStorageEntry;
20
+ export type SqlStorageTypeEntry = StorageTypeInstance | StorageTypeInstanceInput;
25
21
 
26
22
  export interface SqlNamespaceTablesInput {
27
23
  readonly id: string;
28
- readonly tables?: Record<string, StorageTable | StorageTableInput>;
29
- readonly enum?: Record<string, PostgresEnumStorageEntry>;
24
+ readonly entries: {
25
+ readonly table: Record<string, StorageTable | StorageTableInput>;
26
+ readonly valueSet?: Record<string, StorageValueSet | StorageValueSetInput>;
27
+ };
30
28
  }
31
29
 
32
30
  export interface SqlStorageInput<THash extends string = string> {
@@ -51,30 +49,35 @@ export interface SqlStorageInput<THash extends string = string> {
51
49
  *
52
50
  * The constructor normalises optional `types` into class instances.
53
51
  * `types` is polymorphic per Decision 18 Option B: codec-triple inputs
54
- * are stamped with `kind: 'codec-instance'`; class-instance kinds
55
- * (e.g. Postgres-enum entries satisfying `PostgresEnumStorageEntry`)
56
- * pass through; hydration of raw JSON class-instance entries (carrying
57
- * their narrower `kind` literal) is the per-target serializer's
58
- * responsibility (so the family base does not import target-specific
59
- * subclasses).
52
+ * are stamped with `kind: 'codec-instance'`; hydration of raw JSON
53
+ * class-instance entries (carrying their narrower `kind` literal) is
54
+ * the per-target serializer's responsibility (so the family base does
55
+ * not import target-specific subclasses).
60
56
  */
61
- // SQL concretions always store `StorageTable`-shaped values in `tables`.
62
- // `tables` is a SQL-family idiom — the framework `Namespace` contract no
63
- // longer mandates this field; Mongo namespaces carry `collections`
64
- // instead. The `tables` slot uses the same narrowing as every other
65
- // SQL namespace; the wider `Record<string, object>` on `StorageTable` is
66
- // only there so emitted `contract.d.ts` table literals (which lack the
67
- // runtime `kind` discriminator on `StorageTable`) structurally satisfy
68
- // the slot without a class-instance check.
57
+ // SQL concretions store `StorageTable` values under `entries.table`.
58
+ // Mongo namespaces carry `entries.collection` instead. The wider
59
+ // `Record<string, object>` on `StorageTable` is only there so emitted
60
+ // `contract.d.ts` table literals (which lack the runtime `kind`
61
+ // discriminator on `StorageTable`) structurally satisfy the slot without
62
+ // a class-instance check.
69
63
  export type SqlNamespace = Namespace & {
70
- readonly tables: Readonly<Record<string, StorageTable>>;
71
- readonly enum?: Readonly<Record<string, PostgresEnumStorageEntry>>;
64
+ readonly entries: Readonly<{
65
+ readonly table: Readonly<Record<string, StorageTable>>;
66
+ readonly valueSet?: Readonly<Record<string, StorageValueSet>>;
67
+ }>;
68
+ /**
69
+ * Render a dialect-qualified table reference for runtime SQL emission.
70
+ * Present on materialised target concretions (`PostgresSchema`,
71
+ * `SqliteDatabase`, …) and family placeholders; omitted on emitted
72
+ * contract structural namespace literals (methods are not serialised).
73
+ */
74
+ qualifyTable?(tableName: string): string;
72
75
  };
73
76
 
74
77
  export class SqlStorage<THash extends string = string> extends SqlNode implements Storage {
75
78
  readonly storageHash: StorageHashBase<THash>;
76
79
  readonly namespaces: Readonly<Record<string, SqlNamespace>>;
77
- declare readonly types?: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
80
+ declare readonly types?: Readonly<Record<string, StorageTypeInstance>>;
78
81
 
79
82
  constructor(input: SqlStorageInput<THash>) {
80
83
  super();
@@ -91,11 +94,18 @@ export class SqlStorage<THash extends string = string> extends SqlNode implement
91
94
  }
92
95
  }
93
96
 
97
+ export function storageTableAt(
98
+ storage: SqlStorage,
99
+ namespaceId: string,
100
+ tableName: string,
101
+ ): StorageTable | undefined {
102
+ return storage.namespaces[namespaceId]?.entries.table[tableName];
103
+ }
104
+
94
105
  /**
95
106
  * Strict polymorphic-slot dispatch for `SqlStorage.types` entries.
96
- * Every entry must carry a recognised `kind` discriminator — either
97
- * `'codec-instance'` (codec triple, family-shared) or
98
- * `'postgres-enum'` (target-specific IR class). Untagged or
107
+ * Every entry must carry a `kind: 'codec-instance'` discriminator or
108
+ * be an already-constructed `StorageTypeInstance`. Untagged or
99
109
  * unrecognised inputs throw a diagnostic naming the entry and its
100
110
  * `kind`, so format drift surfaces loudly at the deserializer
101
111
  * boundary instead of slipping past the seam and corrupting
@@ -108,24 +118,17 @@ export class SqlStorage<THash extends string = string> extends SqlNode implement
108
118
  * arktype schema rejects untagged entries earlier, so this throw
109
119
  * only fires for in-memory authoring bugs.
110
120
  */
111
- function normaliseTypeEntry(
112
- name: string,
113
- entry: SqlStorageTypeEntry,
114
- ): StorageTypeInstance | PostgresEnumStorageEntry {
115
- if (isPostgresEnumStorageEntry(entry)) {
116
- // Live class instances pass through unchanged; raw JSON envelopes
117
- // (e.g. `kind: 'postgres-enum'` without the class identity) are
118
- // rejected so the target serializer's hydration path is the only
119
- // way IR class instances enter the slot.
120
- if (entry instanceof SqlNode) {
121
+ function normaliseTypeEntry(name: string, entry: SqlStorageTypeEntry): StorageTypeInstance {
122
+ if (isStorageTypeInstance(entry)) {
123
+ // Normalise on-disk objects that omit `typeParams` (the canonical on-disk
124
+ // form strips empty typeParams to keep JSON compact). The in-memory invariant
125
+ // is always `typeParams: {}` when empty — never `undefined`. Only create a
126
+ // new object when necessary to preserve identity-equality for callers that
127
+ // hold a reference to an already-correct in-memory entry.
128
+ if ('typeParams' in entry) {
121
129
  return entry;
122
130
  }
123
- throw new Error(
124
- `Encountered raw postgres-enum JSON in storage.types[${JSON.stringify(name)}] without serializer hydration; use a target ContractSerializer that registers the matching entity-type factory.`,
125
- );
126
- }
127
- if (isStorageTypeInstance(entry)) {
128
- return entry;
131
+ return toStorageTypeInstance(entry);
129
132
  }
130
133
  const rawKind = (entry as { kind?: unknown }).kind;
131
134
  const kindDescription =
@@ -133,6 +136,6 @@ function normaliseTypeEntry(
133
136
  ? 'missing `kind` discriminator'
134
137
  : `unrecognised \`kind\` discriminator ${JSON.stringify(rawKind)}`;
135
138
  throw new Error(
136
- `storage.types[${JSON.stringify(name)}] has ${kindDescription}; expected ${JSON.stringify('codec-instance')} or ${JSON.stringify('postgres-enum')}. Untagged codec triples should be wrapped with toStorageTypeInstance(...) before construction.`,
139
+ `storage.types[${JSON.stringify(name)}] has ${kindDescription}; expected ${JSON.stringify('codec-instance')}. Untagged codec triples should be wrapped with toStorageTypeInstance(...) before construction.`,
137
140
  );
138
141
  }
@@ -19,9 +19,10 @@ import type { StorageTable } from './storage-table';
19
19
  * envelope and runtime walk are honest at every layer.
20
20
  *
21
21
  * The `kind` discriminator is installed as a non-enumerable own property
22
- * so the JSON envelope reads `{ "id": "__unbound__" }` — symmetric
23
- * with the family-level non-enumerable `kind` on `SqlNode` and bounded
24
- * to the minimum data the framework `Namespace` interface promises.
22
+ * so the JSON envelope reads `{ "id": "__unbound__", "entries": { … } }`
23
+ * — symmetric with the family-level non-enumerable `kind` on `SqlNode`
24
+ * and bounded to the minimum data the framework `Namespace` interface
25
+ * promises.
25
26
  *
26
27
  * **Freeze-trap warning.** The leaf constructor calls
27
28
  * `freezeNode(this)` after installing `kind`. The leaf-class shape
@@ -39,7 +40,9 @@ export class SqlUnboundNamespace extends NamespaceBase {
39
40
  static readonly instance: SqlUnboundNamespace = new SqlUnboundNamespace();
40
41
 
41
42
  readonly id = UNBOUND_NAMESPACE_ID;
42
- readonly tables: Readonly<Record<string, StorageTable>> = Object.freeze({});
43
+ readonly entries: Readonly<{
44
+ readonly table: Readonly<Record<string, StorageTable>>;
45
+ }> = Object.freeze({ table: Object.freeze({}) });
43
46
  declare readonly kind: string;
44
47
 
45
48
  private constructor() {
@@ -52,4 +55,8 @@ export class SqlUnboundNamespace extends NamespaceBase {
52
55
  });
53
56
  freezeNode(this);
54
57
  }
58
+
59
+ qualifyTable(tableName: string): string {
60
+ return `"${tableName}"`;
61
+ }
55
62
  }
@@ -1,4 +1,4 @@
1
- import type { ColumnDefault } from '@prisma-next/contract/types';
1
+ import type { ColumnDefault, ControlPolicy, ValueSetRef } from '@prisma-next/contract/types';
2
2
  import { freezeNode } from '@prisma-next/framework-components/ir';
3
3
  import { SqlNode } from './sql-node';
4
4
 
@@ -19,6 +19,8 @@ export interface StorageColumnInput {
19
19
  readonly typeParams?: Record<string, unknown>;
20
20
  readonly typeRef?: string;
21
21
  readonly default?: ColumnDefault;
22
+ readonly control?: ControlPolicy;
23
+ readonly valueSet?: ValueSetRef;
22
24
  }
23
25
 
24
26
  /**
@@ -41,6 +43,8 @@ export class StorageColumn extends SqlNode {
41
43
  declare readonly typeParams?: Record<string, unknown>;
42
44
  declare readonly typeRef?: string;
43
45
  declare readonly default?: ColumnDefault;
46
+ declare readonly control?: ControlPolicy;
47
+ declare readonly valueSet?: ValueSetRef;
44
48
 
45
49
  constructor(input: StorageColumnInput) {
46
50
  super();
@@ -50,6 +54,8 @@ export class StorageColumn extends SqlNode {
50
54
  if (input.typeParams !== undefined) this.typeParams = input.typeParams;
51
55
  if (input.typeRef !== undefined) this.typeRef = input.typeRef;
52
56
  if (input.default !== undefined) this.default = input.default;
57
+ if (input.control !== undefined) this.control = input.control;
58
+ if (input.valueSet !== undefined) this.valueSet = input.valueSet;
53
59
  freezeNode(this);
54
60
  }
55
61
  }
@@ -1,4 +1,6 @@
1
+ import type { ControlPolicy } from '@prisma-next/contract/types';
1
2
  import { freezeNode } from '@prisma-next/framework-components/ir';
3
+ import { CheckConstraint, type CheckConstraintInput } from './check-constraint';
2
4
  import { ForeignKey, type ForeignKeyInput } from './foreign-key';
3
5
  import { PrimaryKey, type PrimaryKeyInput } from './primary-key';
4
6
  import { Index, type IndexInput } from './sql-index';
@@ -12,6 +14,8 @@ export interface StorageTableInput {
12
14
  readonly uniques: ReadonlyArray<UniqueConstraint | UniqueConstraintInput>;
13
15
  readonly indexes: ReadonlyArray<Index | IndexInput>;
14
16
  readonly foreignKeys: ReadonlyArray<ForeignKey | ForeignKeyInput>;
17
+ readonly control?: ControlPolicy;
18
+ readonly checks?: ReadonlyArray<CheckConstraint | CheckConstraintInput>;
15
19
  }
16
20
 
17
21
  /**
@@ -32,6 +36,8 @@ export class StorageTable extends SqlNode {
32
36
  readonly indexes: ReadonlyArray<Index>;
33
37
  readonly foreignKeys: ReadonlyArray<ForeignKey>;
34
38
  declare readonly primaryKey?: PrimaryKey;
39
+ declare readonly control?: ControlPolicy;
40
+ declare readonly checks?: ReadonlyArray<CheckConstraint>;
35
41
 
36
42
  constructor(input: StorageTableInput) {
37
43
  super();
@@ -56,6 +62,10 @@ export class StorageTable extends SqlNode {
56
62
  this.foreignKeys = Object.freeze(
57
63
  input.foreignKeys.map((fk) => (fk instanceof ForeignKey ? fk : new ForeignKey(fk))),
58
64
  );
65
+ if (input.control !== undefined) this.control = input.control;
66
+ if (input.checks !== undefined && input.checks.length > 0) {
67
+ this.checks = Object.freeze(input.checks.map((cc) => new CheckConstraint(cc)));
68
+ }
59
69
  freezeNode(this);
60
70
  }
61
71
  }
@@ -28,24 +28,26 @@ export interface StorageTypeInstance extends StorageType {
28
28
  * Construction-time input for a codec-triple entry. Symmetric with the
29
29
  * structural runtime shape minus the `kind` discriminator — callers may
30
30
  * omit `kind`; the helper {@link toStorageTypeInstance} stamps it on.
31
+ * `typeParams` may be omitted on input; the constructor normalises a
32
+ * missing value to `{}` so the in-memory shape is always present.
31
33
  */
32
34
  export interface StorageTypeInstanceInput {
33
35
  readonly codecId: string;
34
36
  readonly nativeType: string;
35
- readonly typeParams: Record<string, unknown>;
37
+ readonly typeParams?: Record<string, unknown>;
36
38
  }
37
39
 
38
40
  /**
39
41
  * Stamp the codec-instance `kind` discriminator on a caller-supplied
40
42
  * codec triple. Idempotent: input that already carries the discriminator
41
- * passes through unchanged.
43
+ * passes through unchanged. Missing `typeParams` is normalised to `{}`.
42
44
  */
43
45
  export function toStorageTypeInstance(input: StorageTypeInstanceInput): StorageTypeInstance {
44
46
  return {
45
47
  kind: CODEC_INSTANCE_KIND,
46
48
  codecId: input.codecId,
47
49
  nativeType: input.nativeType,
48
- typeParams: input.typeParams,
50
+ typeParams: input.typeParams ?? {},
49
51
  };
50
52
  }
51
53
 
@@ -0,0 +1,42 @@
1
+ import { freezeNode } from '@prisma-next/framework-components/ir';
2
+ import { SqlNode } from './sql-node';
3
+
4
+ /**
5
+ * Hydration / construction input shape for {@link StorageValueSet}.
6
+ * Mirrors the on-disk storage JSON envelope so the serializer hydration
7
+ * walker can hand a validated literal straight to `new`.
8
+ */
9
+ export interface StorageValueSetInput {
10
+ readonly kind: 'value-set';
11
+ /** Ordered permitted values, codec-encoded. Declaration order is preserved. */
12
+ readonly values: readonly string[];
13
+ }
14
+
15
+ /**
16
+ * SQL Contract IR node for a value-set entry in a namespace's `valueSet`
17
+ * map (`SqlNamespace.entries.valueSet`).
18
+ *
19
+ * A value-set records the ordered set of permitted codec-encoded values for
20
+ * an enum-like column restriction. It does not carry a `codecId` — the
21
+ * column that references it already holds the codec; the value-set holds
22
+ * only the permitted values.
23
+ *
24
+ * The node's `kind` is enumerable (`'value-set'`) so the JSON envelope
25
+ * carries the discriminator and the serializer hydration walker can
26
+ * dispatch on it. This follows the per-leaf enumerable-kind convention
27
+ * established in the SQL-node comment (future polymorphic dispatch on
28
+ * namespace entries needs the discriminator in JSON).
29
+ *
30
+ * The entry's name is not on the class — value-sets are keyed by name in
31
+ * the parent namespace's `valueSet: Record<string, StorageValueSet>` map.
32
+ */
33
+ export class StorageValueSet extends SqlNode {
34
+ override readonly kind = 'value-set' as const;
35
+ readonly values: readonly string[];
36
+
37
+ constructor(input: StorageValueSetInput) {
38
+ super();
39
+ this.values = Object.freeze([...input.values]);
40
+ freezeNode(this);
41
+ }
42
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReferentialAction } from './ir/foreign-key';
2
+
3
+ /**
4
+ * Maps each `ReferentialAction` value to the SQL keyword used in ON DELETE /
5
+ * ON UPDATE clauses. Shared across the migration planner and adapter DDL
6
+ * renderers — single source of truth for the action → SQL mapping.
7
+ */
8
+ export const REFERENTIAL_ACTION_SQL: Record<ReferentialAction, string> = {
9
+ noAction: 'NO ACTION',
10
+ restrict: 'RESTRICT',
11
+ cascade: 'CASCADE',
12
+ setNull: 'SET NULL',
13
+ setDefault: 'SET DEFAULT',
14
+ };