@prisma-next/family-sql 0.14.0-dev.39 → 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 (46) hide show
  1. package/dist/contract-to-schema-ir-S-evq8E6.mjs +264 -0
  2. package/dist/contract-to-schema-ir-S-evq8E6.mjs.map +1 -0
  3. package/dist/{control-adapter-B2q9WN1V.d.mts → control-adapter-DHYFuOBy.d.mts} +14 -17
  4. package/dist/{control-adapter-B2q9WN1V.d.mts.map → control-adapter-DHYFuOBy.d.mts.map} +1 -1
  5. package/dist/control-adapter.d.mts +1 -1
  6. package/dist/control.d.mts +17 -2
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +42 -943
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/{schema-verify.d.mts → diff.d.mts} +4 -4
  11. package/dist/diff.d.mts.map +1 -0
  12. package/dist/{verify-sql-schema-CEY7b78t.mjs → diff.mjs} +154 -244
  13. package/dist/diff.mjs.map +1 -0
  14. package/dist/migration.d.mts +1 -1
  15. package/dist/psl-infer.d.mts +80 -0
  16. package/dist/psl-infer.d.mts.map +1 -0
  17. package/dist/psl-infer.mjs +334 -0
  18. package/dist/psl-infer.mjs.map +1 -0
  19. package/dist/{verify-sql-schema-B8uEjw2s.d.mts → sql-schema-diff-6z36dZt6.d.mts} +43 -4
  20. package/dist/sql-schema-diff-6z36dZt6.d.mts.map +1 -0
  21. package/dist/{types-BxsFkHYv.d.mts → types-CkOIJXxU.d.mts} +83 -16
  22. package/dist/types-CkOIJXxU.d.mts.map +1 -0
  23. package/package.json +23 -22
  24. package/src/core/control-adapter.ts +12 -24
  25. package/src/core/control-instance.ts +113 -45
  26. package/src/core/{schema-verify/verify-sql-schema.ts → diff/sql-schema-diff.ts} +196 -8
  27. package/src/core/{schema-verify → diff}/verify-helpers.ts +12 -0
  28. package/src/core/migrations/contract-to-schema-ir.ts +48 -13
  29. package/src/core/migrations/field-event-planner.ts +3 -8
  30. package/src/core/migrations/schema-differ.ts +40 -0
  31. package/src/core/migrations/types.ts +43 -2
  32. package/src/core/psl-contract-infer/printer-config.ts +0 -6
  33. package/src/exports/control.ts +1 -0
  34. package/src/exports/diff.ts +18 -0
  35. package/src/exports/psl-infer.ts +36 -0
  36. package/dist/schema-verify.d.mts.map +0 -1
  37. package/dist/schema-verify.mjs +0 -2
  38. package/dist/types-BxsFkHYv.d.mts.map +0 -1
  39. package/dist/verify-sql-schema-B8uEjw2s.d.mts.map +0 -1
  40. package/dist/verify-sql-schema-CEY7b78t.mjs.map +0 -1
  41. package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
  42. package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
  43. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
  44. package/src/exports/schema-verify.ts +0 -18
  45. /package/src/core/{schema-verify → diff}/control-verify-emit.ts +0 -0
  46. /package/src/core/{schema-verify → diff}/verifier-disposition.ts +0 -0
@@ -18,15 +18,15 @@ import type {
18
18
  } from '@prisma-next/framework-components/control';
19
19
 
20
20
  import {
21
- isStorageTable,
22
21
  isStorageTypeInstance,
23
22
  type SqlStorage,
24
23
  type StorageColumn,
25
- type StorageTable,
24
+ StorageTable,
26
25
  type StorageTypeInstance,
27
26
  } from '@prisma-next/sql-contract/types';
28
- import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
27
+ import type { SqlSchemaIR, SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
29
28
  import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
29
+ import { blindCast } from '@prisma-next/utils/casts';
30
30
  import { ifDefined } from '@prisma-next/utils/defined';
31
31
  import { extractCodecControlHooks } from '../assembly';
32
32
  import { resolveValueSetValues } from '../migrations/contract-to-schema-ir';
@@ -43,6 +43,30 @@ import {
43
43
  verifyUniqueConstraints,
44
44
  } from './verify-helpers';
45
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
+
46
70
  /**
47
71
  * Function type for normalizing raw database default expressions into ColumnDefault.
48
72
  * Target-specific implementations handle database dialect differences.
@@ -90,6 +114,15 @@ export interface VerifySqlSchemaOptions {
90
114
  * with contract native types (e.g., Postgres 'varchar' → 'character varying').
91
115
  */
92
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>;
93
126
  }
94
127
 
95
128
  /**
@@ -129,6 +162,7 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
129
162
  storageTypes,
130
163
  ...ifDefined('normalizeDefault', normalizeDefault),
131
164
  ...ifDefined('normalizeNativeType', normalizeNativeType),
165
+ ...ifDefined('restrictToNamespaceIds', options.restrictToNamespaceIds),
132
166
  });
133
167
 
134
168
  validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
@@ -276,6 +310,7 @@ function verifySchemaTables(options: {
276
310
  storageTypes: Readonly<Record<string, StorageTypeInstance>>;
277
311
  normalizeDefault?: DefaultNormalizer;
278
312
  normalizeNativeType?: NativeTypeNormalizer;
313
+ restrictToNamespaceIds?: ReadonlySet<string>;
279
314
  }): { issues: SchemaIssue[]; rootChildren: SchemaVerificationNode[] } {
280
315
  const {
281
316
  contract,
@@ -286,6 +321,7 @@ function verifySchemaTables(options: {
286
321
  storageTypes,
287
322
  normalizeDefault,
288
323
  normalizeNativeType,
324
+ restrictToNamespaceIds,
289
325
  } = options;
290
326
  const contractDefaultControl = contract.defaultControlPolicy;
291
327
  const issues: SchemaIssue[] = [];
@@ -296,14 +332,17 @@ function verifySchemaTables(options: {
296
332
  );
297
333
 
298
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;
299
339
  const ns = contract.storage.namespaces[namespaceId];
300
340
  if (!ns) continue;
301
341
  for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table ?? {})) {
302
- if (!isStorageTable(contractTableRaw)) {
303
- throw new Error(
304
- `verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.entries.table.${tableName}`,
305
- );
306
- }
342
+ StorageTable.assert(
343
+ contractTableRaw,
344
+ `storage.namespaces.${namespaceId}.entries.table.${tableName}`,
345
+ );
307
346
  const contractTable = contractTableRaw;
308
347
  const tableControlPolicy = effectiveControlPolicy(
309
348
  contractTable.control,
@@ -315,6 +354,7 @@ function verifySchemaTables(options: {
315
354
  if (!schemaTable) {
316
355
  const issue: SchemaIssue = {
317
356
  kind: 'missing_table',
357
+ reason: 'not-found',
318
358
  table: tableName,
319
359
  namespaceId,
320
360
  message: `Table "${tableName}" is missing from database`,
@@ -369,6 +409,7 @@ function verifySchemaTables(options: {
369
409
  const extraTableControlPolicy = effectiveControlPolicy(undefined, contractDefaultControl);
370
410
  const issue: SchemaIssue = {
371
411
  kind: 'extra_table',
412
+ reason: 'not-expected',
372
413
  table: tableName,
373
414
  message: `Extra table "${tableName}" found in database (not in contract)`,
374
415
  };
@@ -385,6 +426,7 @@ function verifySchemaTables(options: {
385
426
  expected: undefined,
386
427
  actual: undefined,
387
428
  children: [],
429
+ reason: 'not-expected',
388
430
  },
389
431
  issues,
390
432
  rootChildren,
@@ -509,6 +551,7 @@ function verifyTableChildren(options: {
509
551
  } else if (schemaTable.primaryKey && strict) {
510
552
  const issue: SchemaIssue = {
511
553
  kind: 'extra_primary_key',
554
+ reason: 'not-expected',
512
555
  table: tableName,
513
556
  namespaceId,
514
557
  message: 'Extra primary key found in database (not in contract)',
@@ -652,6 +695,7 @@ function collectContractColumnNodes(options: {
652
695
  if (!schemaColumn) {
653
696
  const issue: SchemaIssue = {
654
697
  kind: 'missing_column',
698
+ reason: 'not-found',
655
699
  table: tableName,
656
700
  namespaceId,
657
701
  column: columnName,
@@ -724,6 +768,7 @@ function appendExtraColumnNodes(options: {
724
768
  if (!contractTable.columns[columnName]) {
725
769
  const issue: SchemaIssue = {
726
770
  kind: 'extra_column',
771
+ reason: 'not-expected',
727
772
  table: tableName,
728
773
  namespaceId,
729
774
  column: columnName,
@@ -801,6 +846,7 @@ function verifyColumn(options: {
801
846
  if (!typesMatch) {
802
847
  const issue: SchemaIssue = {
803
848
  kind: 'type_mismatch',
849
+ reason: 'not-equal',
804
850
  table: tableName,
805
851
  namespaceId,
806
852
  column: columnName,
@@ -861,6 +907,7 @@ function verifyColumn(options: {
861
907
  if (contractColumn.nullable !== schemaColumn.nullable) {
862
908
  const issue: SchemaIssue = {
863
909
  kind: 'nullability_mismatch',
910
+ reason: 'not-equal',
864
911
  table: tableName,
865
912
  namespaceId,
866
913
  column: columnName,
@@ -891,6 +938,7 @@ function verifyColumn(options: {
891
938
  const defaultDescription = describeColumnDefault(contractColumn.default);
892
939
  const issue: SchemaIssue = {
893
940
  kind: 'default_missing',
941
+ reason: 'not-found',
894
942
  table: tableName,
895
943
  namespaceId,
896
944
  column: columnName,
@@ -925,6 +973,7 @@ function verifyColumn(options: {
925
973
  const actualDescription = schemaColumn.default;
926
974
  const issue: SchemaIssue = {
927
975
  kind: 'default_mismatch',
976
+ reason: 'not-equal',
928
977
  table: tableName,
929
978
  namespaceId,
930
979
  column: columnName,
@@ -952,6 +1001,7 @@ function verifyColumn(options: {
952
1001
  } else if (strict && schemaColumn.default) {
953
1002
  const issue: SchemaIssue = {
954
1003
  kind: 'extra_default',
1004
+ reason: 'not-expected',
955
1005
  table: tableName,
956
1006
  namespaceId,
957
1007
  column: columnName,
@@ -1313,3 +1363,141 @@ function formatLiteralValue(value: unknown): string {
1313
1363
  }
1314
1364
  return JSON.stringify(value);
1315
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,
@@ -4,11 +4,10 @@ import {
4
4
  type CheckConstraint,
5
5
  type ForeignKey,
6
6
  type Index,
7
- isStorageTable,
8
7
  isStorageTypeInstance,
9
8
  type SqlStorage,
10
9
  type StorageColumn,
11
- type StorageTable,
10
+ StorageTable,
12
11
  type StorageTypeInstance,
13
12
  type UniqueConstraint,
14
13
  } from '@prisma-next/sql-contract/types';
@@ -305,7 +304,7 @@ export function detectDestructiveChanges(
305
304
 
306
305
  for (const tableName of Object.keys(fromTables)) {
307
306
  const toTableRaw = toNs?.entries.table?.[tableName];
308
- if (!isStorageTable(toTableRaw)) {
307
+ if (!StorageTable.is(toTableRaw)) {
309
308
  conflicts.push({
310
309
  kind: 'tableRemoved',
311
310
  summary: `Table "${tableName}" was removed`,
@@ -315,7 +314,7 @@ export function detectDestructiveChanges(
315
314
  const toTable = toTableRaw;
316
315
 
317
316
  const fromTableRaw = fromTables[tableName];
318
- if (!isStorageTable(fromTableRaw)) continue;
317
+ if (!StorageTable.is(fromTableRaw)) continue;
319
318
  const fromTable = fromTableRaw;
320
319
 
321
320
  for (const columnName of Object.keys(fromTable.columns)) {
@@ -360,6 +359,48 @@ export interface ContractToSchemaIROptions {
360
359
  *
361
360
  * Returns an empty schema IR when `contract` is `null` (new project).
362
361
  */
362
+ /**
363
+ * Converts the tables of a single namespace into a `SqlSchemaIR`, keyed by
364
+ * table name within that namespace. Unlike {@link contractToSchemaIR}, which
365
+ * flattens every namespace's tables into one bare-keyed record (and throws on a
366
+ * cross-namespace name collision), this scopes the table iteration to one
367
+ * namespace so the same table name can exist in two schemas.
368
+ *
369
+ * The full `storage` is still passed to `convertTable`, so value-set / enum /
370
+ * type resolution that legitimately spans namespaces is unaffected. Foreign
371
+ * keys are built purely from the FK descriptor (`fk.target`), so cross-namespace
372
+ * FKs survive per-namespace conversion. The `annotations` block (storage-type
373
+ * derived) is omitted here — the per-namespace tree consumer reads only the
374
+ * per-table fields.
375
+ */
376
+ export function contractNamespaceToSchemaIR(
377
+ storage: SqlStorage,
378
+ namespaceId: string,
379
+ options: ContractToSchemaIROptions,
380
+ ): SqlSchemaIR {
381
+ if (options.annotationNamespace.length === 0) {
382
+ throw new Error('annotationNamespace must be a non-empty string');
383
+ }
384
+ const namespace = storage.namespaces[namespaceId];
385
+ if (!namespace) {
386
+ return { tables: {} };
387
+ }
388
+ const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
389
+ const tables: Record<string, SqlTableIR> = {};
390
+ for (const [tableName, tableDefRaw] of Object.entries(namespace.entries.table ?? {})) {
391
+ StorageTable.assert(tableDefRaw, `namespaces.${namespaceId}.entries.table.${tableName}`);
392
+ tables[tableName] = convertTable(
393
+ tableName,
394
+ tableDefRaw,
395
+ storageTypes,
396
+ options.expandNativeType,
397
+ options.renderDefault,
398
+ storage,
399
+ );
400
+ }
401
+ return { tables };
402
+ }
403
+
363
404
  export function contractToSchemaIR(
364
405
  contract: Contract<SqlStorage> | null,
365
406
  options: ContractToSchemaIROptions,
@@ -373,17 +414,11 @@ export function contractToSchemaIR(
373
414
  }
374
415
 
375
416
  const storage = contract.storage;
376
- const storageTypes: ResolvedStorageTypes = {
377
- ...((storage.types ?? {}) as ResolvedStorageTypes),
378
- };
417
+ const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
379
418
  const tables: Record<string, SqlTableIR> = {};
380
419
  for (const ns of Object.values(storage.namespaces)) {
381
420
  for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table ?? {})) {
382
- if (!isStorageTable(tableDefRaw)) {
383
- throw new Error(
384
- `contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.entries.table.${tableName}`,
385
- );
386
- }
421
+ StorageTable.assert(tableDefRaw, `namespaces.${ns.id}.entries.table.${tableName}`);
387
422
  const tableDef = tableDefRaw;
388
423
  if (tables[tableName] !== undefined) {
389
424
  throw new Error(
@@ -420,7 +455,7 @@ function deriveAnnotations(
420
455
  ): SqlAnnotations | undefined {
421
456
  const storageTypes: Record<string, StorageTypeInstance> = {};
422
457
 
423
- for (const typeInstance of Object.values((storage.types ?? {}) as ResolvedStorageTypes)) {
458
+ for (const typeInstance of Object.values(storage.types ?? {})) {
424
459
  if (isStorageTypeInstance(typeInstance)) {
425
460
  storageTypes[typeInstance.nativeType] = typeInstance;
426
461
  }
@@ -24,12 +24,7 @@
24
24
 
25
25
  import type { Contract } from '@prisma-next/contract/types';
26
26
  import type { OpFactoryCall } from '@prisma-next/framework-components/control';
27
- import {
28
- isStorageTable,
29
- type SqlStorage,
30
- type StorageColumn,
31
- type StorageTable,
32
- } from '@prisma-next/sql-contract/types';
27
+ import { type SqlStorage, type StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
33
28
  import type { CodecControlHooks, FieldEvent, FieldEventContext } from './types';
34
29
 
35
30
  export interface PlanFieldEventOperationsOptions {
@@ -93,8 +88,8 @@ export function planFieldEventOperations(
93
88
  for (const tableName of tableNames) {
94
89
  const priorTableRaw = priorTables?.[tableName];
95
90
  const newTableRaw = newTables?.[tableName];
96
- const priorTable = isStorageTable(priorTableRaw) ? priorTableRaw : undefined;
97
- const newTable = isStorageTable(newTableRaw) ? newTableRaw : undefined;
91
+ const priorTable = StorageTable.is(priorTableRaw) ? priorTableRaw : undefined;
92
+ const newTable = StorageTable.is(newTableRaw) ? newTableRaw : undefined;
98
93
  const fieldNames = unionSorted(
99
94
  priorTable ? Object.keys(priorTable.columns) : [],
100
95
  newTable ? Object.keys(newTable.columns) : [],
@@ -0,0 +1,40 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
3
+ import type {
4
+ SchemaDiffer,
5
+ VerifyDatabaseSchemaResult,
6
+ } from '@prisma-next/framework-components/control';
7
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
8
+ import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
9
+
10
+ /**
11
+ * Inputs to a SQL target's schema-differ (`diffDatabaseSchema` /
12
+ * `verifyDatabaseSchema` on the descriptor): the contract (the expected side
13
+ * derives from it), the introspected actual schema node, and the resolution
14
+ * context the relational diff needs.
15
+ */
16
+ export interface DiffDatabaseSchemaInput {
17
+ readonly contract: Contract<SqlStorage>;
18
+ readonly schema: SqlSchemaIRNode;
19
+ readonly strict: boolean;
20
+ readonly typeMetadataRegistry: ReadonlyMap<string, { readonly nativeType?: string }>;
21
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
22
+ }
23
+
24
+ /**
25
+ * The `SchemaDiffer` a SQL target implements: the black-box comparison of the
26
+ * contract's expected schema against the introspected actual schema, projected
27
+ * to the two issue lists. How it computes them is private to the target.
28
+ */
29
+ export type SqlDiffDatabaseSchema = SchemaDiffer<DiffDatabaseSchemaInput>['diff'];
30
+
31
+ /**
32
+ * The same combined comparison as {@link SqlDiffDatabaseSchema}, wrapped in the
33
+ * verify envelope (`ok`/`summary`/`code`/`target`/`timings`) plus the
34
+ * pass/warn/fail tree the CLI renders. Verify calls this instead of the diff so
35
+ * the relational walk that produces the tree runs once per verify, not once for
36
+ * the diff and again for the tree.
37
+ */
38
+ export type SqlVerifyDatabaseSchema = (
39
+ input: DiffDatabaseSchemaInput,
40
+ ) => VerifyDatabaseSchemaResult;