@prisma-next/sql-contract 0.13.0 → 0.14.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.
Files changed (48) hide show
  1. package/dist/{types-DqhaAjCH.mjs → entity-kinds-Cl36zL5j.mjs} +121 -205
  2. package/dist/entity-kinds-Cl36zL5j.mjs.map +1 -0
  3. package/dist/entity-kinds.d.mts +18 -0
  4. package/dist/entity-kinds.d.mts.map +1 -0
  5. package/dist/entity-kinds.mjs +2 -0
  6. package/dist/factories.d.mts +2 -2
  7. package/dist/factories.mjs +2 -1
  8. package/dist/factories.mjs.map +1 -1
  9. package/dist/index-type-validation.d.mts +1 -1
  10. package/dist/index-type-validation.mjs +9 -12
  11. package/dist/index-type-validation.mjs.map +1 -1
  12. package/dist/resolve-storage-table.d.mts +2 -1
  13. package/dist/resolve-storage-table.d.mts.map +1 -1
  14. package/dist/resolve-storage-table.mjs +11 -8
  15. package/dist/resolve-storage-table.mjs.map +1 -1
  16. package/dist/sql-storage-Dga0jwP2.d.mts +128 -0
  17. package/dist/sql-storage-Dga0jwP2.d.mts.map +1 -0
  18. package/dist/{sql-storage-CXf9xjAL.d.mts → storage-value-set-WnYsIFM8.d.mts} +8 -120
  19. package/dist/storage-value-set-WnYsIFM8.d.mts.map +1 -0
  20. package/dist/types-B-eiQXff.mjs +191 -0
  21. package/dist/types-B-eiQXff.mjs.map +1 -0
  22. package/dist/{types-DEnWD3xB.d.mts → types-B1N8w0I2.d.mts} +11 -62
  23. package/dist/types-B1N8w0I2.d.mts.map +1 -0
  24. package/dist/types.d.mts +4 -3
  25. package/dist/types.mjs +3 -2
  26. package/dist/validators.d.mts +75 -40
  27. package/dist/validators.d.mts.map +1 -1
  28. package/dist/validators.mjs +156 -184
  29. package/dist/validators.mjs.map +1 -1
  30. package/package.json +8 -7
  31. package/src/entity-kinds.ts +45 -0
  32. package/src/exports/entity-kinds.ts +5 -0
  33. package/src/exports/types.ts +2 -4
  34. package/src/index-type-validation.ts +2 -3
  35. package/src/ir/build-sql-namespace.ts +39 -32
  36. package/src/ir/sql-node.ts +2 -2
  37. package/src/ir/sql-storage.ts +22 -24
  38. package/src/ir/sql-unbound-namespace.ts +15 -3
  39. package/src/ir/storage-entry-schemas.ts +128 -0
  40. package/src/ir/storage-type-instance.ts +3 -3
  41. package/src/ir/storage-value-set.ts +6 -5
  42. package/src/resolve-storage-table.ts +12 -17
  43. package/src/types.ts +10 -10
  44. package/src/validators.ts +288 -225
  45. package/dist/sql-storage-CXf9xjAL.d.mts.map +0 -1
  46. package/dist/types-DEnWD3xB.d.mts.map +0 -1
  47. package/dist/types-DqhaAjCH.mjs.map +0 -1
  48. package/src/ir/postgres-enum-storage-entry.ts +0 -57
package/src/validators.ts CHANGED
@@ -6,50 +6,48 @@ import {
6
6
  CrossReferenceSchema,
7
7
  } from '@prisma-next/contract/types';
8
8
  import { validateContractDomain } from '@prisma-next/contract/validate-domain';
9
- import { type Namespace, UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
9
+ import {
10
+ type AnyEntityKindDescriptor,
11
+ isPlainRecord,
12
+ type Namespace,
13
+ UNBOUND_NAMESPACE_ID,
14
+ } from '@prisma-next/framework-components/ir';
10
15
  import { blindCast } from '@prisma-next/utils/casts';
11
16
  import { ifDefined } from '@prisma-next/utils/defined';
12
17
  import { type Type, type } from 'arktype';
18
+ import { composeSqlEntityKinds } from './entity-kinds';
13
19
  import { buildSqlNamespaceMap } from './ir/build-sql-namespace';
20
+
21
+ export {
22
+ CheckConstraintSchema,
23
+ ColumnDefaultFunctionSchema,
24
+ ColumnDefaultLiteralSchema,
25
+ ColumnDefaultSchema,
26
+ ForeignKeyReferenceSchema,
27
+ ForeignKeySchema,
28
+ ForeignKeySourceSchema,
29
+ IndexSchema,
30
+ ReferentialActionSchema,
31
+ StorageTableSchema,
32
+ StorageValueSetSchema,
33
+ } from './ir/storage-entry-schemas';
34
+
14
35
  import { SqlUnboundNamespace } from './ir/sql-unbound-namespace';
15
36
  import {
16
- type ForeignKeyInput,
17
- type ForeignKeyReferenceInput,
18
- type PrimaryKeyInput,
19
- type ReferentialAction,
20
37
  type SqlModelStorage,
21
38
  SqlStorage,
22
39
  type SqlStorageInput,
40
+ type StorageColumn,
23
41
  type StorageTable,
24
42
  type StorageTypeInstanceInput,
25
- type UniqueConstraintInput,
26
43
  } from './types';
27
44
 
28
- type ColumnDefaultLiteral = {
29
- readonly kind: 'literal';
30
- readonly value: string | number | boolean | Record<string, unknown> | unknown[] | null;
31
- };
32
- type ColumnDefaultFunction = { readonly kind: 'function'; readonly expression: string };
33
- const literalKindSchema = type("'literal'");
34
- const functionKindSchema = type("'function'");
35
45
  const generatorKindSchema = type("'generator'");
36
46
  const ControlPolicySchema = type("'managed' | 'tolerated' | 'external' | 'observed'");
37
47
  const generatorIdSchema = type('string').narrow((value, ctx) => {
38
48
  return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe('a flat generator id');
39
49
  });
40
50
 
41
- export const ColumnDefaultLiteralSchema = type.declare<ColumnDefaultLiteral>().type({
42
- kind: literalKindSchema,
43
- value: 'string | number | boolean | null | unknown[] | Record<string, unknown>',
44
- });
45
-
46
- export const ColumnDefaultFunctionSchema = type.declare<ColumnDefaultFunction>().type({
47
- kind: functionKindSchema,
48
- expression: 'string',
49
- });
50
-
51
- export const ColumnDefaultSchema = ColumnDefaultLiteralSchema.or(ColumnDefaultFunctionSchema);
52
-
53
51
  const ExecutionMutationDefaultValueSchema = type({
54
52
  '+': 'reject',
55
53
  kind: generatorKindSchema,
@@ -61,6 +59,7 @@ const ExecutionMutationDefaultSchema = type({
61
59
  '+': 'reject',
62
60
  ref: {
63
61
  '+': 'reject',
62
+ namespace: 'string',
64
63
  table: 'string',
65
64
  column: 'string',
66
65
  },
@@ -77,31 +76,14 @@ const ExecutionSchema = type({
77
76
  },
78
77
  });
79
78
 
80
- const ValueSetRefSchema = type({
81
- plane: "'domain' | 'storage'",
79
+ const DomainEnumRefSchema = type({
80
+ plane: "'domain'",
82
81
  namespaceId: 'string',
83
- entityKind: "'enum' | 'value-set'",
84
- name: 'string',
82
+ entityKind: "'enum'",
83
+ entityName: 'string',
85
84
  'spaceId?': 'string',
86
85
  });
87
86
 
88
- const StorageColumnSchema = type({
89
- '+': 'reject',
90
- nativeType: 'string',
91
- codecId: 'string',
92
- nullable: 'boolean',
93
- 'typeParams?': 'Record<string, unknown>',
94
- 'typeRef?': 'string',
95
- 'default?': ColumnDefaultSchema,
96
- 'control?': ControlPolicySchema,
97
- 'valueSet?': ValueSetRefSchema,
98
- }).narrow((col, ctx) => {
99
- if (col.typeParams !== undefined && col.typeRef !== undefined) {
100
- return ctx.mustBe('a column with either typeParams or typeRef, not both');
101
- }
102
- return true;
103
- });
104
-
105
87
  /**
106
88
  * Codec-triple entry persisted under `storage.types[name]`. Carries an
107
89
  * enumerable literal `kind: 'codec-instance'` discriminator so the
@@ -117,32 +99,9 @@ const StorageTypeInstanceSchema = type
117
99
  'typeParams?': 'Record<string, unknown>',
118
100
  });
119
101
 
120
- /**
121
- * Postgres native enum entry under `storage.namespaces[namespaceId].entries.type[name]`.
122
- * Document-scoped `storage.types` carries codec aliases only
123
- * (`DocumentScopedStorageTypeSchema`).
124
- */
125
- const PostgresEnumTypeSchema = type({
126
- kind: "'postgres-enum'",
127
- 'name?': 'string',
128
- 'nativeType?': 'string',
129
- values: type.string.array().readonly(),
130
- 'control?': ControlPolicySchema,
131
- });
132
-
133
102
  /** Document-scoped `storage.types`: codec triples only. */
134
103
  const DocumentScopedStorageTypeSchema = StorageTypeInstanceSchema;
135
104
 
136
- /**
137
- * Storage value-set entry under `storage.namespaces[id].entries.valueSet[name]`.
138
- * Carries a `kind: 'value-set'` discriminator (enumerable, survives JSON) and an
139
- * ordered `values` array of codec-encoded permitted values.
140
- */
141
- export const StorageValueSetSchema = type({
142
- kind: "'value-set'",
143
- values: type.string.array().readonly(),
144
- });
145
-
146
105
  /**
147
106
  * Domain enum entry under `domain.namespaces[id].enum[name]`.
148
107
  * Carries the codec id and an ordered `members` array of `{name, value}` pairs.
@@ -152,174 +111,82 @@ export const ContractEnumSchema = type({
152
111
  codecId: 'string',
153
112
  members: type({
154
113
  name: 'string',
155
- value: 'string',
114
+ value: 'string | number | boolean | null | unknown[] | Record<string, unknown>',
156
115
  })
157
116
  .array()
158
117
  .readonly(),
159
118
  });
160
119
 
161
- const PrimaryKeySchema = type.declare<PrimaryKeyInput>().type({
162
- columns: type.string.array().readonly(),
163
- 'name?': 'string',
164
- });
165
-
166
- const UniqueConstraintSchema = type.declare<UniqueConstraintInput>().type({
167
- columns: type.string.array().readonly(),
168
- 'name?': 'string',
169
- });
170
-
171
- export const IndexSchema = type({
172
- columns: type.string.array().readonly(),
173
- 'name?': 'string',
174
- 'type?': 'string',
175
- 'options?': 'Record<string, unknown>',
176
- });
177
-
178
- export const ForeignKeyReferenceSchema = type({
179
- '+': 'reject',
180
- namespaceId: 'string',
181
- tableName: 'string',
182
- columns: type.string.array().readonly(),
183
- 'spaceId?': 'string',
184
- }) satisfies Type<ForeignKeyReferenceInput>;
185
-
186
- export const ForeignKeySourceSchema = type({
187
- '+': 'reject',
188
- namespaceId: 'string',
189
- tableName: 'string',
190
- columns: type.string.array().readonly(),
191
- }) satisfies Type<ForeignKeyReferenceInput>;
192
-
193
- export const ReferentialActionSchema = type
194
- .declare<ReferentialAction>()
195
- .type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
196
-
197
- export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
198
- source: ForeignKeySourceSchema,
199
- target: ForeignKeyReferenceSchema,
200
- 'name?': 'string',
201
- 'onDelete?': ReferentialActionSchema,
202
- 'onUpdate?': ReferentialActionSchema,
203
- constraint: 'boolean',
204
- index: 'boolean',
205
- });
206
-
207
- export const CheckConstraintSchema = type({
208
- '+': 'reject',
209
- name: 'string',
210
- column: 'string',
211
- valueSet: ValueSetRefSchema,
212
- });
213
-
214
- const StorageTableSchema = type({
215
- '+': 'reject',
216
- columns: type({ '[string]': StorageColumnSchema }),
217
- 'primaryKey?': PrimaryKeySchema,
218
- uniques: UniqueConstraintSchema.array().readonly(),
219
- indexes: IndexSchema.array().readonly(),
220
- foreignKeys: ForeignKeySchema.array().readonly(),
221
- 'control?': ControlPolicySchema,
222
- 'checks?': CheckConstraintSchema.array().readonly(),
223
- });
224
-
225
120
  /**
226
- * Re-exported so target packs can register their `validatorSchema`
227
- * fragment without re-declaring the schema for the kinds the family
228
- * core already validates. Full extraction of enum-specific schemas
229
- * into the Postgres pack is a follow-up; today the symbol lives here.
121
+ * Derives a schema map from a descriptor map: maps each kind's key to its
122
+ * `schema` field. Used by validation functions to validate entries.
230
123
  */
231
- export { PostgresEnumTypeSchema };
124
+ function schemaViewOf(
125
+ kinds: ReadonlyMap<string, AnyEntityKindDescriptor>,
126
+ ): ReadonlyMap<string, Type<unknown>> {
127
+ return new Map([...kinds].map(([k, d]) => [k, d.schema]));
128
+ }
129
+
130
+ const DEFAULT_SQL_KINDS = composeSqlEntityKinds();
232
131
 
233
132
  /**
234
- * Composes a hardcoded family `fallback` schema with optional
235
- * pack-contributed `fragments` keyed by the entry's `kind`
236
- * discriminator. The composition is **additive**, not substitutive:
133
+ * Builds the per-namespace entry schema for `storage.namespaces[id]`.
237
134
  *
238
- * - No fragments registered entries are validated by `fallback`
239
- * alone (the unchanged baseline).
240
- * - An entry's `kind` matches `fallbackKind` AND a fragment for that
241
- * kind is registered → the entry must pass **both** `fallback` and
242
- * the fragment. This preserves family-owned invariants (e.g. the
243
- * built-in `PostgresEnumType` shape) even when a pack contributes
244
- * its own schema for the same kind.
245
- * - An entry's `kind` matches a registered fragment for some
246
- * non-fallback kind → the fragment alone validates the entry.
247
- * `fallback` is family-specific (validates a single hardcoded kind)
248
- * and would reject any other kind, so it does not apply here.
249
- * - An entry's `kind` matches no fragment → fall through to
250
- * `fallback`.
135
+ * Validation is descriptor-driven: the `kinds` map carries both the schema
136
+ * (used here for structural validation) and the construct function (used at
137
+ * hydration time). An unregistered key fails validation naming the kind and
138
+ * the namespace id, so validation fails closed.
251
139
  */
252
- function namespaceSlotEntrySchema(
253
- fallback: Type<unknown>,
254
- fallbackKind: string,
255
- fragments?: ReadonlyMap<string, Type<unknown>>,
140
+ export function createNamespaceEntrySchema(
141
+ kinds: ReadonlyMap<string, AnyEntityKindDescriptor>,
256
142
  ): Type<unknown> {
257
- if (fragments === undefined || fragments.size === 0) {
258
- return fallback;
259
- }
260
- return type('unknown').narrow((entry, ctx) => {
261
- if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
262
- return ctx.mustBe('an object');
143
+ const schemas = schemaViewOf(kinds);
144
+ const knownKinds = new Set(kinds.keys());
145
+ return type({
146
+ '+': 'reject',
147
+ id: 'string',
148
+ 'kind?': 'string',
149
+ entries: 'object',
150
+ }).narrow((ns, ctx) => {
151
+ if (!isPlainRecord(ns.entries)) {
152
+ return ctx.mustBe('an entries object');
263
153
  }
264
- const kind = (entry as { kind?: unknown }).kind;
265
- if (typeof kind === 'string') {
266
- const fragment = fragments.get(kind);
267
- if (fragment !== undefined) {
268
- if (kind === fallbackKind) {
269
- const baseParsed = fallback(entry);
270
- if (baseParsed instanceof type.errors) {
271
- return ctx.reject({ expected: baseParsed.summary });
272
- }
273
- }
274
- const parsed = fragment(entry);
154
+ for (const [key, innerMap] of Object.entries(ns.entries)) {
155
+ if (!knownKinds.has(key)) {
156
+ return ctx.reject({
157
+ expected: `entries key "${key}" in namespace "${ns.id}" is not a registered entity kind`,
158
+ });
159
+ }
160
+ if (!isPlainRecord(innerMap)) {
161
+ return ctx.reject({
162
+ expected: `entries["${key}"] in namespace "${ns.id}" must be an object`,
163
+ });
164
+ }
165
+ const entrySchema = blindCast<
166
+ Type<unknown>,
167
+ 'knownKinds.has(key) guarantees schemas.get(key) is defined'
168
+ >(schemas.get(key));
169
+ for (const [, value] of Object.entries(innerMap)) {
170
+ const parsed = entrySchema(value);
275
171
  if (parsed instanceof type.errors) {
276
172
  return ctx.reject({ expected: parsed.summary });
277
173
  }
278
- return true;
279
174
  }
280
175
  }
281
- const parsed = fallback(entry);
282
- if (parsed instanceof type.errors) {
283
- return ctx.reject({ expected: parsed.summary });
284
- }
285
176
  return true;
286
- });
287
- }
288
-
289
- /**
290
- * Builds the per-namespace entry schema for `storage.namespaces[id]`.
291
- * Pack-contributed `validatorSchema` fragments — keyed by the
292
- * descriptor's `discriminator` — validate each entry by matching the
293
- * entry's `kind` field on the `'entries.type'` slot.
294
- */
295
- export function createNamespaceEntrySchema(
296
- fragments?: ReadonlyMap<string, Type<unknown>>,
297
- ): Type<unknown> {
298
- return type({
299
- '+': 'reject',
300
- id: 'string',
301
- 'kind?': 'string',
302
- entries: type({
303
- '+': 'reject',
304
- 'table?': type({ '[string]': StorageTableSchema }),
305
- 'type?': type({
306
- '[string]': namespaceSlotEntrySchema(PostgresEnumTypeSchema, 'postgres-enum', fragments),
307
- }),
308
- 'valueSet?': type({ '[string]': StorageValueSetSchema }),
309
- }),
310
177
  }) as Type<unknown>;
311
178
  }
312
179
 
313
180
  /**
314
181
  * Builds the storage schema. Pack contributions reach the per-namespace
315
182
  * entry shape through {@link createNamespaceEntrySchema}; the
316
- * document-scoped `storage.types` slot (codec triples only) and the
183
+ * document-scoped `storage.types` field (codec triples only) and the
317
184
  * storage hash stay family-shared.
318
185
  */
319
186
  export function createSqlStorageSchema(
320
- fragments?: ReadonlyMap<string, Type<unknown>>,
187
+ kinds: ReadonlyMap<string, AnyEntityKindDescriptor>,
321
188
  ): Type<unknown> {
322
- const namespaceEntry = createNamespaceEntrySchema(fragments);
189
+ const namespaceEntry = createNamespaceEntrySchema(kinds);
323
190
  return type({
324
191
  '+': 'reject',
325
192
  storageHash: 'string',
@@ -333,24 +200,20 @@ export function createSqlStorageSchema(
333
200
  }) as Type<unknown>;
334
201
  }
335
202
 
336
- const StorageSchema = createSqlStorageSchema();
203
+ const StorageSchema = createSqlStorageSchema(DEFAULT_SQL_KINDS);
337
204
 
338
- // SQL-specific namespace walk shape (`entries.table` is the SQL family's
339
- // idiom). The wider `object` table value keeps this helper structurally
340
- // compatible with `SqlNamespace` and JSON envelope variants that lose class
341
- // identity.
342
205
  type NamespacedStorageWalk = {
343
206
  readonly namespaces: Readonly<
344
207
  Record<
345
208
  string,
346
- Namespace & { readonly entries: { readonly table: Readonly<Record<string, object>> } }
209
+ Namespace & { readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>> }
347
210
  >
348
211
  >;
349
212
  };
350
213
 
351
214
  function eachStorageTable(storage: NamespacedStorageWalk) {
352
215
  return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) =>
353
- Object.entries(ns.entries.table).map(([tableName, table]) => ({
216
+ Object.entries(ns.entries['table'] ?? {}).map(([tableName, table]) => ({
354
217
  namespaceId,
355
218
  tableName,
356
219
  table,
@@ -358,10 +221,6 @@ function eachStorageTable(storage: NamespacedStorageWalk) {
358
221
  );
359
222
  }
360
223
 
361
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
362
- return typeof value === 'object' && value !== null && !Array.isArray(value);
363
- }
364
-
365
224
  function findDuplicateValue(values: readonly string[]): string | undefined {
366
225
  const seen = new Set<string>();
367
226
  for (const value of values) {
@@ -403,7 +262,7 @@ const ModelFieldSchema = type({
403
262
  type: ContractFieldTypeSchema,
404
263
  'many?': 'true',
405
264
  'dict?': 'true',
406
- 'valueSet?': ValueSetRefSchema,
265
+ 'valueSet?': DomainEnumRefSchema,
407
266
  });
408
267
 
409
268
  const ModelStorageFieldSchema = type({
@@ -480,9 +339,9 @@ const ContractMetaSchema = type({
480
339
  * of the contract envelope is family-shared.
481
340
  */
482
341
  export function createSqlContractSchema(
483
- fragments?: ReadonlyMap<string, Type<unknown>>,
342
+ kinds: ReadonlyMap<string, AnyEntityKindDescriptor>,
484
343
  ): Type<unknown> {
485
- const storage = createSqlStorageSchema(fragments);
344
+ const storage = createSqlStorageSchema(kinds);
486
345
  return type({
487
346
  '+': 'reject',
488
347
  target: 'string',
@@ -508,7 +367,7 @@ export function createSqlContractSchema(
508
367
  }) as Type<unknown>;
509
368
  }
510
369
 
511
- const SqlContractSchema = createSqlContractSchema();
370
+ const SqlContractSchema = createSqlContractSchema(DEFAULT_SQL_KINDS);
512
371
 
513
372
  // NOTE: StorageColumnSchema, StorageTableSchema, and StorageSchema use bare type()
514
373
  // instead of type.declare<T>().type() because the ColumnDefault union's value field
@@ -792,7 +651,8 @@ export function validateModelStorageReferences(contract: Contract<SqlStorage>):
792
651
  }
793
652
 
794
653
  const storageTable = model.storage.table;
795
- const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table[storageTable];
654
+ const storageNs = contract.storage.namespaces[storageNamespaceId];
655
+ const rawTable = storageNs?.entries.table?.[storageTable];
796
656
  if (rawTable === undefined) {
797
657
  throw new ContractValidationError(
798
658
  `Model "${qualifiedName}" references non-existent table "${storageNamespaceId}.${storageTable}"`,
@@ -912,7 +772,7 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
912
772
 
913
773
  if (fk.target.spaceId === undefined) {
914
774
  const targetNamespace = contract.storage.namespaces[fk.target.namespaceId];
915
- const referencedRaw = targetNamespace?.entries.table[fk.target.tableName];
775
+ const referencedRaw = targetNamespace?.entries.table?.[fk.target.tableName];
916
776
  if (referencedRaw === undefined) {
917
777
  throw new ContractValidationError(
918
778
  `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,
@@ -987,5 +847,208 @@ export function validateSqlContractFully<T extends Contract<SqlStorage>>(
987
847
  );
988
848
  }
989
849
  validateModelStorageReferences(validated);
850
+ validateRelationThroughConsistency(validated);
990
851
  return validated;
991
852
  }
853
+
854
+ /** Storage column lookup for through-consistency validation. */
855
+ function lookupStorageColumn(
856
+ contract: Contract<SqlStorage>,
857
+ namespaceId: string,
858
+ tableName: string,
859
+ columnName: string,
860
+ ): StorageColumn | undefined {
861
+ const rawTable = contract.storage.namespaces[namespaceId]?.entries.table?.[tableName];
862
+ if (rawTable === undefined) {
863
+ return undefined;
864
+ }
865
+ const table = blindCast<StorageTable, 'structurally validated storage table'>(rawTable);
866
+ return table.columns[columnName];
867
+ }
868
+
869
+ /**
870
+ * Two storage columns share a type when their `nativeType` and `typeParams`
871
+ * match. The contract is canonicalized, so `typeParams` key order is stable and
872
+ * a JSON comparison is exact. `codecId` and `nullable` are intentionally not
873
+ * compared: they do not change the database-level type that governs a join.
874
+ */
875
+ function sameStorageType(a: StorageColumn, b: StorageColumn): boolean {
876
+ return (
877
+ a.nativeType === b.nativeType &&
878
+ JSON.stringify(a.typeParams ?? null) === JSON.stringify(b.typeParams ?? null)
879
+ );
880
+ }
881
+
882
+ function describeColumnType(column: StorageColumn): string {
883
+ return column.typeParams === undefined
884
+ ? column.nativeType
885
+ : `${column.nativeType} ${JSON.stringify(column.typeParams)}`;
886
+ }
887
+
888
+ /**
889
+ * Validates one side of an N:M join: the junction columns and the model
890
+ * columns they pair against positionally must be equal in number, exist in
891
+ * their tables, and share the same storage type (`nativeType` + `typeParams`).
892
+ * The junction's storage foreign keys already guarantee this for user-declared
893
+ * FK constraints, but `through` is a logical descriptor never tied to them by
894
+ * the rest of validation — and the TS builder accepts explicit join columns
895
+ * without requiring a junction FK at all — so this checks the columns directly
896
+ * against storage, one path regardless of how the junction was authored.
897
+ *
898
+ * Joined columns must be the *same* storage type, not merely compatible:
899
+ * relying on implicit conversion (e.g. `text`↔`character`) is unsafe on writes
900
+ * — `character(n)` space-padding makes such coercions non-associative — and no
901
+ * ADR sanctions heterogeneous junction columns. Equality is the conservative
902
+ * default; it can be relaxed deliberately if a real use case ever appears.
903
+ */
904
+ function validateThroughJoinSide(input: {
905
+ readonly contract: Contract<SqlStorage>;
906
+ readonly qualifiedName: string;
907
+ readonly modelColumns: readonly string[];
908
+ readonly modelColumnsLabel: string;
909
+ /**
910
+ * The model side of the join, when resolvable. Absent for a cross-space
911
+ * target whose storage lives in another contract: the length and
912
+ * junction-column-existence checks still run, but the target-column
913
+ * existence and type checks are skipped because the target table is not
914
+ * available here. `fieldToColumn` is present when `modelColumns` are domain
915
+ * field names (the `on.*Fields` arrays), absent when they are already storage
916
+ * column names (the `through.*Columns` arrays).
917
+ */
918
+ readonly model?: {
919
+ readonly namespaceId: string;
920
+ readonly table: string;
921
+ readonly fieldToColumn?: Readonly<Record<string, { readonly column: string }>>;
922
+ };
923
+ readonly junctionColumns: readonly string[];
924
+ readonly junctionColumnsLabel: string;
925
+ readonly junctionNamespaceId: string;
926
+ readonly junctionTable: string;
927
+ }): void {
928
+ const fail = (detail: string): ContractValidationError =>
929
+ new ContractValidationError(
930
+ `Many-to-many relation "${input.qualifiedName}" ${detail}`,
931
+ 'storage',
932
+ );
933
+ if (input.junctionColumns.length !== input.modelColumns.length) {
934
+ throw fail(
935
+ `pairs ${input.junctionColumnsLabel} (${input.junctionColumns.length}) with ${input.modelColumnsLabel} (${input.modelColumns.length}) of differing length; they join positionally and must match.`,
936
+ );
937
+ }
938
+ for (const [index, junctionColumnName] of input.junctionColumns.entries()) {
939
+ const modelColumnRef = input.modelColumns[index];
940
+ if (modelColumnRef === undefined) {
941
+ continue;
942
+ }
943
+ const junctionColumn = lookupStorageColumn(
944
+ input.contract,
945
+ input.junctionNamespaceId,
946
+ input.junctionTable,
947
+ junctionColumnName,
948
+ );
949
+ if (junctionColumn === undefined) {
950
+ throw fail(
951
+ `${input.junctionColumnsLabel} references column "${junctionColumnName}" absent from junction table "${input.junctionNamespaceId}.${input.junctionTable}".`,
952
+ );
953
+ }
954
+ // Cross-space target: its storage lives in another contract, so the target
955
+ // column's existence and type cannot be checked here. Length and
956
+ // junction-column existence have already been validated above.
957
+ const model = input.model;
958
+ if (model === undefined) {
959
+ continue;
960
+ }
961
+ let modelColumnName = modelColumnRef;
962
+ if (model.fieldToColumn !== undefined) {
963
+ const mapped = model.fieldToColumn[modelColumnRef];
964
+ if (mapped === undefined) {
965
+ throw fail(
966
+ `${input.modelColumnsLabel} references field "${modelColumnRef}" absent from model on table "${model.namespaceId}.${model.table}".`,
967
+ );
968
+ }
969
+ modelColumnName = mapped.column;
970
+ }
971
+ const modelColumn = lookupStorageColumn(
972
+ input.contract,
973
+ model.namespaceId,
974
+ model.table,
975
+ modelColumnName,
976
+ );
977
+ if (modelColumn === undefined) {
978
+ throw fail(
979
+ `${input.modelColumnsLabel} references column "${modelColumnName}" absent from table "${model.namespaceId}.${model.table}".`,
980
+ );
981
+ }
982
+ if (!sameStorageType(junctionColumn, modelColumn)) {
983
+ throw fail(
984
+ `joins "${input.junctionTable}.${junctionColumnName}" (${describeColumnType(junctionColumn)}) with "${model.table}.${modelColumnName}" (${describeColumnType(modelColumn)}) of differing storage type; junction columns must match the type of the column they reference.`,
985
+ );
986
+ }
987
+ }
988
+ }
989
+
990
+ /**
991
+ * Validates that every N:M relation's `through` descriptor is consistent with
992
+ * the storage columns it joins: both join sides match in column count,
993
+ * reference columns that exist in their tables, and pair columns of the same
994
+ * storage type. Without this, a `through` that disagrees with storage surfaces
995
+ * as a silently wrong JOIN at query time rather than a validation error here.
996
+ */
997
+ function validateRelationThroughConsistency(contract: Contract<SqlStorage>): void {
998
+ for (const [namespaceId, namespace] of Object.entries(contract.domain.namespaces)) {
999
+ for (const [modelName, model] of Object.entries(namespace.models)) {
1000
+ for (const [relationName, relation] of Object.entries(model.relations)) {
1001
+ if (relation.cardinality !== 'N:M') continue;
1002
+ const qualifiedName = `${namespaceId}.${modelName}.${relationName}`;
1003
+ const { on, through } = relation;
1004
+ const modelStorage = blindCast<SqlModelStorage, 'SQL contract model storage'>(
1005
+ model.storage,
1006
+ );
1007
+ // Parent side: the owning model's localFields (domain field names) join
1008
+ // the junction's parentColumns (storage columns).
1009
+ validateThroughJoinSide({
1010
+ contract,
1011
+ qualifiedName,
1012
+ modelColumns: on.localFields,
1013
+ modelColumnsLabel: 'on.localFields',
1014
+ model: {
1015
+ namespaceId,
1016
+ table: modelStorage.table,
1017
+ fieldToColumn: modelStorage.fields,
1018
+ },
1019
+ junctionColumns: through.parentColumns,
1020
+ junctionColumnsLabel: 'through.parentColumns',
1021
+ junctionNamespaceId: through.namespaceId,
1022
+ junctionTable: through.table,
1023
+ });
1024
+ // Child side: the junction's childColumns join the target model's
1025
+ // targetColumns. Length and junction-column existence are checked
1026
+ // regardless; a cross-space target lives in another contract, so its
1027
+ // column existence and type are checked only when it is resolvable here.
1028
+ const targetModel =
1029
+ relation.to.space === undefined
1030
+ ? contract.domain.namespaces[relation.to.namespace]?.models[relation.to.model]
1031
+ : undefined;
1032
+ const targetModelSide =
1033
+ targetModel === undefined
1034
+ ? undefined
1035
+ : {
1036
+ namespaceId: relation.to.namespace,
1037
+ table: blindCast<SqlModelStorage, 'SQL contract model storage'>(targetModel.storage)
1038
+ .table,
1039
+ };
1040
+ validateThroughJoinSide({
1041
+ contract,
1042
+ qualifiedName,
1043
+ modelColumns: through.targetColumns,
1044
+ modelColumnsLabel: 'through.targetColumns',
1045
+ ...ifDefined('model', targetModelSide),
1046
+ junctionColumns: through.childColumns,
1047
+ junctionColumnsLabel: 'through.childColumns',
1048
+ junctionNamespaceId: through.namespaceId,
1049
+ junctionTable: through.table,
1050
+ });
1051
+ }
1052
+ }
1053
+ }
1054
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"sql-storage-CXf9xjAL.d.mts","names":[],"sources":["../src/ir/check-constraint.ts","../src/ir/primary-key.ts","../src/ir/sql-index.ts","../src/ir/storage-column.ts","../src/ir/unique-constraint.ts","../src/ir/storage-table.ts","../src/ir/storage-type-instance.ts","../src/ir/storage-value-set.ts","../src/ir/sql-storage.ts"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-DEnWD3xB.d.mts","names":[],"sources":["../src/ir/build-sql-namespace.ts","../src/ir/postgres-enum-storage-entry.ts","../src/ir/sql-unbound-namespace.ts","../src/types.ts"],"mappings":"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"}