@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
@@ -0,0 +1,61 @@
1
+ import type { SqlNamespace, SqlStorage } from './ir/sql-storage';
2
+ import type { StorageTable } from './ir/storage-table';
3
+
4
+ export interface ResolvedStorageTable {
5
+ readonly namespaceId: string;
6
+ readonly table: StorageTable;
7
+ }
8
+
9
+ function tableInNamespace(
10
+ namespace: SqlNamespace | undefined,
11
+ tableName: string,
12
+ ): StorageTable | undefined {
13
+ if (namespace === undefined) {
14
+ return undefined;
15
+ }
16
+ const tables = namespace.entries.table;
17
+ if (!Object.hasOwn(tables, tableName)) {
18
+ return undefined;
19
+ }
20
+ return tables[tableName];
21
+ }
22
+
23
+ /**
24
+ * Resolve a bare storage table name to its namespace coordinate and table IR.
25
+ *
26
+ * When `namespaceId` is supplied, the table is resolved strictly within that
27
+ * namespace (no scan). When omitted, a bare name unique across namespaces
28
+ * resolves to its sole namespace; a bare name declared in more than one
29
+ * namespace throws a fail-fast diagnostic naming the candidate namespaces
30
+ * rather than silently selecting the first match.
31
+ */
32
+ export function resolveStorageTable(
33
+ storage: SqlStorage,
34
+ tableName: string,
35
+ namespaceId?: string,
36
+ ): ResolvedStorageTable | undefined {
37
+ if (namespaceId !== undefined) {
38
+ const table = tableInNamespace(storage.namespaces[namespaceId], tableName);
39
+ return table === undefined ? undefined : { namespaceId, table };
40
+ }
41
+
42
+ const matches: ResolvedStorageTable[] = [];
43
+ for (const candidateNamespaceId of Object.keys(storage.namespaces)) {
44
+ const table = tableInNamespace(storage.namespaces[candidateNamespaceId], tableName);
45
+ if (table !== undefined) {
46
+ matches.push({ namespaceId: candidateNamespaceId, table });
47
+ }
48
+ }
49
+
50
+ if (matches.length > 1) {
51
+ const candidates = matches
52
+ .map((match) => match.namespaceId)
53
+ .sort()
54
+ .join(', ');
55
+ throw new Error(
56
+ `Storage table "${tableName}" is ambiguous across namespaces [${candidates}]; qualify it with a namespace coordinate.`,
57
+ );
58
+ }
59
+
60
+ return matches[0];
61
+ }
package/src/types.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  import type { CodecTrait } from '@prisma-next/framework-components/codec';
2
+ import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
2
3
  import type { ReferentialAction } from './ir/foreign-key';
3
4
 
5
+ export interface SqlControlDriverInstance<T extends string = string>
6
+ extends ControlDriverInstance<'sql', T> {
7
+ query<Row = Record<string, unknown>>(
8
+ sql: string,
9
+ params?: readonly unknown[],
10
+ ): Promise<{ readonly rows: Row[] }>;
11
+ }
12
+
4
13
  export { buildSqlNamespace, buildSqlNamespaceMap } from './ir/build-sql-namespace';
14
+ export { CheckConstraint, type CheckConstraintInput } from './ir/check-constraint';
5
15
  export {
6
16
  ForeignKey,
7
17
  type ForeignKeyInput,
@@ -24,6 +34,7 @@ export {
24
34
  SqlStorage,
25
35
  type SqlStorageInput,
26
36
  type SqlStorageTypeEntry,
37
+ storageTableAt,
27
38
  } from './ir/sql-storage';
28
39
  export { SqlUnboundNamespace } from './ir/sql-unbound-namespace';
29
40
  export { StorageColumn, type StorageColumnInput } from './ir/storage-column';
@@ -35,6 +46,7 @@ export {
35
46
  type StorageTypeInstanceInput,
36
47
  toStorageTypeInstance,
37
48
  } from './ir/storage-type-instance';
49
+ export { StorageValueSet, type StorageValueSetInput } from './ir/storage-value-set';
38
50
  export {
39
51
  UniqueConstraint,
40
52
  type UniqueConstraintInput,
@@ -54,6 +66,7 @@ export type SqlModelFieldStorage = {
54
66
 
55
67
  export type SqlModelStorage = {
56
68
  readonly table: string;
69
+ readonly namespaceId: string;
57
70
  readonly fields: Record<string, SqlModelFieldStorage>;
58
71
  };
59
72
 
package/src/validators.ts CHANGED
@@ -33,6 +33,7 @@ type ColumnDefaultFunction = { readonly kind: 'function'; readonly expression: s
33
33
  const literalKindSchema = type("'literal'");
34
34
  const functionKindSchema = type("'function'");
35
35
  const generatorKindSchema = type("'generator'");
36
+ const ControlPolicySchema = type("'managed' | 'tolerated' | 'external' | 'observed'");
36
37
  const generatorIdSchema = type('string').narrow((value, ctx) => {
37
38
  return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe('a flat generator id');
38
39
  });
@@ -76,6 +77,14 @@ const ExecutionSchema = type({
76
77
  },
77
78
  });
78
79
 
80
+ const ValueSetRefSchema = type({
81
+ plane: "'domain' | 'storage'",
82
+ namespaceId: 'string',
83
+ entityKind: "'enum' | 'value-set'",
84
+ name: 'string',
85
+ 'spaceId?': 'string',
86
+ });
87
+
79
88
  const StorageColumnSchema = type({
80
89
  '+': 'reject',
81
90
  nativeType: 'string',
@@ -84,6 +93,8 @@ const StorageColumnSchema = type({
84
93
  'typeParams?': 'Record<string, unknown>',
85
94
  'typeRef?': 'string',
86
95
  'default?': ColumnDefaultSchema,
96
+ 'control?': ControlPolicySchema,
97
+ 'valueSet?': ValueSetRefSchema,
87
98
  }).narrow((col, ctx) => {
88
99
  if (col.typeParams !== undefined && col.typeRef !== undefined) {
89
100
  return ctx.mustBe('a column with either typeParams or typeRef, not both');
@@ -103,11 +114,11 @@ const StorageTypeInstanceSchema = type
103
114
  kind: "'codec-instance'",
104
115
  codecId: 'string',
105
116
  nativeType: 'string',
106
- typeParams: 'Record<string, unknown>',
117
+ 'typeParams?': 'Record<string, unknown>',
107
118
  });
108
119
 
109
120
  /**
110
- * Postgres native enum entry under `storage.namespaces[namespaceId].enum[name]`.
121
+ * Postgres native enum entry under `storage.namespaces[namespaceId].entries.type[name]`.
111
122
  * Document-scoped `storage.types` carries codec aliases only
112
123
  * (`DocumentScopedStorageTypeSchema`).
113
124
  */
@@ -116,11 +127,37 @@ const PostgresEnumTypeSchema = type({
116
127
  'name?': 'string',
117
128
  'nativeType?': 'string',
118
129
  values: type.string.array().readonly(),
130
+ 'control?': ControlPolicySchema,
119
131
  });
120
132
 
121
133
  /** Document-scoped `storage.types`: codec triples only. */
122
134
  const DocumentScopedStorageTypeSchema = StorageTypeInstanceSchema;
123
135
 
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
+ /**
147
+ * Domain enum entry under `domain.namespaces[id].enum[name]`.
148
+ * Carries the codec id and an ordered `members` array of `{name, value}` pairs.
149
+ */
150
+ export const ContractEnumSchema = type({
151
+ '+': 'reject',
152
+ codecId: 'string',
153
+ members: type({
154
+ name: 'string',
155
+ value: 'string',
156
+ })
157
+ .array()
158
+ .readonly(),
159
+ });
160
+
124
161
  const PrimaryKeySchema = type.declare<PrimaryKeyInput>().type({
125
162
  columns: type.string.array().readonly(),
126
163
  'name?': 'string',
@@ -143,6 +180,14 @@ export const ForeignKeyReferenceSchema = type({
143
180
  namespaceId: 'string',
144
181
  tableName: 'string',
145
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(),
146
191
  }) satisfies Type<ForeignKeyReferenceInput>;
147
192
 
148
193
  export const ReferentialActionSchema = type
@@ -150,7 +195,7 @@ export const ReferentialActionSchema = type
150
195
  .type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
151
196
 
152
197
  export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
153
- source: ForeignKeyReferenceSchema,
198
+ source: ForeignKeySourceSchema,
154
199
  target: ForeignKeyReferenceSchema,
155
200
  'name?': 'string',
156
201
  'onDelete?': ReferentialActionSchema,
@@ -159,6 +204,13 @@ export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
159
204
  index: 'boolean',
160
205
  });
161
206
 
207
+ export const CheckConstraintSchema = type({
208
+ '+': 'reject',
209
+ name: 'string',
210
+ column: 'string',
211
+ valueSet: ValueSetRefSchema,
212
+ });
213
+
162
214
  const StorageTableSchema = type({
163
215
  '+': 'reject',
164
216
  columns: type({ '[string]': StorageColumnSchema }),
@@ -166,6 +218,8 @@ const StorageTableSchema = type({
166
218
  uniques: UniqueConstraintSchema.array().readonly(),
167
219
  indexes: IndexSchema.array().readonly(),
168
220
  foreignKeys: ForeignKeySchema.array().readonly(),
221
+ 'control?': ControlPolicySchema,
222
+ 'checks?': CheckConstraintSchema.array().readonly(),
169
223
  });
170
224
 
171
225
  /**
@@ -236,7 +290,7 @@ function namespaceSlotEntrySchema(
236
290
  * Builds the per-namespace entry schema for `storage.namespaces[id]`.
237
291
  * Pack-contributed `validatorSchema` fragments — keyed by the
238
292
  * descriptor's `discriminator` — validate each entry by matching the
239
- * entry's `kind` field on the `'enum?'` slot.
293
+ * entry's `kind` field on the `'entries.type'` slot.
240
294
  */
241
295
  export function createNamespaceEntrySchema(
242
296
  fragments?: ReadonlyMap<string, Type<unknown>>,
@@ -245,9 +299,13 @@ export function createNamespaceEntrySchema(
245
299
  '+': 'reject',
246
300
  id: 'string',
247
301
  'kind?': 'string',
248
- 'tables?': type({ '[string]': StorageTableSchema }),
249
- 'enum?': type({
250
- '[string]': namespaceSlotEntrySchema(PostgresEnumTypeSchema, 'postgres-enum', fragments),
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 }),
251
309
  }),
252
310
  }) as Type<unknown>;
253
311
  }
@@ -277,20 +335,22 @@ export function createSqlStorageSchema(
277
335
 
278
336
  const StorageSchema = createSqlStorageSchema();
279
337
 
280
- // SQL-specific namespace walk shape (`tables` is the SQL family's idiom —
281
- // the framework `Namespace` interface no longer carries it). The wider
282
- // `object` table value keeps this helper structurally compatible with
283
- // `SqlNamespace` (whose tables narrow to `StorageTable`) and the JSON
284
- // envelope variants that lose class identity.
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.
285
342
  type NamespacedStorageWalk = {
286
343
  readonly namespaces: Readonly<
287
- Record<string, Namespace & { readonly tables?: Readonly<Record<string, object>> }>
344
+ Record<
345
+ string,
346
+ Namespace & { readonly entries: { readonly table: Readonly<Record<string, object>> } }
347
+ >
288
348
  >;
289
349
  };
290
350
 
291
351
  function eachStorageTable(storage: NamespacedStorageWalk) {
292
352
  return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) =>
293
- Object.entries(ns.tables ?? {}).map(([tableName, table]) => ({
353
+ Object.entries(ns.entries.table).map(([tableName, table]) => ({
294
354
  namespaceId,
295
355
  tableName,
296
356
  table,
@@ -298,16 +358,6 @@ function eachStorageTable(storage: NamespacedStorageWalk) {
298
358
  );
299
359
  }
300
360
 
301
- function findStorageTableByTableName(storage: NamespacedStorageWalk, tableName: string): unknown {
302
- for (const ns of Object.values(storage.namespaces)) {
303
- const t = ns.tables?.[tableName];
304
- if (t !== undefined) {
305
- return t;
306
- }
307
- }
308
- return undefined;
309
- }
310
-
311
361
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
312
362
  return typeof value === 'object' && value !== null && !Array.isArray(value);
313
363
  }
@@ -353,6 +403,7 @@ const ModelFieldSchema = type({
353
403
  type: ContractFieldTypeSchema,
354
404
  'many?': 'true',
355
405
  'dict?': 'true',
406
+ 'valueSet?': ValueSetRefSchema,
356
407
  });
357
408
 
358
409
  const ModelStorageFieldSchema = type({
@@ -363,20 +414,44 @@ const ModelStorageFieldSchema = type({
363
414
 
364
415
  const ModelStorageSchema = type({
365
416
  table: 'string',
417
+ namespaceId: 'string',
366
418
  fields: type({ '[string]': ModelStorageFieldSchema }),
367
419
  });
368
420
 
369
- const ContractReferenceRelationSchema = type({
421
+ const ContractRelationThroughSchema = type({
422
+ '+': 'reject',
423
+ table: 'string',
424
+ namespaceId: 'string',
425
+ parentColumns: type.string.array().readonly(),
426
+ childColumns: type.string.array().readonly(),
427
+ targetColumns: type.string.array().readonly(),
428
+ });
429
+
430
+ const ContractRelationOnSchema = type({
431
+ '+': 'reject',
432
+ localFields: type.string.array().readonly(),
433
+ targetFields: type.string.array().readonly(),
434
+ });
435
+
436
+ const ContractManyToManyRelationSchema = type({
437
+ '+': 'reject',
438
+ to: CrossReferenceSchema,
439
+ cardinality: "'N:M'",
440
+ on: ContractRelationOnSchema,
441
+ through: ContractRelationThroughSchema,
442
+ });
443
+
444
+ const ContractNonJunctionRelationSchema = type({
370
445
  '+': 'reject',
371
446
  to: CrossReferenceSchema,
372
447
  cardinality: "'1:1' | '1:N' | 'N:1'",
373
- on: type({
374
- '+': 'reject',
375
- localFields: type.string.array().readonly(),
376
- targetFields: type.string.array().readonly(),
377
- }),
448
+ on: ContractRelationOnSchema,
378
449
  });
379
450
 
451
+ const ContractReferenceRelationSchema = ContractManyToManyRelationSchema.or(
452
+ ContractNonJunctionRelationSchema,
453
+ );
454
+
380
455
  const ContractEmbedRelationSchema = type({
381
456
  '+': 'reject',
382
457
  to: CrossReferenceSchema,
@@ -417,12 +492,14 @@ export function createSqlContractSchema(
417
492
  'capabilities?': 'Record<string, Record<string, boolean>>',
418
493
  'extensionPacks?': 'Record<string, unknown>',
419
494
  'meta?': ContractMetaSchema,
495
+ 'defaultControlPolicy?': ControlPolicySchema,
420
496
  'roots?': type({ '[string]': CrossReferenceSchema }),
421
497
  domain: type({
422
498
  namespaces: type({
423
499
  '[string]': type({
424
500
  models: type({ '[string]': ModelSchema }),
425
501
  'valueObjects?': 'Record<string, unknown>',
502
+ 'enum?': type({ '[string]': ContractEnumSchema }),
426
503
  }),
427
504
  }),
428
505
  }),
@@ -557,6 +634,9 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
557
634
  for (const fk of table.foreignKeys) {
558
635
  registerNamedObject('foreign key', fk.name);
559
636
  }
637
+ for (const check of table.checks ?? []) {
638
+ registerNamedObject('check constraint', check.name);
639
+ }
560
640
 
561
641
  for (const [name, kinds] of namedObjects) {
562
642
  if (kinds.length > 1) {
@@ -675,6 +755,18 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
675
755
  }
676
756
  }
677
757
  }
758
+
759
+ const seenCheckDefinitions = new Set<string>();
760
+ for (const check of table.checks ?? []) {
761
+ const signature = JSON.stringify({ column: check.column, valueSet: check.valueSet });
762
+ if (seenCheckDefinitions.has(signature)) {
763
+ errors.push(
764
+ `Namespace "${namespaceId}" table "${tableName}": duplicate check constraint definition on column "${check.column}"`,
765
+ );
766
+ continue;
767
+ }
768
+ seenCheckDefinitions.add(signature);
769
+ }
678
770
  }
679
771
 
680
772
  return errors;
@@ -691,12 +783,19 @@ export function validateModelStorageReferences(contract: Contract<SqlStorage>):
691
783
  const models = namespace.models as Record<string, ContractModel<SqlModelStorage>>;
692
784
  for (const [modelName, model] of Object.entries(models)) {
693
785
  const qualifiedName = `${namespaceId}:${modelName}`;
694
- const storageTable = model.storage.table;
786
+ const storageNamespaceId = model.storage.namespaceId;
787
+ if (storageNamespaceId !== namespaceId) {
788
+ throw new ContractValidationError(
789
+ `Model "${qualifiedName}" storage.namespaceId "${storageNamespaceId}" does not match domain namespace "${namespaceId}"`,
790
+ 'storage',
791
+ );
792
+ }
695
793
 
696
- const rawTable = findStorageTableByTableName(contract.storage, storageTable);
794
+ const storageTable = model.storage.table;
795
+ const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table[storageTable];
697
796
  if (rawTable === undefined) {
698
797
  throw new ContractValidationError(
699
- `Model "${qualifiedName}" references non-existent table "${storageTable}"`,
798
+ `Model "${qualifiedName}" references non-existent table "${storageNamespaceId}.${storageTable}"`,
700
799
  'storage',
701
800
  );
702
801
  }
@@ -776,6 +875,15 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
776
875
  }
777
876
  }
778
877
 
878
+ for (const check of table.checks ?? []) {
879
+ if (!columnNames.has(check.column)) {
880
+ throw new ContractValidationError(
881
+ `Namespace "${namespaceId}" table "${tableName}" check constraint "${check.name}" references non-existent column "${check.column}"`,
882
+ 'storage',
883
+ );
884
+ }
885
+ }
886
+
779
887
  for (const [colName, column] of Object.entries(table.columns)) {
780
888
  if (!column.nullable && column.default?.kind === 'literal' && column.default.value === null) {
781
889
  throw new ContractValidationError(
@@ -802,23 +910,25 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
802
910
  }
803
911
  }
804
912
 
805
- const targetNamespace = contract.storage.namespaces[fk.target.namespaceId];
806
- const referencedRaw = targetNamespace?.tables?.[fk.target.tableName];
807
- if (referencedRaw === undefined) {
808
- throw new ContractValidationError(
809
- `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,
810
- 'storage',
811
- );
812
- }
813
- const referencedTable = referencedRaw as StorageTable;
814
- const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
815
- for (const colName of fk.target.columns) {
816
- if (!referencedColumnNames.has(colName)) {
913
+ if (fk.target.spaceId === undefined) {
914
+ const targetNamespace = contract.storage.namespaces[fk.target.namespaceId];
915
+ const referencedRaw = targetNamespace?.entries.table[fk.target.tableName];
916
+ if (referencedRaw === undefined) {
817
917
  throw new ContractValidationError(
818
- `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.target.tableName}"`,
918
+ `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,
819
919
  'storage',
820
920
  );
821
921
  }
922
+ const referencedTable = referencedRaw as StorageTable;
923
+ const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
924
+ for (const colName of fk.target.columns) {
925
+ if (!referencedColumnNames.has(colName)) {
926
+ throw new ContractValidationError(
927
+ `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.target.tableName}"`,
928
+ 'storage',
929
+ );
930
+ }
931
+ }
822
932
  }
823
933
 
824
934
  if (fk.source.columns.length !== fk.target.columns.length) {