@prisma-next/family-sql 0.12.0-dev.6 → 0.12.0-dev.60

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 (55) hide show
  1. package/dist/{authoring-type-constructors-F4JpCJl7.mjs → authoring-type-constructors-D4lQ-qpj.mjs} +1 -1
  2. package/dist/{authoring-type-constructors-F4JpCJl7.mjs.map → authoring-type-constructors-D4lQ-qpj.mjs.map} +1 -1
  3. package/dist/control-adapter-76pzcIFV.d.mts +172 -0
  4. package/dist/control-adapter-76pzcIFV.d.mts.map +1 -0
  5. package/dist/control-adapter.d.mts +2 -109
  6. package/dist/control.d.mts +118 -4
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +305 -44
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/ir.d.mts +4 -5
  11. package/dist/ir.d.mts.map +1 -1
  12. package/dist/ir.mjs +1 -1
  13. package/dist/migration.d.mts +1 -1
  14. package/dist/migration.d.mts.map +1 -1
  15. package/dist/pack.mjs +1 -1
  16. package/dist/runtime.d.mts +4 -2
  17. package/dist/runtime.d.mts.map +1 -1
  18. package/dist/runtime.mjs +4 -2
  19. package/dist/runtime.mjs.map +1 -1
  20. package/dist/schema-verify.d.mts +2 -1
  21. package/dist/schema-verify.d.mts.map +1 -1
  22. package/dist/schema-verify.mjs +1 -1
  23. package/dist/{sql-contract-serializer-8axtK4lg.mjs → sql-contract-serializer-CY7qnms7.mjs} +18 -36
  24. package/dist/sql-contract-serializer-CY7qnms7.mjs.map +1 -0
  25. package/dist/{timestamp-now-generator-BkjCQIde.mjs → timestamp-now-generator-CloimujU.mjs} +1 -1
  26. package/dist/{timestamp-now-generator-BkjCQIde.mjs.map → timestamp-now-generator-CloimujU.mjs.map} +1 -1
  27. package/dist/{types-CeeCStqw.d.mts → types-CRpx_fk5.d.mts} +69 -15
  28. package/dist/types-CRpx_fk5.d.mts.map +1 -0
  29. package/dist/{verify-Crewz6hG.mjs → verify-C-G0obRm.mjs} +1 -1
  30. package/dist/{verify-Crewz6hG.mjs.map → verify-C-G0obRm.mjs.map} +1 -1
  31. package/dist/{verify-sql-schema-CN7pPoTC.d.mts → verify-sql-schema-DcMaT5Zj.d.mts} +1 -1
  32. package/dist/{verify-sql-schema-CN7pPoTC.d.mts.map → verify-sql-schema-DcMaT5Zj.d.mts.map} +1 -1
  33. package/dist/{verify-sql-schema-CYLsGCFO.mjs → verify-sql-schema-d3P9NA_8.mjs} +400 -317
  34. package/dist/verify-sql-schema-d3P9NA_8.mjs.map +1 -0
  35. package/dist/verify.mjs +1 -1
  36. package/package.json +23 -23
  37. package/src/core/control-adapter.ts +105 -7
  38. package/src/core/control-instance.ts +260 -66
  39. package/src/core/default-namespace.ts +9 -0
  40. package/src/core/ir/sql-contract-serializer-base.ts +72 -56
  41. package/src/core/migrations/contract-to-schema-ir.ts +13 -9
  42. package/src/core/migrations/control-policy.ts +322 -0
  43. package/src/core/migrations/field-event-planner.ts +2 -2
  44. package/src/core/migrations/plan-helpers.ts +16 -0
  45. package/src/core/migrations/types.ts +16 -6
  46. package/src/core/schema-verify/control-verify-emit.ts +46 -0
  47. package/src/core/schema-verify/verifier-disposition.ts +53 -0
  48. package/src/core/schema-verify/verify-helpers.ts +151 -110
  49. package/src/core/schema-verify/verify-sql-schema.ts +281 -178
  50. package/src/exports/control.ts +6 -0
  51. package/src/exports/runtime.ts +7 -0
  52. package/dist/control-adapter.d.mts.map +0 -1
  53. package/dist/sql-contract-serializer-8axtK4lg.mjs.map +0 -1
  54. package/dist/types-CeeCStqw.d.mts.map +0 -1
  55. 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,
@@ -140,7 +145,10 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
140
145
  >),
141
146
  };
142
147
  for (const ns of Object.values(contract.storage.namespaces)) {
143
- const nsEnums = (ns as { enum?: Record<string, PostgresEnumStorageEntry> }).enum;
148
+ const nsEnums = blindCast<
149
+ { readonly type?: Readonly<Record<string, PostgresEnumStorageEntry | StorageTypeInstance>> },
150
+ 'postgres target namespace entries carry a type slot beyond the family-shared SqlNamespace.entries type'
151
+ >(ns.entries).type;
144
152
  if (nsEnums) {
145
153
  for (const [k, v] of Object.entries(nsEnums)) {
146
154
  allStorageTypesMap[k] = v;
@@ -171,51 +179,54 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
171
179
  // namespace coordinate — a bare-name aggregation would collapse them
172
180
  // (last-write-wins) and verify only one.
173
181
  const typeNodes: SchemaVerificationNode[] = [];
182
+ // Storage-type findings dispatch through the same control policy as tables
183
+ // and columns: each issue's disposition (fail / warn / suppress) is resolved
184
+ // from the type's effective control so an `external`/`observed` enum no longer
185
+ // hard-fails on value drift. `suppress` drops the issue entirely; the node
186
+ // status is the worst surviving disposition.
174
187
  const pushTypeNode = (
175
188
  typeName: string,
176
189
  contractPath: string,
177
190
  typeIssues: readonly SchemaIssue[],
191
+ controlPolicy: ControlPolicy,
178
192
  ): void => {
179
- if (typeIssues.length > 0) {
180
- issues.push(...typeIssues);
193
+ let status: VerificationStatus = 'pass';
194
+ let code = '';
195
+ let emitted = 0;
196
+ for (const issue of typeIssues) {
197
+ const disposition = verifierDisposition(controlPolicy, issue.kind);
198
+ if (disposition === 'suppress') continue;
199
+ issues.push(issue);
200
+ emitted += 1;
201
+ if (code === '') code = issue.kind;
202
+ if (disposition === 'fail') {
203
+ status = 'fail';
204
+ } else if (disposition === 'warn' && status !== 'fail') {
205
+ status = 'warn';
206
+ }
181
207
  }
182
208
  typeNodes.push({
183
- status: typeIssues.length > 0 ? 'fail' : 'pass',
209
+ status,
184
210
  kind: 'storageType',
185
211
  name: `type ${typeName}`,
186
212
  contractPath,
187
- code: typeIssues.length > 0 ? (typeIssues[0]?.kind ?? '') : '',
188
- message:
189
- typeIssues.length > 0
190
- ? `${typeIssues.length} issue${typeIssues.length === 1 ? '' : 's'}`
191
- : '',
213
+ code: status === 'pass' ? '' : code,
214
+ message: emitted > 0 ? `${emitted} issue${emitted === 1 ? '' : 's'}` : '',
192
215
  expected: undefined,
193
216
  actual: undefined,
194
217
  children: [],
195
218
  });
196
219
  };
197
220
 
198
- // Top-level `storage.types`: codec-typed entries via codec hooks; a
199
- // defensive top-level enum is verified under the unbound coordinate.
221
+ // Top-level `storage.types`: codec-typed entries via codec hooks.
200
222
  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)) {
223
+ if (isStorageTypeInstance(typeInstance)) {
214
224
  const hook = codecHooks.get(typeInstance.codecId);
215
225
  pushTypeNode(
216
226
  typeName,
217
227
  `storage.types.${typeName}`,
218
228
  hook?.verifyType ? hook.verifyType({ typeName, typeInstance, schema }) : [],
229
+ effectiveControlPolicy(undefined, contract.defaultControlPolicy),
219
230
  );
220
231
  }
221
232
  }
@@ -224,13 +235,13 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
224
235
  for (const nsId of Object.keys(contract.storage.namespaces)) {
225
236
  const ns = contract.storage.namespaces[nsId];
226
237
  if (!ns) continue;
227
- const nsEnums = ns.enum;
238
+ const nsEnums = ns.entries['type'];
228
239
  if (!nsEnums) continue;
229
240
  for (const [typeName, entry] of Object.entries(nsEnums)) {
230
241
  if (!isPostgresEnumStorageEntry(entry)) continue;
231
242
  pushTypeNode(
232
243
  typeName,
233
- `storage.namespaces.${nsId}.enum.${typeName}`,
244
+ `storage.namespaces.${nsId}.entries.type.${typeName}`,
234
245
  verifyEnumType({
235
246
  typeName,
236
247
  typeInstance: entry,
@@ -238,12 +249,17 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
238
249
  resolveExistingEnumValues,
239
250
  namespaceId: nsId,
240
251
  }),
252
+ effectiveControlPolicy(entry.control, contract.defaultControlPolicy),
241
253
  );
242
254
  }
243
255
  }
244
256
 
245
257
  if (typeNodes.length > 0) {
246
- const typesStatus = typeNodes.some((n) => n.status === 'fail') ? 'fail' : 'pass';
258
+ const typesStatus: VerificationStatus = typeNodes.some((n) => n.status === 'fail')
259
+ ? 'fail'
260
+ : typeNodes.some((n) => n.status === 'warn')
261
+ ? 'warn'
262
+ : 'pass';
247
263
  rootChildren.push({
248
264
  status: typesStatus,
249
265
  kind: 'storageTypes',
@@ -303,8 +319,6 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
303
319
  };
304
320
  }
305
321
 
306
- type VerificationStatus = 'pass' | 'warn' | 'fail';
307
-
308
322
  /**
309
323
  * Native verification walk for `PostgresEnumStorageEntry` instances (no codec hook).
310
324
  *
@@ -397,6 +411,7 @@ function verifySchemaTables(options: {
397
411
  normalizeDefault,
398
412
  normalizeNativeType,
399
413
  } = options;
414
+ const contractDefaultControl = contract.defaultControlPolicy;
400
415
  const issues: SchemaIssue[] = [];
401
416
  const rootChildren: SchemaVerificationNode[] = [];
402
417
  const schemaTables = schema.tables;
@@ -407,34 +422,44 @@ function verifySchemaTables(options: {
407
422
  for (const namespaceId of namespaceIds) {
408
423
  const ns = contract.storage.namespaces[namespaceId];
409
424
  if (!ns) continue;
410
- for (const [tableName, contractTableRaw] of Object.entries(ns.tables)) {
425
+ for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table)) {
411
426
  if (!(contractTableRaw instanceof StorageTable)) {
412
427
  throw new Error(
413
- `verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.tables.${tableName}`,
428
+ `verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.entries.table.${tableName}`,
414
429
  );
415
430
  }
416
431
  const contractTable = contractTableRaw;
432
+ const tableControlPolicy = effectiveControlPolicy(
433
+ contractTable.control,
434
+ contractDefaultControl,
435
+ );
417
436
  const schemaTable = schemaTables[tableName];
418
- const tablePath = `storage.namespaces.${namespaceId}.tables.${tableName}`;
437
+ const tablePath = `storage.namespaces.${namespaceId}.entries.table.${tableName}`;
419
438
 
420
439
  if (!schemaTable) {
421
- issues.push({
440
+ const issue: SchemaIssue = {
422
441
  kind: 'missing_table',
423
442
  table: tableName,
424
443
  namespaceId,
425
444
  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
- });
445
+ };
446
+ emitIssueAndNodeUnderControlPolicy(
447
+ tableControlPolicy,
448
+ issue,
449
+ {
450
+ status: 'fail',
451
+ kind: 'table',
452
+ name: `table ${tableName}`,
453
+ contractPath: tablePath,
454
+ code: 'missing_table',
455
+ message: `Table "${tableName}" is missing`,
456
+ expected: undefined,
457
+ actual: undefined,
458
+ children: [],
459
+ },
460
+ issues,
461
+ rootChildren,
462
+ );
438
463
  continue;
439
464
  }
440
465
 
@@ -444,6 +469,7 @@ function verifySchemaTables(options: {
444
469
  tableName,
445
470
  namespaceId,
446
471
  tablePath,
472
+ tableControlPolicy,
447
473
  issues,
448
474
  strict,
449
475
  typeMetadataRegistry,
@@ -459,29 +485,33 @@ function verifySchemaTables(options: {
459
485
  if (strict) {
460
486
  for (const tableName of Object.keys(schemaTables)) {
461
487
  const claimed = namespaceIds.some(
462
- (namespaceId) => contract.storage.namespaces[namespaceId]?.tables[tableName] !== undefined,
488
+ (namespaceId) =>
489
+ contract.storage.namespaces[namespaceId]?.entries.table[tableName] !== undefined,
463
490
  );
464
491
  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({
492
+ const extraTableControlPolicy = effectiveControlPolicy(undefined, contractDefaultControl);
493
+ const issue: SchemaIssue = {
470
494
  kind: 'extra_table',
471
495
  table: tableName,
472
496
  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
- });
497
+ };
498
+ emitIssueAndNodeUnderControlPolicy(
499
+ extraTableControlPolicy,
500
+ issue,
501
+ {
502
+ status: 'fail',
503
+ kind: 'table',
504
+ name: `table ${tableName}`,
505
+ contractPath: `storage.namespaces.*.entries.table.${tableName}`,
506
+ code: 'extra_table',
507
+ message: `Extra table "${tableName}" found`,
508
+ expected: undefined,
509
+ actual: undefined,
510
+ children: [],
511
+ },
512
+ issues,
513
+ rootChildren,
514
+ );
485
515
  }
486
516
  }
487
517
  }
@@ -495,6 +525,7 @@ function verifyTableChildren(options: {
495
525
  tableName: string;
496
526
  namespaceId: string;
497
527
  tablePath: string;
528
+ tableControlPolicy: ControlPolicy;
498
529
  issues: SchemaIssue[];
499
530
  strict: boolean;
500
531
  typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
@@ -509,6 +540,7 @@ function verifyTableChildren(options: {
509
540
  tableName,
510
541
  namespaceId,
511
542
  tablePath,
543
+ tableControlPolicy,
512
544
  issues,
513
545
  strict,
514
546
  typeMetadataRegistry,
@@ -524,6 +556,7 @@ function verifyTableChildren(options: {
524
556
  tableName,
525
557
  namespaceId,
526
558
  tablePath,
559
+ tableControlPolicy,
527
560
  issues,
528
561
  strict,
529
562
  typeMetadataRegistry,
@@ -542,6 +575,7 @@ function verifyTableChildren(options: {
542
575
  tableName,
543
576
  namespaceId,
544
577
  tablePath,
578
+ tableControlPolicy,
545
579
  issues,
546
580
  columnNodes,
547
581
  });
@@ -553,6 +587,7 @@ function verifyTableChildren(options: {
553
587
  schemaTable.primaryKey,
554
588
  tableName,
555
589
  namespaceId,
590
+ tableControlPolicy,
556
591
  issues,
557
592
  );
558
593
  if (pkStatus === 'fail') {
@@ -567,6 +602,18 @@ function verifyTableChildren(options: {
567
602
  actual: schemaTable.primaryKey,
568
603
  children: [],
569
604
  });
605
+ } else if (pkStatus === 'warn') {
606
+ tableChildren.push({
607
+ status: 'warn',
608
+ kind: 'primaryKey',
609
+ name: `primary key: ${contractTable.primaryKey.columns.join(', ')}`,
610
+ contractPath: `${tablePath}.primaryKey`,
611
+ code: 'primary_key_mismatch',
612
+ message: 'Primary key mismatch',
613
+ expected: contractTable.primaryKey,
614
+ actual: schemaTable.primaryKey,
615
+ children: [],
616
+ });
570
617
  } else {
571
618
  tableChildren.push({
572
619
  status: 'pass',
@@ -581,23 +628,29 @@ function verifyTableChildren(options: {
581
628
  });
582
629
  }
583
630
  } else if (schemaTable.primaryKey && strict) {
584
- issues.push({
631
+ const issue: SchemaIssue = {
585
632
  kind: 'extra_primary_key',
586
633
  table: tableName,
587
634
  namespaceId,
588
635
  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
- });
636
+ };
637
+ emitIssueAndNodeUnderControlPolicy(
638
+ tableControlPolicy,
639
+ issue,
640
+ {
641
+ status: 'fail',
642
+ kind: 'primaryKey',
643
+ name: `primary key: ${schemaTable.primaryKey.columns.join(', ')}`,
644
+ contractPath: `${tablePath}.primaryKey`,
645
+ code: 'extra_primary_key',
646
+ message: 'Extra primary key found',
647
+ expected: undefined,
648
+ actual: schemaTable.primaryKey,
649
+ children: [],
650
+ },
651
+ issues,
652
+ tableChildren,
653
+ );
601
654
  }
602
655
 
603
656
  // Verify FK constraints only for FKs with constraint: true.
@@ -611,6 +664,7 @@ function verifyTableChildren(options: {
611
664
  tableName,
612
665
  namespaceId,
613
666
  tablePath,
667
+ tableControlPolicy,
614
668
  issues,
615
669
  strict,
616
670
  );
@@ -624,6 +678,7 @@ function verifyTableChildren(options: {
624
678
  tableName,
625
679
  namespaceId,
626
680
  tablePath,
681
+ tableControlPolicy,
627
682
  issues,
628
683
  strict,
629
684
  );
@@ -648,6 +703,7 @@ function verifyTableChildren(options: {
648
703
  tableName,
649
704
  namespaceId,
650
705
  tablePath,
706
+ tableControlPolicy,
651
707
  issues,
652
708
  strict,
653
709
  );
@@ -662,6 +718,7 @@ function collectContractColumnNodes(options: {
662
718
  tableName: string;
663
719
  namespaceId: string;
664
720
  tablePath: string;
721
+ tableControlPolicy: ControlPolicy;
665
722
  issues: SchemaIssue[];
666
723
  strict: boolean;
667
724
  typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
@@ -676,6 +733,7 @@ function collectContractColumnNodes(options: {
676
733
  tableName,
677
734
  namespaceId,
678
735
  tablePath,
736
+ tableControlPolicy,
679
737
  issues,
680
738
  strict,
681
739
  typeMetadataRegistry,
@@ -691,24 +749,30 @@ function collectContractColumnNodes(options: {
691
749
  const columnPath = `${tablePath}.columns.${columnName}`;
692
750
 
693
751
  if (!schemaColumn) {
694
- issues.push({
752
+ const issue: SchemaIssue = {
695
753
  kind: 'missing_column',
696
754
  table: tableName,
697
755
  namespaceId,
698
756
  column: columnName,
699
757
  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
- });
758
+ };
759
+ emitIssueAndNodeUnderControlPolicy(
760
+ tableControlPolicy,
761
+ issue,
762
+ {
763
+ status: 'fail',
764
+ kind: 'column',
765
+ name: `${columnName}: missing`,
766
+ contractPath: columnPath,
767
+ code: 'missing_column',
768
+ message: `Column "${columnName}" is missing`,
769
+ expected: undefined,
770
+ actual: undefined,
771
+ children: [],
772
+ },
773
+ issues,
774
+ columnNodes,
775
+ );
712
776
  continue;
713
777
  }
714
778
 
@@ -720,6 +784,7 @@ function collectContractColumnNodes(options: {
720
784
  contractColumn,
721
785
  schemaColumn,
722
786
  columnPath,
787
+ tableControlPolicy,
723
788
  issues,
724
789
  strict,
725
790
  typeMetadataRegistry,
@@ -740,31 +805,46 @@ function appendExtraColumnNodes(options: {
740
805
  tableName: string;
741
806
  namespaceId: string;
742
807
  tablePath: string;
808
+ tableControlPolicy: ControlPolicy;
743
809
  issues: SchemaIssue[];
744
810
  columnNodes: SchemaVerificationNode[];
745
811
  }): void {
746
- const { contractTable, schemaTable, tableName, namespaceId, tablePath, issues, columnNodes } =
747
- options;
812
+ const {
813
+ contractTable,
814
+ schemaTable,
815
+ tableName,
816
+ namespaceId,
817
+ tablePath,
818
+ tableControlPolicy,
819
+ issues,
820
+ columnNodes,
821
+ } = options;
748
822
  for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) {
749
823
  if (!contractTable.columns[columnName]) {
750
- issues.push({
824
+ const issue: SchemaIssue = {
751
825
  kind: 'extra_column',
752
826
  table: tableName,
753
827
  namespaceId,
754
828
  column: columnName,
755
829
  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
- });
830
+ };
831
+ emitIssueAndNodeUnderControlPolicy(
832
+ tableControlPolicy,
833
+ issue,
834
+ {
835
+ status: 'fail',
836
+ kind: 'column',
837
+ name: `${columnName}: extra`,
838
+ contractPath: `${tablePath}.columns.${columnName}`,
839
+ code: 'extra_column',
840
+ message: `Extra column "${columnName}" found`,
841
+ expected: undefined,
842
+ actual: nativeType,
843
+ children: [],
844
+ },
845
+ issues,
846
+ columnNodes,
847
+ );
768
848
  }
769
849
  }
770
850
  }
@@ -776,6 +856,7 @@ function verifyColumn(options: {
776
856
  contractColumn: StorageTable['columns'][string];
777
857
  schemaColumn: SqlSchemaIR['tables'][string]['columns'][string];
778
858
  columnPath: string;
859
+ tableControlPolicy: ControlPolicy;
779
860
  issues: SchemaIssue[];
780
861
  strict: boolean;
781
862
  typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
@@ -791,6 +872,7 @@ function verifyColumn(options: {
791
872
  contractColumn,
792
873
  schemaColumn,
793
874
  columnPath,
875
+ tableControlPolicy,
794
876
  issues,
795
877
  strict,
796
878
  codecHooks,
@@ -812,8 +894,10 @@ function verifyColumn(options: {
812
894
  const schemaNativeType =
813
895
  normalizeNativeType?.(schemaColumn.nativeType) ?? schemaColumn.nativeType;
814
896
 
815
- if (contractNativeType !== schemaNativeType) {
816
- issues.push({
897
+ const typesMatch = contractNativeType === schemaNativeType;
898
+
899
+ if (!typesMatch) {
900
+ const issue: SchemaIssue = {
817
901
  kind: 'type_mismatch',
818
902
  table: tableName,
819
903
  namespaceId,
@@ -821,19 +905,23 @@ function verifyColumn(options: {
821
905
  expected: contractNativeType,
822
906
  actual: schemaNativeType,
823
907
  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';
908
+ };
909
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
910
+ if (disposition !== 'suppress') {
911
+ issues.push(issue);
912
+ columnChildren.push({
913
+ status: disposition,
914
+ kind: 'type',
915
+ name: 'type',
916
+ contractPath: `${columnPath}.nativeType`,
917
+ code: 'type_mismatch',
918
+ message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
919
+ expected: contractNativeType,
920
+ actual: schemaNativeType,
921
+ children: [],
922
+ });
923
+ columnStatus = disposition;
924
+ }
837
925
  }
838
926
 
839
927
  if (resolvedContractColumn.codecId) {
@@ -869,7 +957,7 @@ function verifyColumn(options: {
869
957
  }
870
958
 
871
959
  if (contractColumn.nullable !== schemaColumn.nullable) {
872
- issues.push({
960
+ const issue: SchemaIssue = {
873
961
  kind: 'nullability_mismatch',
874
962
  table: tableName,
875
963
  namespaceId,
@@ -877,44 +965,52 @@ function verifyColumn(options: {
877
965
  expected: String(contractColumn.nullable),
878
966
  actual: String(schemaColumn.nullable),
879
967
  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';
968
+ };
969
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
970
+ if (disposition !== 'suppress') {
971
+ issues.push(issue);
972
+ columnChildren.push({
973
+ status: disposition,
974
+ kind: 'nullability',
975
+ name: 'nullability',
976
+ contractPath: `${columnPath}.nullable`,
977
+ code: 'nullability_mismatch',
978
+ message: `Nullability mismatch: expected ${contractColumn.nullable ? 'nullable' : 'not null'}, got ${schemaColumn.nullable ? 'nullable' : 'not null'}`,
979
+ expected: contractColumn.nullable,
980
+ actual: schemaColumn.nullable,
981
+ children: [],
982
+ });
983
+ columnStatus = disposition;
984
+ }
893
985
  }
894
986
 
895
987
  if (contractColumn.default) {
896
988
  if (!schemaColumn.default) {
897
989
  const defaultDescription = describeColumnDefault(contractColumn.default);
898
- issues.push({
990
+ const issue: SchemaIssue = {
899
991
  kind: 'default_missing',
900
992
  table: tableName,
901
993
  namespaceId,
902
994
  column: columnName,
903
995
  expected: defaultDescription,
904
996
  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';
997
+ };
998
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
999
+ if (disposition !== 'suppress') {
1000
+ issues.push(issue);
1001
+ columnChildren.push({
1002
+ status: disposition,
1003
+ kind: 'default',
1004
+ name: 'default',
1005
+ contractPath: `${columnPath}.default`,
1006
+ code: 'default_missing',
1007
+ message: `Default missing: expected ${defaultDescription}`,
1008
+ expected: defaultDescription,
1009
+ actual: undefined,
1010
+ children: [],
1011
+ });
1012
+ columnStatus = disposition;
1013
+ }
918
1014
  } else if (
919
1015
  !columnDefaultsEqual(
920
1016
  contractColumn.default,
@@ -924,9 +1020,8 @@ function verifyColumn(options: {
924
1020
  )
925
1021
  ) {
926
1022
  const expectedDescription = describeColumnDefault(contractColumn.default);
927
- // schemaColumn.default is now a raw string, describe it as-is
928
1023
  const actualDescription = schemaColumn.default;
929
- issues.push({
1024
+ const issue: SchemaIssue = {
930
1025
  kind: 'default_mismatch',
931
1026
  table: tableName,
932
1027
  namespaceId,
@@ -934,41 +1029,49 @@ function verifyColumn(options: {
934
1029
  expected: expectedDescription,
935
1030
  actual: actualDescription,
936
1031
  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';
1032
+ };
1033
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1034
+ if (disposition !== 'suppress') {
1035
+ issues.push(issue);
1036
+ columnChildren.push({
1037
+ status: disposition,
1038
+ kind: 'default',
1039
+ name: 'default',
1040
+ contractPath: `${columnPath}.default`,
1041
+ code: 'default_mismatch',
1042
+ message: `Default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
1043
+ expected: expectedDescription,
1044
+ actual: actualDescription,
1045
+ children: [],
1046
+ });
1047
+ columnStatus = disposition;
1048
+ }
950
1049
  }
951
1050
  } else if (strict && schemaColumn.default) {
952
- issues.push({
1051
+ const issue: SchemaIssue = {
953
1052
  kind: 'extra_default',
954
1053
  table: tableName,
955
1054
  namespaceId,
956
1055
  column: columnName,
957
1056
  actual: schemaColumn.default,
958
1057
  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';
1058
+ };
1059
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1060
+ if (disposition !== 'suppress') {
1061
+ issues.push(issue);
1062
+ columnChildren.push({
1063
+ status: disposition,
1064
+ kind: 'default',
1065
+ name: 'default',
1066
+ contractPath: `${columnPath}.default`,
1067
+ code: 'extra_default',
1068
+ message: `Extra default: ${schemaColumn.default}`,
1069
+ expected: undefined,
1070
+ actual: schemaColumn.default,
1071
+ children: [],
1072
+ });
1073
+ columnStatus = disposition;
1074
+ }
972
1075
  }
973
1076
 
974
1077
  // Single-pass aggregation for better performance