@prisma-next/family-sql 0.14.0-dev.4 → 0.14.0-dev.40

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 (66) hide show
  1. package/dist/{authoring-type-constructors-CjFfO6LM.mjs → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
  2. package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
  3. package/dist/contract-to-schema-ir-S-evq8E6.mjs +264 -0
  4. package/dist/contract-to-schema-ir-S-evq8E6.mjs.map +1 -0
  5. package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-DHYFuOBy.d.mts} +12 -7
  6. package/dist/{control-adapter-Cmw9LvEP.d.mts.map → control-adapter-DHYFuOBy.d.mts.map} +1 -1
  7. package/dist/control-adapter.d.mts +1 -1
  8. package/dist/control.d.mts +18 -3
  9. package/dist/control.d.mts.map +1 -1
  10. package/dist/control.mjs +81 -943
  11. package/dist/control.mjs.map +1 -1
  12. package/dist/{schema-verify.d.mts → diff.d.mts} +5 -5
  13. package/dist/diff.d.mts.map +1 -0
  14. package/dist/{verify-sql-schema-xT4udQLQ.mjs → diff.mjs} +169 -253
  15. package/dist/diff.mjs.map +1 -0
  16. package/dist/ir.d.mts +9 -6
  17. package/dist/ir.d.mts.map +1 -1
  18. package/dist/ir.mjs +1 -1
  19. package/dist/migration.d.mts +11 -2
  20. package/dist/migration.d.mts.map +1 -1
  21. package/dist/migration.mjs +7 -0
  22. package/dist/migration.mjs.map +1 -1
  23. package/dist/pack.mjs +1 -1
  24. package/dist/psl-infer.d.mts +80 -0
  25. package/dist/psl-infer.d.mts.map +1 -0
  26. package/dist/psl-infer.mjs +334 -0
  27. package/dist/psl-infer.mjs.map +1 -0
  28. package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-CYtXg_zs.mjs} +17 -20
  29. package/dist/sql-contract-serializer-CYtXg_zs.mjs.map +1 -0
  30. package/dist/{verify-sql-schema-thU-jKpf.d.mts → sql-schema-diff-6z36dZt6.d.mts} +44 -5
  31. package/dist/sql-schema-diff-6z36dZt6.d.mts.map +1 -0
  32. package/dist/{types-kgstZ_Zd.d.mts → types-CkOIJXxU.d.mts} +84 -17
  33. package/dist/types-CkOIJXxU.d.mts.map +1 -0
  34. package/package.json +23 -23
  35. package/src/core/authoring-entity-types.ts +12 -37
  36. package/src/core/control-adapter.ts +10 -5
  37. package/src/core/control-instance.ts +174 -41
  38. package/src/core/{schema-verify/verify-sql-schema.ts → diff/sql-schema-diff.ts} +208 -16
  39. package/src/core/{schema-verify → diff}/verify-helpers.ts +12 -0
  40. package/src/core/ir/sql-contract-serializer-base.ts +23 -55
  41. package/src/core/ir/sql-contract-serializer.ts +5 -7
  42. package/src/core/migrations/contract-to-schema-ir.ts +49 -12
  43. package/src/core/migrations/field-event-planner.ts +2 -2
  44. package/src/core/migrations/schema-differ.ts +40 -0
  45. package/src/core/migrations/types.ts +43 -2
  46. package/src/core/psl-contract-infer/printer-config.ts +0 -6
  47. package/src/core/sql-migration.ts +12 -1
  48. package/src/exports/control.ts +1 -0
  49. package/src/exports/diff.ts +18 -0
  50. package/src/exports/psl-infer.ts +36 -0
  51. package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
  52. package/dist/schema-verify.d.mts.map +0 -1
  53. package/dist/schema-verify.mjs +0 -2
  54. package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
  55. package/dist/test-utils.d.mts +0 -2
  56. package/dist/test-utils.mjs +0 -2
  57. package/dist/types-kgstZ_Zd.d.mts.map +0 -1
  58. package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
  59. package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
  60. package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
  61. package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
  62. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
  63. package/src/exports/schema-verify.ts +0 -18
  64. package/src/exports/test-utils.ts +0 -9
  65. /package/src/core/{schema-verify → diff}/control-verify-emit.ts +0 -0
  66. /package/src/core/{schema-verify → diff}/verifier-disposition.ts +0 -0
@@ -24,8 +24,9 @@ import {
24
24
  StorageTable,
25
25
  type StorageTypeInstance,
26
26
  } from '@prisma-next/sql-contract/types';
27
- import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
27
+ import type { SqlSchemaIR, SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
28
28
  import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
29
+ import { blindCast } from '@prisma-next/utils/casts';
29
30
  import { ifDefined } from '@prisma-next/utils/defined';
30
31
  import { extractCodecControlHooks } from '../assembly';
31
32
  import { resolveValueSetValues } from '../migrations/contract-to-schema-ir';
@@ -42,6 +43,30 @@ import {
42
43
  verifyUniqueConstraints,
43
44
  } from './verify-helpers';
44
45
 
46
+ /**
47
+ * Returns the per-schema namespace nodes of an introspected schema node, for
48
+ * the relational verify to consume one at a time. Structure-agnostic — imports
49
+ * no target node class. A root exposing a `namespaces` record (Postgres) yields
50
+ * its namespace nodes (never merged, so same-named tables in different schemas
51
+ * cannot collide); a flat schema (SQLite) is its own single namespace and
52
+ * yields itself. Handles spread-flattened input (own-enumerable fields survive).
53
+ */
54
+ function namespaceSchemaNodes(schema: SqlSchemaIRNode): readonly SqlSchemaIR[] {
55
+ const obj = blindCast<
56
+ { readonly namespaces?: Readonly<Record<string, SqlSchemaIR>> },
57
+ 'structural read of an own-enumerable namespaces record; survives the projectSchemaToSpace spread'
58
+ >(schema);
59
+ if (obj.namespaces !== undefined) {
60
+ return Object.values(obj.namespaces);
61
+ }
62
+ return [
63
+ blindCast<
64
+ SqlSchemaIR,
65
+ 'a flat schema node (no namespaces) is its own single namespace, exposing the per-schema { tables } shape'
66
+ >(schema),
67
+ ];
68
+ }
69
+
45
70
  /**
46
71
  * Function type for normalizing raw database default expressions into ColumnDefault.
47
72
  * Target-specific implementations handle database dialect differences.
@@ -89,6 +114,15 @@ export interface VerifySqlSchemaOptions {
89
114
  * with contract native types (e.g., Postgres 'varchar' → 'character varying').
90
115
  */
91
116
  readonly normalizeNativeType?: NativeTypeNormalizer;
117
+ /**
118
+ * When set, only the contract tables in these namespace ids are checked
119
+ * against `schema` (the matching actual namespace node). The full contract is
120
+ * still consulted for cross-namespace value-set / control-policy resolution.
121
+ * Used by the multi-schema verify, which pairs each contract namespace to its
122
+ * own actual node. Absent ⇒ all contract namespaces are checked against the
123
+ * single (flat) `schema` — the single-schema / SQLite path.
124
+ */
125
+ readonly restrictToNamespaceIds?: ReadonlySet<string>;
92
126
  }
93
127
 
94
128
  /**
@@ -128,6 +162,7 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
128
162
  storageTypes,
129
163
  ...ifDefined('normalizeDefault', normalizeDefault),
130
164
  ...ifDefined('normalizeNativeType', normalizeNativeType),
165
+ ...ifDefined('restrictToNamespaceIds', options.restrictToNamespaceIds),
131
166
  });
132
167
 
133
168
  validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
@@ -236,6 +271,7 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
236
271
  },
237
272
  schema: {
238
273
  issues,
274
+ schemaDiffIssues: [],
239
275
  root,
240
276
  counts,
241
277
  },
@@ -274,6 +310,7 @@ function verifySchemaTables(options: {
274
310
  storageTypes: Readonly<Record<string, StorageTypeInstance>>;
275
311
  normalizeDefault?: DefaultNormalizer;
276
312
  normalizeNativeType?: NativeTypeNormalizer;
313
+ restrictToNamespaceIds?: ReadonlySet<string>;
277
314
  }): { issues: SchemaIssue[]; rootChildren: SchemaVerificationNode[] } {
278
315
  const {
279
316
  contract,
@@ -284,6 +321,7 @@ function verifySchemaTables(options: {
284
321
  storageTypes,
285
322
  normalizeDefault,
286
323
  normalizeNativeType,
324
+ restrictToNamespaceIds,
287
325
  } = options;
288
326
  const contractDefaultControl = contract.defaultControlPolicy;
289
327
  const issues: SchemaIssue[] = [];
@@ -294,14 +332,17 @@ function verifySchemaTables(options: {
294
332
  );
295
333
 
296
334
  for (const namespaceId of namespaceIds) {
335
+ // When the caller pairs each contract namespace to its own actual node, it
336
+ // restricts the table check to that namespace; the full contract is still
337
+ // consulted for value-set / control-policy resolution.
338
+ if (restrictToNamespaceIds !== undefined && !restrictToNamespaceIds.has(namespaceId)) continue;
297
339
  const ns = contract.storage.namespaces[namespaceId];
298
340
  if (!ns) continue;
299
341
  for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table ?? {})) {
300
- if (!(contractTableRaw instanceof StorageTable)) {
301
- throw new Error(
302
- `verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.entries.table.${tableName}`,
303
- );
304
- }
342
+ StorageTable.assert(
343
+ contractTableRaw,
344
+ `storage.namespaces.${namespaceId}.entries.table.${tableName}`,
345
+ );
305
346
  const contractTable = contractTableRaw;
306
347
  const tableControlPolicy = effectiveControlPolicy(
307
348
  contractTable.control,
@@ -313,6 +354,7 @@ function verifySchemaTables(options: {
313
354
  if (!schemaTable) {
314
355
  const issue: SchemaIssue = {
315
356
  kind: 'missing_table',
357
+ reason: 'not-found',
316
358
  table: tableName,
317
359
  namespaceId,
318
360
  message: `Table "${tableName}" is missing from database`,
@@ -367,6 +409,7 @@ function verifySchemaTables(options: {
367
409
  const extraTableControlPolicy = effectiveControlPolicy(undefined, contractDefaultControl);
368
410
  const issue: SchemaIssue = {
369
411
  kind: 'extra_table',
412
+ reason: 'not-expected',
370
413
  table: tableName,
371
414
  message: `Extra table "${tableName}" found in database (not in contract)`,
372
415
  };
@@ -383,6 +426,7 @@ function verifySchemaTables(options: {
383
426
  expected: undefined,
384
427
  actual: undefined,
385
428
  children: [],
429
+ reason: 'not-expected',
386
430
  },
387
431
  issues,
388
432
  rootChildren,
@@ -507,6 +551,7 @@ function verifyTableChildren(options: {
507
551
  } else if (schemaTable.primaryKey && strict) {
508
552
  const issue: SchemaIssue = {
509
553
  kind: 'extra_primary_key',
554
+ reason: 'not-expected',
510
555
  table: tableName,
511
556
  namespaceId,
512
557
  message: 'Extra primary key found in database (not in contract)',
@@ -650,6 +695,7 @@ function collectContractColumnNodes(options: {
650
695
  if (!schemaColumn) {
651
696
  const issue: SchemaIssue = {
652
697
  kind: 'missing_column',
698
+ reason: 'not-found',
653
699
  table: tableName,
654
700
  namespaceId,
655
701
  column: columnName,
@@ -722,6 +768,7 @@ function appendExtraColumnNodes(options: {
722
768
  if (!contractTable.columns[columnName]) {
723
769
  const issue: SchemaIssue = {
724
770
  kind: 'extra_column',
771
+ reason: 'not-expected',
725
772
  table: tableName,
726
773
  namespaceId,
727
774
  column: columnName,
@@ -790,14 +837,16 @@ function verifyColumn(options: {
790
837
  tableName,
791
838
  columnName,
792
839
  });
793
- const schemaNativeType =
840
+ const schemaBaseNativeType =
794
841
  normalizeNativeType?.(schemaColumn.nativeType) ?? schemaColumn.nativeType;
842
+ const schemaNativeType = schemaColumn.many ? `${schemaBaseNativeType}[]` : schemaBaseNativeType;
795
843
 
796
844
  const typesMatch = contractNativeType === schemaNativeType;
797
845
 
798
846
  if (!typesMatch) {
799
847
  const issue: SchemaIssue = {
800
848
  kind: 'type_mismatch',
849
+ reason: 'not-equal',
801
850
  table: tableName,
802
851
  namespaceId,
803
852
  column: columnName,
@@ -858,6 +907,7 @@ function verifyColumn(options: {
858
907
  if (contractColumn.nullable !== schemaColumn.nullable) {
859
908
  const issue: SchemaIssue = {
860
909
  kind: 'nullability_mismatch',
910
+ reason: 'not-equal',
861
911
  table: tableName,
862
912
  namespaceId,
863
913
  column: columnName,
@@ -888,6 +938,7 @@ function verifyColumn(options: {
888
938
  const defaultDescription = describeColumnDefault(contractColumn.default);
889
939
  const issue: SchemaIssue = {
890
940
  kind: 'default_missing',
941
+ reason: 'not-found',
891
942
  table: tableName,
892
943
  namespaceId,
893
944
  column: columnName,
@@ -922,6 +973,7 @@ function verifyColumn(options: {
922
973
  const actualDescription = schemaColumn.default;
923
974
  const issue: SchemaIssue = {
924
975
  kind: 'default_mismatch',
976
+ reason: 'not-equal',
925
977
  table: tableName,
926
978
  namespaceId,
927
979
  column: columnName,
@@ -949,6 +1001,7 @@ function verifyColumn(options: {
949
1001
  } else if (strict && schemaColumn.default) {
950
1002
  const issue: SchemaIssue = {
951
1003
  kind: 'extra_default',
1004
+ reason: 'not-expected',
952
1005
  table: tableName,
953
1006
  namespaceId,
954
1007
  column: columnName,
@@ -1144,19 +1197,20 @@ function renderExpectedNativeType(
1144
1197
  context,
1145
1198
  );
1146
1199
 
1200
+ let baseType: string;
1201
+
1147
1202
  // If no typeParams or codecId, return the base native type
1148
1203
  if (!typeParams || !codecId) {
1149
- return nativeType;
1204
+ baseType = nativeType;
1205
+ } else {
1206
+ // Try to use the codec's expandNativeType hook if available
1207
+ const hooks = codecHooks.get(codecId);
1208
+ baseType = hooks?.expandNativeType
1209
+ ? hooks.expandNativeType({ nativeType, codecId, typeParams })
1210
+ : nativeType;
1150
1211
  }
1151
1212
 
1152
- // Try to use the codec's expandNativeType hook if available
1153
- const hooks = codecHooks.get(codecId);
1154
- if (hooks?.expandNativeType) {
1155
- return hooks.expandNativeType({ nativeType, codecId, typeParams });
1156
- }
1157
-
1158
- // Fallback: return base native type if no hook is available
1159
- return nativeType;
1213
+ return contractColumn.many ? `${baseType}[]` : baseType;
1160
1214
  }
1161
1215
 
1162
1216
  function resolveContractColumnTypeMetadata(
@@ -1309,3 +1363,141 @@ function formatLiteralValue(value: unknown): string {
1309
1363
  }
1310
1364
  return JSON.stringify(value);
1311
1365
  }
1366
+
1367
+ /**
1368
+ * Reads a namespace node's DDL schema name. Namespaced target nodes (Postgres
1369
+ * `PostgresNamespaceSchemaNode`) carry `schemaName`; a flat schema (SQLite) has
1370
+ * none and pairs by position as the sole namespace.
1371
+ */
1372
+ function namespaceSchemaName(node: SqlSchemaIR): string | undefined {
1373
+ return blindCast<
1374
+ { readonly schemaName?: string },
1375
+ 'reading the optional namespace schemaName off a per-schema node'
1376
+ >(node).schemaName;
1377
+ }
1378
+
1379
+ /**
1380
+ * Returns a shallow copy of `contract` exposing only the one named namespace —
1381
+ * used solely to resolve that namespace's live DDL schema via the target's
1382
+ * expected-tree projection (never for verification, so global value-sets it may
1383
+ * reference are not consulted).
1384
+ */
1385
+ function scopeContractToNamespace(
1386
+ contract: Contract<SqlStorage>,
1387
+ namespaceId: string,
1388
+ ): Contract<SqlStorage> {
1389
+ const namespace = contract.storage.namespaces[namespaceId];
1390
+ const scopedNamespaces = namespace === undefined ? {} : { [namespaceId]: namespace };
1391
+ return blindCast<
1392
+ Contract<SqlStorage>,
1393
+ 'narrowing storage.namespaces to one entry; the rest of the contract is preserved'
1394
+ >({
1395
+ ...contract,
1396
+ storage: blindCast<
1397
+ SqlStorage,
1398
+ 'shallow storage copy with a single-namespace map; other storage fields are preserved'
1399
+ >({
1400
+ ...contract.storage,
1401
+ namespaces: scopedNamespaces,
1402
+ }),
1403
+ });
1404
+ }
1405
+
1406
+ /**
1407
+ * Combines two `VerifyDatabaseSchemaResult`s by concatenating issues and summing
1408
+ * counts — used to fold the per-namespace pairings of a multi-schema database
1409
+ * into one result. The verification-tree `root` of the first pairing is
1410
+ * retained (multi-schema verify-tree shaping is future work).
1411
+ */
1412
+ function mergeVerifyResults(
1413
+ a: VerifyDatabaseSchemaResult,
1414
+ b: VerifyDatabaseSchemaResult,
1415
+ ): VerifyDatabaseSchemaResult {
1416
+ return {
1417
+ ...a,
1418
+ ok: a.ok && b.ok,
1419
+ ...ifDefined('code', a.code ?? b.code),
1420
+ schema: {
1421
+ ...a.schema,
1422
+ issues: [...a.schema.issues, ...b.schema.issues],
1423
+ schemaDiffIssues: [...a.schema.schemaDiffIssues, ...b.schema.schemaDiffIssues],
1424
+ counts: {
1425
+ pass: a.schema.counts.pass + b.schema.counts.pass,
1426
+ warn: a.schema.counts.warn + b.schema.counts.warn,
1427
+ fail: a.schema.counts.fail + b.schema.counts.fail,
1428
+ totalNodes: a.schema.counts.totalNodes + b.schema.counts.totalNodes,
1429
+ },
1430
+ },
1431
+ };
1432
+ }
1433
+
1434
+ /**
1435
+ * The single per-namespace-paired relational verify shared by the migration
1436
+ * planner and the family schema verify — there is exactly one such operation.
1437
+ *
1438
+ * Each contract namespace is paired to the introspected namespace node holding
1439
+ * the same DDL schema, then `verifySqlSchema` checks that namespace's tables
1440
+ * against the matching actual node (a contract table under `auth` is only ever
1441
+ * looked up in the `auth` actual node, so a multi-schema database no longer
1442
+ * reports tables in other schemas as missing). The full contract is passed every
1443
+ * time — `restrictToNamespaceIds` scopes only which tables are checked, so
1444
+ * cross-namespace value-set / control-policy resolution is unaffected.
1445
+ *
1446
+ * The DDL schema of each contract namespace is read from a single-namespace
1447
+ * expected projection (`buildExpectedSchema`), which both callers already build
1448
+ * the same way. Empty contract namespaces verify nothing and are skipped.
1449
+ * Single-schema (one namespace) and SQLite's flat schema are one pairing —
1450
+ * byte-identical to the prior per-node verify.
1451
+ */
1452
+ export function verifySqlSchemaTree(options: {
1453
+ readonly contract: Contract<SqlStorage>;
1454
+ readonly actualSchema: SqlSchemaIRNode;
1455
+ readonly buildExpectedSchema: (contract: Contract<SqlStorage>) => SqlSchemaIRNode;
1456
+ readonly strict: boolean;
1457
+ readonly typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
1458
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
1459
+ readonly normalizeDefault?: DefaultNormalizer;
1460
+ readonly normalizeNativeType?: NativeTypeNormalizer;
1461
+ }): VerifyDatabaseSchemaResult {
1462
+ const baseOptions = {
1463
+ contract: options.contract,
1464
+ strict: options.strict,
1465
+ typeMetadataRegistry: options.typeMetadataRegistry,
1466
+ frameworkComponents: options.frameworkComponents,
1467
+ ...ifDefined('normalizeDefault', options.normalizeDefault),
1468
+ ...ifDefined('normalizeNativeType', options.normalizeNativeType),
1469
+ };
1470
+
1471
+ const actualNodes = namespaceSchemaNodes(options.actualSchema);
1472
+ const actualByName = new Map<string, SqlSchemaIR>();
1473
+ for (const node of actualNodes) {
1474
+ const name = namespaceSchemaName(node);
1475
+ if (name !== undefined) actualByName.set(name, node);
1476
+ }
1477
+ // A flat actual schema (SQLite) has no named namespaces — it is the sole node.
1478
+ const soleFlatActual = actualByName.size === 0 ? actualNodes[0] : undefined;
1479
+ const emptyNamespace: SqlSchemaIR = { tables: {} };
1480
+
1481
+ let combined: VerifyDatabaseSchemaResult | undefined;
1482
+ for (const namespaceId of Object.keys(options.contract.storage.namespaces)) {
1483
+ const namespace = options.contract.storage.namespaces[namespaceId];
1484
+ if (!namespace || Object.keys(namespace.entries.table ?? {}).length === 0) continue;
1485
+
1486
+ const ddlSchema = namespaceSchemaNodes(
1487
+ options.buildExpectedSchema(scopeContractToNamespace(options.contract, namespaceId)),
1488
+ )
1489
+ .map(namespaceSchemaName)
1490
+ .find((name) => name !== undefined);
1491
+ const actualNode =
1492
+ (ddlSchema !== undefined ? actualByName.get(ddlSchema) : soleFlatActual) ?? emptyNamespace;
1493
+
1494
+ const result = verifySqlSchema({
1495
+ ...baseOptions,
1496
+ schema: actualNode,
1497
+ restrictToNamespaceIds: new Set([namespaceId]),
1498
+ });
1499
+ combined = combined === undefined ? result : mergeVerifyResults(combined, result);
1500
+ }
1501
+
1502
+ return combined ?? verifySqlSchema({ ...baseOptions, schema: emptyNamespace });
1503
+ }
@@ -151,6 +151,7 @@ export function verifyPrimaryKey(
151
151
  if (!schemaPK) {
152
152
  const issue: SchemaIssue = {
153
153
  kind: 'primary_key_mismatch',
154
+ reason: 'not-equal',
154
155
  table: tableName,
155
156
  namespaceId,
156
157
  expected: contractPK.columns.join(', '),
@@ -163,6 +164,7 @@ export function verifyPrimaryKey(
163
164
  if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
164
165
  const issue: SchemaIssue = {
165
166
  kind: 'primary_key_mismatch',
167
+ reason: 'not-equal',
166
168
  table: tableName,
167
169
  namespaceId,
168
170
  expected: contractPK.columns.join(', '),
@@ -220,6 +222,7 @@ export function verifyForeignKeys(
220
222
  if (!matchingFK) {
221
223
  const issue: SchemaIssue = {
222
224
  kind: 'foreign_key_mismatch',
225
+ reason: 'not-equal',
223
226
  table: tableName,
224
227
  namespaceId,
225
228
  expected: `${contractFK.source.columns.join(', ')} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(', ')})`,
@@ -250,6 +253,7 @@ export function verifyForeignKeys(
250
253
  const combinedActual = actionMismatches.map((m) => m.actual).join(', ');
251
254
  const issue: SchemaIssue = {
252
255
  kind: 'foreign_key_mismatch',
256
+ reason: 'not-equal',
253
257
  table: tableName,
254
258
  namespaceId,
255
259
  indexOrConstraint: matchingFK.name ?? `fk(${contractFK.source.columns.join(',')})`,
@@ -309,6 +313,7 @@ export function verifyForeignKeys(
309
313
  if (!matchingFK) {
310
314
  const issue: SchemaIssue = {
311
315
  kind: 'extra_foreign_key',
316
+ reason: 'not-expected',
312
317
  table: tableName,
313
318
  namespaceId,
314
319
  indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(',')})`,
@@ -379,6 +384,7 @@ export function verifyUniqueConstraints(
379
384
  if (!matchingUnique && !matchingUniqueIndex) {
380
385
  const issue: SchemaIssue = {
381
386
  kind: 'unique_constraint_mismatch',
387
+ reason: 'not-equal',
382
388
  table: tableName,
383
389
  namespaceId,
384
390
  expected: contractUnique.columns.join(', '),
@@ -425,6 +431,7 @@ export function verifyUniqueConstraints(
425
431
  if (!matchingUnique) {
426
432
  const issue: SchemaIssue = {
427
433
  kind: 'extra_unique_constraint',
434
+ reason: 'not-expected',
428
435
  table: tableName,
429
436
  namespaceId,
430
437
  indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(',')})`,
@@ -501,6 +508,7 @@ export function verifyIndexes(
501
508
  if (!matchingIndex && !matchingUniqueConstraint) {
502
509
  const issue: SchemaIssue = {
503
510
  kind: 'index_mismatch',
511
+ reason: 'not-equal',
504
512
  table: tableName,
505
513
  namespaceId,
506
514
  expected: contractIndex.columns.join(', '),
@@ -552,6 +560,7 @@ export function verifyIndexes(
552
560
  if (!matchingIndex) {
553
561
  const issue: SchemaIssue = {
554
562
  kind: 'extra_index',
563
+ reason: 'not-expected',
555
564
  table: tableName,
556
565
  namespaceId,
557
566
  indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(',')})`,
@@ -718,6 +727,7 @@ export function verifyCheckConstraints(
718
727
  if (!liveCheck) {
719
728
  const issue: SchemaIssue = {
720
729
  kind: 'check_missing',
730
+ reason: 'not-found',
721
731
  table: tableName,
722
732
  namespaceId,
723
733
  indexOrConstraint: contractCheck.name,
@@ -744,6 +754,7 @@ export function verifyCheckConstraints(
744
754
  } else if (!valueSetsEqual(contractCheck.permittedValues, liveCheck.permittedValues)) {
745
755
  const issue: SchemaIssue = {
746
756
  kind: 'check_mismatch',
757
+ reason: 'not-equal',
747
758
  table: tableName,
748
759
  namespaceId,
749
760
  indexOrConstraint: contractCheck.name,
@@ -789,6 +800,7 @@ export function verifyCheckConstraints(
789
800
  if (!matchingContract) {
790
801
  const issue: SchemaIssue = {
791
802
  kind: 'check_removed',
803
+ reason: 'not-equal',
792
804
  table: tableName,
793
805
  namespaceId,
794
806
  indexOrConstraint: liveCheck.name,
@@ -6,18 +6,15 @@ import {
6
6
  type AnyEntityKindDescriptor,
7
7
  hydrateNamespaceEntities,
8
8
  type Namespace,
9
- NamespaceBase,
10
- UNBOUND_NAMESPACE_ID,
11
9
  } from '@prisma-next/framework-components/ir';
12
10
  import { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';
13
11
  import { composeSqlEntityKinds } from '@prisma-next/sql-contract/entity-kinds';
14
12
  import {
15
- buildSqlNamespace,
16
- type SqlNamespaceTablesInput,
13
+ isMaterializedSqlNamespace,
14
+ type SqlNamespaceInput,
17
15
  SqlStorage,
18
16
  type SqlStorageInput,
19
17
  type SqlStorageTypeEntry,
20
- SqlUnboundNamespace,
21
18
  } from '@prisma-next/sql-contract/types';
22
19
  import {
23
20
  createSqlContractSchema,
@@ -120,77 +117,48 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
120
117
  'structural',
121
118
  );
122
119
  }
123
- const hydratedNamespaces = this.hydrateSqlNamespaceMap(rawNamespaces);
124
- // Compatibility shim: production code that addresses `__unbound__` for table
125
- // metadata lookups (collection-contract, query-plan-mutations, model-accessor,
126
- // query-plan-meta, where-binding) uses optional chaining and tolerates absence,
127
- // but runtime-qualification (TML-2605) has not yet landed cross-namespace table
128
- // routing. Injecting the empty singleton here keeps helpers that augment the
129
- // deserialized JSON (e.g. buildMixedPolyContract) working by providing a slot to
130
- // write into. Once runtime-qualification routes table lookups by namespace, this
131
- // shim should be removed.
132
- //
133
- // TML-2916: the shim only fires when the target's default namespace IS unbound
134
- // (SQLite, Mongo). On Postgres (`defaultNamespaceId === 'public'`) injecting an
135
- // empty `__unbound__` slot violates ADR 223 — un-namespaced PG models belong in
136
- // `public`, not `__unbound__`.
137
- const withInjectedUnbound =
138
- this.defaultNamespaceId === UNBOUND_NAMESPACE_ID
139
- ? {
140
- ...hydratedNamespaces,
141
- [UNBOUND_NAMESPACE_ID]:
142
- hydratedNamespaces[UNBOUND_NAMESPACE_ID] ?? SqlUnboundNamespace.instance,
143
- }
144
- : hydratedNamespaces;
120
+ const hydratedNamespaces = this.hydrateSqlNamespaceMap(
121
+ blindCast<
122
+ Readonly<Record<string, Record<string, unknown>>>,
123
+ 'parseSqlContractStructure validated raw JSON; namespace entries are plain objects, not SqlNamespace instances.'
124
+ >(rawNamespaces),
125
+ );
145
126
 
146
127
  return {
147
128
  ...validated,
148
129
  storage: new SqlStorage({
149
130
  storageHash: validated.storage.storageHash,
150
131
  ...ifDefined('types', hydratedTypes),
151
- // Cast narrows the result of hydrateSqlNamespaceMap from the wider
152
- // framework `Namespace` to the SQL-family `SqlNamespace`.
153
132
  namespaces: blindCast<
154
133
  SqlStorageInput['namespaces'],
155
- 'hydrateSqlNamespaceMap builds each namespace through the SQL family concretions (SqlBoundNamespace / target schema), so every value is a SqlNamespace; the framework return type only promises the base Namespace.'
156
- >(withInjectedUnbound),
134
+ 'hydrateSqlNamespaceMap builds each namespace through the target serializer override, so every value is a SqlNamespace; the framework return type only promises the base Namespace.'
135
+ >(hydratedNamespaces),
157
136
  }),
158
137
  };
159
138
  }
160
139
 
161
- protected abstract get defaultNamespaceId(): string;
162
-
163
140
  protected hydrateSqlNamespaceMap(
164
- namespaces: Readonly<Record<string, Namespace | Record<string, unknown>>>,
141
+ namespaces: Readonly<Record<string, Record<string, unknown>>>,
165
142
  ): Readonly<Record<string, Namespace>> {
166
143
  return Object.fromEntries(
167
144
  Object.entries(namespaces).map(([nsId, namespaceEntryRaw]) => {
168
- // Raw entries passed structural validation; hydrate materialises family IR class instances.
169
145
  const namespaceHydrated = this.hydrateSqlNamespaceEntry(nsId, namespaceEntryRaw);
170
- const namespaceMaterialised =
171
- namespaceHydrated instanceof NamespaceBase
172
- ? namespaceHydrated
173
- : buildSqlNamespace(
174
- blindCast<
175
- SqlNamespaceTablesInput,
176
- 'hydrateSqlNamespaceEntry returns SqlNamespaceTablesInput when raw is not a NamespaceBase'
177
- >(namespaceHydrated),
178
- );
179
- return [nsId, namespaceMaterialised];
146
+ if (!isMaterializedSqlNamespace(namespaceHydrated)) {
147
+ throw new Error(
148
+ `Target serializer bug: hydrateSqlNamespaceEntry for namespace "${nsId}" returned a non-NamespaceBase value. Override hydrateSqlNamespaceEntry to produce a target namespace concretion.`,
149
+ );
150
+ }
151
+ return [nsId, namespaceHydrated];
180
152
  }),
181
153
  );
182
154
  }
183
155
 
184
156
  protected hydrateSqlNamespaceEntry(
185
157
  nsId: string,
186
- raw: Namespace | Record<string, unknown>,
187
- ): Namespace | SqlNamespaceTablesInput {
188
- if (raw instanceof NamespaceBase) {
189
- return raw;
190
- }
191
- const rawRecord = isPlainRecord(raw) ? raw : {};
192
- const id = typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId;
193
- const parsed = NamespaceRawSchema({ ...rawRecord, id });
158
+ raw: Record<string, unknown>,
159
+ ): Namespace | SqlNamespaceInput {
160
+ const id = typeof raw['id'] === 'string' ? raw['id'] : nsId;
161
+ const parsed = NamespaceRawSchema({ ...raw, id });
194
162
  if (parsed instanceof type.errors) {
195
163
  const messages = parsed.map((p: { message: string }) => p.message).join('; ');
196
164
  throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');
@@ -211,8 +179,8 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
211
179
  }
212
180
 
213
181
  return blindCast<
214
- SqlNamespaceTablesInput,
215
- 'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceTablesInput the family createNamespace consumes.'
182
+ SqlNamespaceInput,
183
+ 'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceInput the target createNamespace consumes.'
216
184
  >({
217
185
  id,
218
186
  entries: entriesOutput,
@@ -1,5 +1,4 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
- import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
3
2
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
4
3
  import { SqlContractSerializerBase } from './sql-contract-serializer-base';
5
4
 
@@ -11,15 +10,14 @@ import { SqlContractSerializerBase } from './sql-contract-serializer-base';
11
10
  * codec-typed envelope wire a target-specific subclass with a populated
12
11
  * registry (see Postgres). Family-level call sites instantiate this
13
12
  * default directly when no target serializer is supplied.
13
+ *
14
+ * Because this serializer has no target concretion, deserialization of
15
+ * contracts that include namespace entries from JSON will throw unless
16
+ * the caller provides pre-hydrated `NamespaceBase` instances. Production
17
+ * paths always supply a target-specific serializer.
14
18
  */
15
19
  export class SqlContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {
16
20
  constructor() {
17
21
  super(new Map());
18
22
  }
19
-
20
- // Family-level fallback when no target descriptor is wired in. Preserves the
21
- // pre-TML-2916 compatibility-shim behaviour for the unbound slot.
22
- protected override get defaultNamespaceId(): string {
23
- return UNBOUND_NAMESPACE_ID;
24
- }
25
23
  }