@prisma-next/family-sql 0.12.0-dev.4 → 0.12.0-dev.41

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 (47) hide show
  1. package/dist/control-adapter-uYnmu04p.d.mts +181 -0
  2. package/dist/control-adapter-uYnmu04p.d.mts.map +1 -0
  3. package/dist/control-adapter.d.mts +2 -109
  4. package/dist/control.d.mts +118 -4
  5. package/dist/control.d.mts.map +1 -1
  6. package/dist/control.mjs +226 -40
  7. package/dist/control.mjs.map +1 -1
  8. package/dist/ir.d.mts +2 -2
  9. package/dist/ir.d.mts.map +1 -1
  10. package/dist/ir.mjs +1 -1
  11. package/dist/migration.d.mts +1 -1
  12. package/dist/runtime.d.mts +4 -2
  13. package/dist/runtime.d.mts.map +1 -1
  14. package/dist/runtime.mjs +3 -1
  15. package/dist/runtime.mjs.map +1 -1
  16. package/dist/schema-verify.d.mts +2 -1
  17. package/dist/schema-verify.d.mts.map +1 -1
  18. package/dist/schema-verify.mjs +1 -1
  19. package/dist/{sql-contract-serializer-8axtK4lg.mjs → sql-contract-serializer-nNw1yk9P.mjs} +14 -35
  20. package/dist/sql-contract-serializer-nNw1yk9P.mjs.map +1 -0
  21. package/dist/{types-CeeCStqw.d.mts → types-DxNgxFkF.d.mts} +61 -7
  22. package/dist/types-DxNgxFkF.d.mts.map +1 -0
  23. package/dist/{verify-sql-schema-CN7pPoTC.d.mts → verify-sql-schema-Cj3c2t5Z.d.mts} +8 -2
  24. package/dist/verify-sql-schema-Cj3c2t5Z.d.mts.map +1 -0
  25. package/dist/{verify-sql-schema-CYLsGCFO.mjs → verify-sql-schema-DV_d2XCG.mjs} +410 -323
  26. package/dist/verify-sql-schema-DV_d2XCG.mjs.map +1 -0
  27. package/package.json +21 -21
  28. package/src/core/control-adapter.ts +112 -3
  29. package/src/core/control-instance.ts +143 -56
  30. package/src/core/default-namespace.ts +9 -0
  31. package/src/core/ir/sql-contract-serializer-base.ts +42 -57
  32. package/src/core/migrations/contract-to-schema-ir.ts +12 -8
  33. package/src/core/migrations/control-policy.ts +322 -0
  34. package/src/core/migrations/field-event-planner.ts +2 -2
  35. package/src/core/migrations/plan-helpers.ts +16 -0
  36. package/src/core/migrations/types.ts +12 -2
  37. package/src/core/schema-verify/control-verify-emit.ts +46 -0
  38. package/src/core/schema-verify/verifier-disposition.ts +53 -0
  39. package/src/core/schema-verify/verify-helpers.ts +151 -110
  40. package/src/core/schema-verify/verify-sql-schema.ts +305 -178
  41. package/src/exports/control.ts +6 -0
  42. package/src/exports/runtime.ts +7 -0
  43. package/dist/control-adapter.d.mts.map +0 -1
  44. package/dist/sql-contract-serializer-8axtK4lg.mjs.map +0 -1
  45. package/dist/types-CeeCStqw.d.mts.map +0 -1
  46. package/dist/verify-sql-schema-CN7pPoTC.d.mts.map +0 -1
  47. package/dist/verify-sql-schema-CYLsGCFO.mjs.map +0 -1
@@ -6,15 +6,17 @@
6
6
  * by migration planners and other tools that need to compare schema states.
7
7
  */
8
8
 
9
- import type { ColumnDefault, Contract } from '@prisma-next/contract/types';
9
+ import type { ColumnDefault, Contract, ControlPolicy } from '@prisma-next/contract/types';
10
+ import { effectiveControlPolicy } from '@prisma-next/contract/types';
10
11
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
11
12
  import type {
12
13
  OperationContext,
13
14
  SchemaIssue,
14
15
  SchemaVerificationNode,
16
+ VerificationStatus,
15
17
  VerifyDatabaseSchemaResult,
16
18
  } from '@prisma-next/framework-components/control';
17
- import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
19
+
18
20
  import {
19
21
  isPostgresEnumStorageEntry,
20
22
  isStorageTypeInstance,
@@ -26,9 +28,12 @@ import {
26
28
  } from '@prisma-next/sql-contract/types';
27
29
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
28
30
  import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
31
+ import { blindCast } from '@prisma-next/utils/casts';
29
32
  import { ifDefined } from '@prisma-next/utils/defined';
30
33
  import { extractCodecControlHooks } from '../assembly';
31
34
  import type { CodecControlHooks } from '../migrations/types';
35
+ import { emitIssueAndNodeUnderControlPolicy } from './control-verify-emit';
36
+ import { verifierDisposition } from './verifier-disposition';
32
37
  import {
33
38
  arraysEqual,
34
39
  computeCounts,
@@ -38,6 +43,8 @@ import {
38
43
  verifyUniqueConstraints,
39
44
  } from './verify-helpers';
40
45
 
46
+ export type ColumnsCompatible = (declared: string, live: string) => boolean;
47
+
41
48
  /**
42
49
  * Function type for normalizing raw database default expressions into ColumnDefault.
43
50
  * Target-specific implementations handle database dialect differences.
@@ -101,6 +108,11 @@ export interface VerifySqlSchemaOptions {
101
108
  enumType: PostgresEnumStorageEntry,
102
109
  namespaceId: string,
103
110
  ) => readonly string[] | null;
111
+ /**
112
+ * Target-supplied compatible-shape relation for `external` column type checks.
113
+ * Defaults to exact string equality when omitted.
114
+ */
115
+ readonly columnsCompatible?: ColumnsCompatible;
104
116
  }
105
117
 
106
118
  /**
@@ -123,6 +135,7 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
123
135
  normalizeDefault,
124
136
  normalizeNativeType,
125
137
  resolveExistingEnumValues,
138
+ columnsCompatible,
126
139
  } = options;
127
140
  const startTime = Date.now();
128
141
 
@@ -140,7 +153,10 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
140
153
  >),
141
154
  };
142
155
  for (const ns of Object.values(contract.storage.namespaces)) {
143
- const nsEnums = (ns as { enum?: Record<string, PostgresEnumStorageEntry> }).enum;
156
+ const nsEnums = blindCast<
157
+ { readonly type?: Readonly<Record<string, PostgresEnumStorageEntry | StorageTypeInstance>> },
158
+ 'postgres target namespace entries carry a type slot beyond the family-shared SqlNamespace.entries type'
159
+ >(ns.entries).type;
144
160
  if (nsEnums) {
145
161
  for (const [k, v] of Object.entries(nsEnums)) {
146
162
  allStorageTypesMap[k] = v;
@@ -159,6 +175,7 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
159
175
  storageTypes,
160
176
  ...ifDefined('normalizeDefault', normalizeDefault),
161
177
  ...ifDefined('normalizeNativeType', normalizeNativeType),
178
+ ...ifDefined('columnsCompatible', columnsCompatible),
162
179
  });
163
180
 
164
181
  validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
@@ -171,51 +188,54 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
171
188
  // namespace coordinate — a bare-name aggregation would collapse them
172
189
  // (last-write-wins) and verify only one.
173
190
  const typeNodes: SchemaVerificationNode[] = [];
191
+ // Storage-type findings dispatch through the same control policy as tables
192
+ // and columns: each issue's disposition (fail / warn / suppress) is resolved
193
+ // from the type's effective control so an `external`/`observed` enum no longer
194
+ // hard-fails on value drift. `suppress` drops the issue entirely; the node
195
+ // status is the worst surviving disposition.
174
196
  const pushTypeNode = (
175
197
  typeName: string,
176
198
  contractPath: string,
177
199
  typeIssues: readonly SchemaIssue[],
200
+ controlPolicy: ControlPolicy,
178
201
  ): void => {
179
- if (typeIssues.length > 0) {
180
- issues.push(...typeIssues);
202
+ let status: VerificationStatus = 'pass';
203
+ let code = '';
204
+ let emitted = 0;
205
+ for (const issue of typeIssues) {
206
+ const disposition = verifierDisposition(controlPolicy, issue.kind);
207
+ if (disposition === 'suppress') continue;
208
+ issues.push(issue);
209
+ emitted += 1;
210
+ if (code === '') code = issue.kind;
211
+ if (disposition === 'fail') {
212
+ status = 'fail';
213
+ } else if (disposition === 'warn' && status !== 'fail') {
214
+ status = 'warn';
215
+ }
181
216
  }
182
217
  typeNodes.push({
183
- status: typeIssues.length > 0 ? 'fail' : 'pass',
218
+ status,
184
219
  kind: 'storageType',
185
220
  name: `type ${typeName}`,
186
221
  contractPath,
187
- code: typeIssues.length > 0 ? (typeIssues[0]?.kind ?? '') : '',
188
- message:
189
- typeIssues.length > 0
190
- ? `${typeIssues.length} issue${typeIssues.length === 1 ? '' : 's'}`
191
- : '',
222
+ code: status === 'pass' ? '' : code,
223
+ message: emitted > 0 ? `${emitted} issue${emitted === 1 ? '' : 's'}` : '',
192
224
  expected: undefined,
193
225
  actual: undefined,
194
226
  children: [],
195
227
  });
196
228
  };
197
229
 
198
- // Top-level `storage.types`: codec-typed entries via codec hooks; a
199
- // defensive top-level enum is verified under the unbound coordinate.
230
+ // Top-level `storage.types`: codec-typed entries via codec hooks.
200
231
  for (const [typeName, typeInstance] of Object.entries(contract.storage.types ?? {})) {
201
- if (isPostgresEnumStorageEntry(typeInstance)) {
202
- pushTypeNode(
203
- typeName,
204
- `storage.types.${typeName}`,
205
- verifyEnumType({
206
- typeName,
207
- typeInstance,
208
- schema,
209
- resolveExistingEnumValues,
210
- namespaceId: UNBOUND_NAMESPACE_ID,
211
- }),
212
- );
213
- } else if (isStorageTypeInstance(typeInstance)) {
232
+ if (isStorageTypeInstance(typeInstance)) {
214
233
  const hook = codecHooks.get(typeInstance.codecId);
215
234
  pushTypeNode(
216
235
  typeName,
217
236
  `storage.types.${typeName}`,
218
237
  hook?.verifyType ? hook.verifyType({ typeName, typeInstance, schema }) : [],
238
+ effectiveControlPolicy(undefined, contract.defaultControlPolicy),
219
239
  );
220
240
  }
221
241
  }
@@ -224,13 +244,13 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
224
244
  for (const nsId of Object.keys(contract.storage.namespaces)) {
225
245
  const ns = contract.storage.namespaces[nsId];
226
246
  if (!ns) continue;
227
- const nsEnums = ns.enum;
247
+ const nsEnums = ns.entries['type'];
228
248
  if (!nsEnums) continue;
229
249
  for (const [typeName, entry] of Object.entries(nsEnums)) {
230
250
  if (!isPostgresEnumStorageEntry(entry)) continue;
231
251
  pushTypeNode(
232
252
  typeName,
233
- `storage.namespaces.${nsId}.enum.${typeName}`,
253
+ `storage.namespaces.${nsId}.entries.type.${typeName}`,
234
254
  verifyEnumType({
235
255
  typeName,
236
256
  typeInstance: entry,
@@ -238,12 +258,17 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
238
258
  resolveExistingEnumValues,
239
259
  namespaceId: nsId,
240
260
  }),
261
+ effectiveControlPolicy(entry.control, contract.defaultControlPolicy),
241
262
  );
242
263
  }
243
264
  }
244
265
 
245
266
  if (typeNodes.length > 0) {
246
- const typesStatus = typeNodes.some((n) => n.status === 'fail') ? 'fail' : 'pass';
267
+ const typesStatus: VerificationStatus = typeNodes.some((n) => n.status === 'fail')
268
+ ? 'fail'
269
+ : typeNodes.some((n) => n.status === 'warn')
270
+ ? 'warn'
271
+ : 'pass';
247
272
  rootChildren.push({
248
273
  status: typesStatus,
249
274
  kind: 'storageTypes',
@@ -303,8 +328,6 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
303
328
  };
304
329
  }
305
330
 
306
- type VerificationStatus = 'pass' | 'warn' | 'fail';
307
-
308
331
  /**
309
332
  * Native verification walk for `PostgresEnumStorageEntry` instances (no codec hook).
310
333
  *
@@ -386,6 +409,7 @@ function verifySchemaTables(options: {
386
409
  storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
387
410
  normalizeDefault?: DefaultNormalizer;
388
411
  normalizeNativeType?: NativeTypeNormalizer;
412
+ columnsCompatible?: ColumnsCompatible;
389
413
  }): { issues: SchemaIssue[]; rootChildren: SchemaVerificationNode[] } {
390
414
  const {
391
415
  contract,
@@ -396,7 +420,9 @@ function verifySchemaTables(options: {
396
420
  storageTypes,
397
421
  normalizeDefault,
398
422
  normalizeNativeType,
423
+ columnsCompatible,
399
424
  } = options;
425
+ const contractDefaultControl = contract.defaultControlPolicy;
400
426
  const issues: SchemaIssue[] = [];
401
427
  const rootChildren: SchemaVerificationNode[] = [];
402
428
  const schemaTables = schema.tables;
@@ -407,34 +433,44 @@ function verifySchemaTables(options: {
407
433
  for (const namespaceId of namespaceIds) {
408
434
  const ns = contract.storage.namespaces[namespaceId];
409
435
  if (!ns) continue;
410
- for (const [tableName, contractTableRaw] of Object.entries(ns.tables)) {
436
+ for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table)) {
411
437
  if (!(contractTableRaw instanceof StorageTable)) {
412
438
  throw new Error(
413
- `verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.tables.${tableName}`,
439
+ `verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.entries.table.${tableName}`,
414
440
  );
415
441
  }
416
442
  const contractTable = contractTableRaw;
443
+ const tableControlPolicy = effectiveControlPolicy(
444
+ contractTable.control,
445
+ contractDefaultControl,
446
+ );
417
447
  const schemaTable = schemaTables[tableName];
418
- const tablePath = `storage.namespaces.${namespaceId}.tables.${tableName}`;
448
+ const tablePath = `storage.namespaces.${namespaceId}.entries.table.${tableName}`;
419
449
 
420
450
  if (!schemaTable) {
421
- issues.push({
451
+ const issue: SchemaIssue = {
422
452
  kind: 'missing_table',
423
453
  table: tableName,
424
454
  namespaceId,
425
455
  message: `Table "${tableName}" is missing from database`,
426
- });
427
- rootChildren.push({
428
- status: 'fail',
429
- kind: 'table',
430
- name: `table ${tableName}`,
431
- contractPath: tablePath,
432
- code: 'missing_table',
433
- message: `Table "${tableName}" is missing`,
434
- expected: undefined,
435
- actual: undefined,
436
- children: [],
437
- });
456
+ };
457
+ emitIssueAndNodeUnderControlPolicy(
458
+ tableControlPolicy,
459
+ issue,
460
+ {
461
+ status: 'fail',
462
+ kind: 'table',
463
+ name: `table ${tableName}`,
464
+ contractPath: tablePath,
465
+ code: 'missing_table',
466
+ message: `Table "${tableName}" is missing`,
467
+ expected: undefined,
468
+ actual: undefined,
469
+ children: [],
470
+ },
471
+ issues,
472
+ rootChildren,
473
+ );
438
474
  continue;
439
475
  }
440
476
 
@@ -444,6 +480,7 @@ function verifySchemaTables(options: {
444
480
  tableName,
445
481
  namespaceId,
446
482
  tablePath,
483
+ tableControlPolicy,
447
484
  issues,
448
485
  strict,
449
486
  typeMetadataRegistry,
@@ -451,6 +488,7 @@ function verifySchemaTables(options: {
451
488
  storageTypes,
452
489
  ...ifDefined('normalizeDefault', normalizeDefault),
453
490
  ...ifDefined('normalizeNativeType', normalizeNativeType),
491
+ ...ifDefined('columnsCompatible', columnsCompatible),
454
492
  });
455
493
  rootChildren.push(buildTableNode(tableName, tablePath, tableChildren));
456
494
  }
@@ -459,29 +497,33 @@ function verifySchemaTables(options: {
459
497
  if (strict) {
460
498
  for (const tableName of Object.keys(schemaTables)) {
461
499
  const claimed = namespaceIds.some(
462
- (namespaceId) => contract.storage.namespaces[namespaceId]?.tables[tableName] !== undefined,
500
+ (namespaceId) =>
501
+ contract.storage.namespaces[namespaceId]?.entries.table[tableName] !== undefined,
463
502
  );
464
503
  if (!claimed) {
465
- // `namespaceId` is intentionally absent: an extra table exists in the
466
- // live database but is not claimed by any contract namespace, so there
467
- // is no contract coordinate to stamp here. Planners that consume this
468
- // issue must handle the unstamped case (drop / quarantine by name).
469
- issues.push({
504
+ const extraTableControlPolicy = effectiveControlPolicy(undefined, contractDefaultControl);
505
+ const issue: SchemaIssue = {
470
506
  kind: 'extra_table',
471
507
  table: tableName,
472
508
  message: `Extra table "${tableName}" found in database (not in contract)`,
473
- });
474
- rootChildren.push({
475
- status: 'fail',
476
- kind: 'table',
477
- name: `table ${tableName}`,
478
- contractPath: `storage.namespaces.*.tables.${tableName}`,
479
- code: 'extra_table',
480
- message: `Extra table "${tableName}" found`,
481
- expected: undefined,
482
- actual: undefined,
483
- children: [],
484
- });
509
+ };
510
+ emitIssueAndNodeUnderControlPolicy(
511
+ extraTableControlPolicy,
512
+ issue,
513
+ {
514
+ status: 'fail',
515
+ kind: 'table',
516
+ name: `table ${tableName}`,
517
+ contractPath: `storage.namespaces.*.entries.table.${tableName}`,
518
+ code: 'extra_table',
519
+ message: `Extra table "${tableName}" found`,
520
+ expected: undefined,
521
+ actual: undefined,
522
+ children: [],
523
+ },
524
+ issues,
525
+ rootChildren,
526
+ );
485
527
  }
486
528
  }
487
529
  }
@@ -495,6 +537,7 @@ function verifyTableChildren(options: {
495
537
  tableName: string;
496
538
  namespaceId: string;
497
539
  tablePath: string;
540
+ tableControlPolicy: ControlPolicy;
498
541
  issues: SchemaIssue[];
499
542
  strict: boolean;
500
543
  typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
@@ -502,6 +545,7 @@ function verifyTableChildren(options: {
502
545
  storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
503
546
  normalizeDefault?: DefaultNormalizer;
504
547
  normalizeNativeType?: NativeTypeNormalizer;
548
+ columnsCompatible?: ColumnsCompatible;
505
549
  }): SchemaVerificationNode[] {
506
550
  const {
507
551
  contractTable,
@@ -509,6 +553,7 @@ function verifyTableChildren(options: {
509
553
  tableName,
510
554
  namespaceId,
511
555
  tablePath,
556
+ tableControlPolicy,
512
557
  issues,
513
558
  strict,
514
559
  typeMetadataRegistry,
@@ -516,6 +561,7 @@ function verifyTableChildren(options: {
516
561
  storageTypes,
517
562
  normalizeDefault,
518
563
  normalizeNativeType,
564
+ columnsCompatible,
519
565
  } = options;
520
566
  const tableChildren: SchemaVerificationNode[] = [];
521
567
  const columnNodes = collectContractColumnNodes({
@@ -524,6 +570,7 @@ function verifyTableChildren(options: {
524
570
  tableName,
525
571
  namespaceId,
526
572
  tablePath,
573
+ tableControlPolicy,
527
574
  issues,
528
575
  strict,
529
576
  typeMetadataRegistry,
@@ -531,6 +578,7 @@ function verifyTableChildren(options: {
531
578
  storageTypes,
532
579
  ...ifDefined('normalizeDefault', normalizeDefault),
533
580
  ...ifDefined('normalizeNativeType', normalizeNativeType),
581
+ ...ifDefined('columnsCompatible', columnsCompatible),
534
582
  });
535
583
  if (columnNodes.length > 0) {
536
584
  tableChildren.push(buildColumnsNode(tablePath, columnNodes));
@@ -542,6 +590,7 @@ function verifyTableChildren(options: {
542
590
  tableName,
543
591
  namespaceId,
544
592
  tablePath,
593
+ tableControlPolicy,
545
594
  issues,
546
595
  columnNodes,
547
596
  });
@@ -553,6 +602,7 @@ function verifyTableChildren(options: {
553
602
  schemaTable.primaryKey,
554
603
  tableName,
555
604
  namespaceId,
605
+ tableControlPolicy,
556
606
  issues,
557
607
  );
558
608
  if (pkStatus === 'fail') {
@@ -567,6 +617,18 @@ function verifyTableChildren(options: {
567
617
  actual: schemaTable.primaryKey,
568
618
  children: [],
569
619
  });
620
+ } else if (pkStatus === 'warn') {
621
+ tableChildren.push({
622
+ status: 'warn',
623
+ kind: 'primaryKey',
624
+ name: `primary key: ${contractTable.primaryKey.columns.join(', ')}`,
625
+ contractPath: `${tablePath}.primaryKey`,
626
+ code: 'primary_key_mismatch',
627
+ message: 'Primary key mismatch',
628
+ expected: contractTable.primaryKey,
629
+ actual: schemaTable.primaryKey,
630
+ children: [],
631
+ });
570
632
  } else {
571
633
  tableChildren.push({
572
634
  status: 'pass',
@@ -581,23 +643,29 @@ function verifyTableChildren(options: {
581
643
  });
582
644
  }
583
645
  } else if (schemaTable.primaryKey && strict) {
584
- issues.push({
646
+ const issue: SchemaIssue = {
585
647
  kind: 'extra_primary_key',
586
648
  table: tableName,
587
649
  namespaceId,
588
650
  message: 'Extra primary key found in database (not in contract)',
589
- });
590
- tableChildren.push({
591
- status: 'fail',
592
- kind: 'primaryKey',
593
- name: `primary key: ${schemaTable.primaryKey.columns.join(', ')}`,
594
- contractPath: `${tablePath}.primaryKey`,
595
- code: 'extra_primary_key',
596
- message: 'Extra primary key found',
597
- expected: undefined,
598
- actual: schemaTable.primaryKey,
599
- children: [],
600
- });
651
+ };
652
+ emitIssueAndNodeUnderControlPolicy(
653
+ tableControlPolicy,
654
+ issue,
655
+ {
656
+ status: 'fail',
657
+ kind: 'primaryKey',
658
+ name: `primary key: ${schemaTable.primaryKey.columns.join(', ')}`,
659
+ contractPath: `${tablePath}.primaryKey`,
660
+ code: 'extra_primary_key',
661
+ message: 'Extra primary key found',
662
+ expected: undefined,
663
+ actual: schemaTable.primaryKey,
664
+ children: [],
665
+ },
666
+ issues,
667
+ tableChildren,
668
+ );
601
669
  }
602
670
 
603
671
  // Verify FK constraints only for FKs with constraint: true.
@@ -611,6 +679,7 @@ function verifyTableChildren(options: {
611
679
  tableName,
612
680
  namespaceId,
613
681
  tablePath,
682
+ tableControlPolicy,
614
683
  issues,
615
684
  strict,
616
685
  );
@@ -624,6 +693,7 @@ function verifyTableChildren(options: {
624
693
  tableName,
625
694
  namespaceId,
626
695
  tablePath,
696
+ tableControlPolicy,
627
697
  issues,
628
698
  strict,
629
699
  );
@@ -648,6 +718,7 @@ function verifyTableChildren(options: {
648
718
  tableName,
649
719
  namespaceId,
650
720
  tablePath,
721
+ tableControlPolicy,
651
722
  issues,
652
723
  strict,
653
724
  );
@@ -662,6 +733,7 @@ function collectContractColumnNodes(options: {
662
733
  tableName: string;
663
734
  namespaceId: string;
664
735
  tablePath: string;
736
+ tableControlPolicy: ControlPolicy;
665
737
  issues: SchemaIssue[];
666
738
  strict: boolean;
667
739
  typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
@@ -669,6 +741,7 @@ function collectContractColumnNodes(options: {
669
741
  storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
670
742
  normalizeDefault?: DefaultNormalizer;
671
743
  normalizeNativeType?: NativeTypeNormalizer;
744
+ columnsCompatible?: ColumnsCompatible;
672
745
  }): SchemaVerificationNode[] {
673
746
  const {
674
747
  contractTable,
@@ -676,6 +749,7 @@ function collectContractColumnNodes(options: {
676
749
  tableName,
677
750
  namespaceId,
678
751
  tablePath,
752
+ tableControlPolicy,
679
753
  issues,
680
754
  strict,
681
755
  typeMetadataRegistry,
@@ -683,6 +757,7 @@ function collectContractColumnNodes(options: {
683
757
  storageTypes,
684
758
  normalizeDefault,
685
759
  normalizeNativeType,
760
+ columnsCompatible,
686
761
  } = options;
687
762
  const columnNodes: SchemaVerificationNode[] = [];
688
763
 
@@ -691,24 +766,30 @@ function collectContractColumnNodes(options: {
691
766
  const columnPath = `${tablePath}.columns.${columnName}`;
692
767
 
693
768
  if (!schemaColumn) {
694
- issues.push({
769
+ const issue: SchemaIssue = {
695
770
  kind: 'missing_column',
696
771
  table: tableName,
697
772
  namespaceId,
698
773
  column: columnName,
699
774
  message: `Column "${tableName}"."${columnName}" is missing from database`,
700
- });
701
- columnNodes.push({
702
- status: 'fail',
703
- kind: 'column',
704
- name: `${columnName}: missing`,
705
- contractPath: columnPath,
706
- code: 'missing_column',
707
- message: `Column "${columnName}" is missing`,
708
- expected: undefined,
709
- actual: undefined,
710
- children: [],
711
- });
775
+ };
776
+ emitIssueAndNodeUnderControlPolicy(
777
+ tableControlPolicy,
778
+ issue,
779
+ {
780
+ status: 'fail',
781
+ kind: 'column',
782
+ name: `${columnName}: missing`,
783
+ contractPath: columnPath,
784
+ code: 'missing_column',
785
+ message: `Column "${columnName}" is missing`,
786
+ expected: undefined,
787
+ actual: undefined,
788
+ children: [],
789
+ },
790
+ issues,
791
+ columnNodes,
792
+ );
712
793
  continue;
713
794
  }
714
795
 
@@ -720,6 +801,7 @@ function collectContractColumnNodes(options: {
720
801
  contractColumn,
721
802
  schemaColumn,
722
803
  columnPath,
804
+ tableControlPolicy,
723
805
  issues,
724
806
  strict,
725
807
  typeMetadataRegistry,
@@ -727,6 +809,7 @@ function collectContractColumnNodes(options: {
727
809
  storageTypes,
728
810
  ...ifDefined('normalizeDefault', normalizeDefault),
729
811
  ...ifDefined('normalizeNativeType', normalizeNativeType),
812
+ ...ifDefined('columnsCompatible', columnsCompatible),
730
813
  }),
731
814
  );
732
815
  }
@@ -740,31 +823,46 @@ function appendExtraColumnNodes(options: {
740
823
  tableName: string;
741
824
  namespaceId: string;
742
825
  tablePath: string;
826
+ tableControlPolicy: ControlPolicy;
743
827
  issues: SchemaIssue[];
744
828
  columnNodes: SchemaVerificationNode[];
745
829
  }): void {
746
- const { contractTable, schemaTable, tableName, namespaceId, tablePath, issues, columnNodes } =
747
- options;
830
+ const {
831
+ contractTable,
832
+ schemaTable,
833
+ tableName,
834
+ namespaceId,
835
+ tablePath,
836
+ tableControlPolicy,
837
+ issues,
838
+ columnNodes,
839
+ } = options;
748
840
  for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) {
749
841
  if (!contractTable.columns[columnName]) {
750
- issues.push({
842
+ const issue: SchemaIssue = {
751
843
  kind: 'extra_column',
752
844
  table: tableName,
753
845
  namespaceId,
754
846
  column: columnName,
755
847
  message: `Extra column "${tableName}"."${columnName}" found in database (not in contract)`,
756
- });
757
- columnNodes.push({
758
- status: 'fail',
759
- kind: 'column',
760
- name: `${columnName}: extra`,
761
- contractPath: `${tablePath}.columns.${columnName}`,
762
- code: 'extra_column',
763
- message: `Extra column "${columnName}" found`,
764
- expected: undefined,
765
- actual: nativeType,
766
- children: [],
767
- });
848
+ };
849
+ emitIssueAndNodeUnderControlPolicy(
850
+ tableControlPolicy,
851
+ issue,
852
+ {
853
+ status: 'fail',
854
+ kind: 'column',
855
+ name: `${columnName}: extra`,
856
+ contractPath: `${tablePath}.columns.${columnName}`,
857
+ code: 'extra_column',
858
+ message: `Extra column "${columnName}" found`,
859
+ expected: undefined,
860
+ actual: nativeType,
861
+ children: [],
862
+ },
863
+ issues,
864
+ columnNodes,
865
+ );
768
866
  }
769
867
  }
770
868
  }
@@ -776,6 +874,7 @@ function verifyColumn(options: {
776
874
  contractColumn: StorageTable['columns'][string];
777
875
  schemaColumn: SqlSchemaIR['tables'][string]['columns'][string];
778
876
  columnPath: string;
877
+ tableControlPolicy: ControlPolicy;
779
878
  issues: SchemaIssue[];
780
879
  strict: boolean;
781
880
  typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
@@ -783,6 +882,7 @@ function verifyColumn(options: {
783
882
  storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
784
883
  normalizeDefault?: DefaultNormalizer;
785
884
  normalizeNativeType?: NativeTypeNormalizer;
885
+ columnsCompatible?: ColumnsCompatible;
786
886
  }): SchemaVerificationNode {
787
887
  const {
788
888
  tableName,
@@ -791,12 +891,14 @@ function verifyColumn(options: {
791
891
  contractColumn,
792
892
  schemaColumn,
793
893
  columnPath,
894
+ tableControlPolicy,
794
895
  issues,
795
896
  strict,
796
897
  codecHooks,
797
898
  storageTypes,
798
899
  normalizeDefault,
799
900
  normalizeNativeType,
901
+ columnsCompatible,
800
902
  } = options;
801
903
  const columnChildren: SchemaVerificationNode[] = [];
802
904
  let columnStatus: VerificationStatus = 'pass';
@@ -812,8 +914,14 @@ function verifyColumn(options: {
812
914
  const schemaNativeType =
813
915
  normalizeNativeType?.(schemaColumn.nativeType) ?? schemaColumn.nativeType;
814
916
 
815
- if (contractNativeType !== schemaNativeType) {
816
- issues.push({
917
+ const typesMatch =
918
+ contractNativeType === schemaNativeType ||
919
+ (tableControlPolicy === 'external' &&
920
+ (columnsCompatible?.(contractNativeType, schemaNativeType) ??
921
+ contractNativeType === schemaNativeType));
922
+
923
+ if (!typesMatch) {
924
+ const issue: SchemaIssue = {
817
925
  kind: 'type_mismatch',
818
926
  table: tableName,
819
927
  namespaceId,
@@ -821,19 +929,23 @@ function verifyColumn(options: {
821
929
  expected: contractNativeType,
822
930
  actual: schemaNativeType,
823
931
  message: `Column "${tableName}"."${columnName}" has type mismatch: expected "${contractNativeType}", got "${schemaNativeType}"`,
824
- });
825
- columnChildren.push({
826
- status: 'fail',
827
- kind: 'type',
828
- name: 'type',
829
- contractPath: `${columnPath}.nativeType`,
830
- code: 'type_mismatch',
831
- message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
832
- expected: contractNativeType,
833
- actual: schemaNativeType,
834
- children: [],
835
- });
836
- columnStatus = 'fail';
932
+ };
933
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
934
+ if (disposition !== 'suppress') {
935
+ issues.push(issue);
936
+ columnChildren.push({
937
+ status: disposition,
938
+ kind: 'type',
939
+ name: 'type',
940
+ contractPath: `${columnPath}.nativeType`,
941
+ code: 'type_mismatch',
942
+ message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
943
+ expected: contractNativeType,
944
+ actual: schemaNativeType,
945
+ children: [],
946
+ });
947
+ columnStatus = disposition;
948
+ }
837
949
  }
838
950
 
839
951
  if (resolvedContractColumn.codecId) {
@@ -869,7 +981,7 @@ function verifyColumn(options: {
869
981
  }
870
982
 
871
983
  if (contractColumn.nullable !== schemaColumn.nullable) {
872
- issues.push({
984
+ const issue: SchemaIssue = {
873
985
  kind: 'nullability_mismatch',
874
986
  table: tableName,
875
987
  namespaceId,
@@ -877,44 +989,52 @@ function verifyColumn(options: {
877
989
  expected: String(contractColumn.nullable),
878
990
  actual: String(schemaColumn.nullable),
879
991
  message: `Column "${tableName}"."${columnName}" has nullability mismatch: expected ${contractColumn.nullable ? 'nullable' : 'not null'}, got ${schemaColumn.nullable ? 'nullable' : 'not null'}`,
880
- });
881
- columnChildren.push({
882
- status: 'fail',
883
- kind: 'nullability',
884
- name: 'nullability',
885
- contractPath: `${columnPath}.nullable`,
886
- code: 'nullability_mismatch',
887
- message: `Nullability mismatch: expected ${contractColumn.nullable ? 'nullable' : 'not null'}, got ${schemaColumn.nullable ? 'nullable' : 'not null'}`,
888
- expected: contractColumn.nullable,
889
- actual: schemaColumn.nullable,
890
- children: [],
891
- });
892
- columnStatus = 'fail';
992
+ };
993
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
994
+ if (disposition !== 'suppress') {
995
+ issues.push(issue);
996
+ columnChildren.push({
997
+ status: disposition,
998
+ kind: 'nullability',
999
+ name: 'nullability',
1000
+ contractPath: `${columnPath}.nullable`,
1001
+ code: 'nullability_mismatch',
1002
+ message: `Nullability mismatch: expected ${contractColumn.nullable ? 'nullable' : 'not null'}, got ${schemaColumn.nullable ? 'nullable' : 'not null'}`,
1003
+ expected: contractColumn.nullable,
1004
+ actual: schemaColumn.nullable,
1005
+ children: [],
1006
+ });
1007
+ columnStatus = disposition;
1008
+ }
893
1009
  }
894
1010
 
895
1011
  if (contractColumn.default) {
896
1012
  if (!schemaColumn.default) {
897
1013
  const defaultDescription = describeColumnDefault(contractColumn.default);
898
- issues.push({
1014
+ const issue: SchemaIssue = {
899
1015
  kind: 'default_missing',
900
1016
  table: tableName,
901
1017
  namespaceId,
902
1018
  column: columnName,
903
1019
  expected: defaultDescription,
904
1020
  message: `Column "${tableName}"."${columnName}" should have default ${defaultDescription} but database has no default`,
905
- });
906
- columnChildren.push({
907
- status: 'fail',
908
- kind: 'default',
909
- name: 'default',
910
- contractPath: `${columnPath}.default`,
911
- code: 'default_missing',
912
- message: `Default missing: expected ${defaultDescription}`,
913
- expected: defaultDescription,
914
- actual: undefined,
915
- children: [],
916
- });
917
- columnStatus = 'fail';
1021
+ };
1022
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1023
+ if (disposition !== 'suppress') {
1024
+ issues.push(issue);
1025
+ columnChildren.push({
1026
+ status: disposition,
1027
+ kind: 'default',
1028
+ name: 'default',
1029
+ contractPath: `${columnPath}.default`,
1030
+ code: 'default_missing',
1031
+ message: `Default missing: expected ${defaultDescription}`,
1032
+ expected: defaultDescription,
1033
+ actual: undefined,
1034
+ children: [],
1035
+ });
1036
+ columnStatus = disposition;
1037
+ }
918
1038
  } else if (
919
1039
  !columnDefaultsEqual(
920
1040
  contractColumn.default,
@@ -924,9 +1044,8 @@ function verifyColumn(options: {
924
1044
  )
925
1045
  ) {
926
1046
  const expectedDescription = describeColumnDefault(contractColumn.default);
927
- // schemaColumn.default is now a raw string, describe it as-is
928
1047
  const actualDescription = schemaColumn.default;
929
- issues.push({
1048
+ const issue: SchemaIssue = {
930
1049
  kind: 'default_mismatch',
931
1050
  table: tableName,
932
1051
  namespaceId,
@@ -934,41 +1053,49 @@ function verifyColumn(options: {
934
1053
  expected: expectedDescription,
935
1054
  actual: actualDescription,
936
1055
  message: `Column "${tableName}"."${columnName}" has default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
937
- });
938
- columnChildren.push({
939
- status: 'fail',
940
- kind: 'default',
941
- name: 'default',
942
- contractPath: `${columnPath}.default`,
943
- code: 'default_mismatch',
944
- message: `Default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
945
- expected: expectedDescription,
946
- actual: actualDescription,
947
- children: [],
948
- });
949
- columnStatus = 'fail';
1056
+ };
1057
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1058
+ if (disposition !== 'suppress') {
1059
+ issues.push(issue);
1060
+ columnChildren.push({
1061
+ status: disposition,
1062
+ kind: 'default',
1063
+ name: 'default',
1064
+ contractPath: `${columnPath}.default`,
1065
+ code: 'default_mismatch',
1066
+ message: `Default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
1067
+ expected: expectedDescription,
1068
+ actual: actualDescription,
1069
+ children: [],
1070
+ });
1071
+ columnStatus = disposition;
1072
+ }
950
1073
  }
951
1074
  } else if (strict && schemaColumn.default) {
952
- issues.push({
1075
+ const issue: SchemaIssue = {
953
1076
  kind: 'extra_default',
954
1077
  table: tableName,
955
1078
  namespaceId,
956
1079
  column: columnName,
957
1080
  actual: schemaColumn.default,
958
1081
  message: `Column "${tableName}"."${columnName}" has default ${schemaColumn.default} in database but contract specifies no default`,
959
- });
960
- columnChildren.push({
961
- status: 'fail',
962
- kind: 'default',
963
- name: 'default',
964
- contractPath: `${columnPath}.default`,
965
- code: 'extra_default',
966
- message: `Extra default: ${schemaColumn.default}`,
967
- expected: undefined,
968
- actual: schemaColumn.default,
969
- children: [],
970
- });
971
- columnStatus = 'fail';
1082
+ };
1083
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1084
+ if (disposition !== 'suppress') {
1085
+ issues.push(issue);
1086
+ columnChildren.push({
1087
+ status: disposition,
1088
+ kind: 'default',
1089
+ name: 'default',
1090
+ contractPath: `${columnPath}.default`,
1091
+ code: 'extra_default',
1092
+ message: `Extra default: ${schemaColumn.default}`,
1093
+ expected: undefined,
1094
+ actual: schemaColumn.default,
1095
+ children: [],
1096
+ });
1097
+ columnStatus = disposition;
1098
+ }
972
1099
  }
973
1100
 
974
1101
  // Single-pass aggregation for better performance