@prisma-next/family-sql 0.12.0 → 0.13.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) 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-CgIL9Vtx.d.mts +182 -0
  4. package/dist/control-adapter-CgIL9Vtx.d.mts.map +1 -0
  5. package/dist/control-adapter.d.mts +2 -109
  6. package/dist/control.d.mts +132 -4
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +277 -215
  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-r7BP5n3l.mjs → timestamp-now-generator-CloimujU.mjs} +2 -1
  26. package/dist/{timestamp-now-generator-r7BP5n3l.mjs.map → timestamp-now-generator-CloimujU.mjs.map} +1 -1
  27. package/dist/{types-CeeCStqw.d.mts → types-CbwQCzXY.d.mts} +70 -16
  28. package/dist/types-CbwQCzXY.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-DlAgBiT_.mjs} +756 -319
  34. package/dist/verify-sql-schema-DlAgBiT_.mjs.map +1 -0
  35. package/dist/verify.mjs +1 -1
  36. package/package.json +23 -23
  37. package/src/core/control-adapter.ts +116 -7
  38. package/src/core/control-instance.ts +269 -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 +75 -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 +17 -7
  46. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +8 -6
  47. package/src/core/schema-verify/control-verify-emit.ts +46 -0
  48. package/src/core/schema-verify/verifier-disposition.ts +58 -0
  49. package/src/core/schema-verify/verify-helpers.ts +310 -111
  50. package/src/core/schema-verify/verify-sql-schema.ts +309 -178
  51. package/src/core/timestamp-now-generator.ts +1 -0
  52. package/src/exports/control-adapter.ts +5 -1
  53. package/src/exports/control.ts +7 -0
  54. package/src/exports/runtime.ts +7 -0
  55. package/dist/control-adapter.d.mts.map +0 -1
  56. package/dist/sql-contract-serializer-8axtK4lg.mjs.map +0 -1
  57. package/dist/types-CeeCStqw.d.mts.map +0 -1
  58. 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,12 +28,17 @@ 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';
34
+ import { resolveValueSetValues } from '../migrations/contract-to-schema-ir';
31
35
  import type { CodecControlHooks } from '../migrations/types';
36
+ import { emitIssueAndNodeUnderControlPolicy } from './control-verify-emit';
37
+ import { verifierDisposition } from './verifier-disposition';
32
38
  import {
33
39
  arraysEqual,
34
40
  computeCounts,
41
+ verifyCheckConstraints,
35
42
  verifyForeignKeys,
36
43
  verifyIndexes,
37
44
  verifyPrimaryKey,
@@ -140,7 +147,10 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
140
147
  >),
141
148
  };
142
149
  for (const ns of Object.values(contract.storage.namespaces)) {
143
- const nsEnums = (ns as { enum?: Record<string, PostgresEnumStorageEntry> }).enum;
150
+ const nsEnums = blindCast<
151
+ { readonly type?: Readonly<Record<string, PostgresEnumStorageEntry | StorageTypeInstance>> },
152
+ 'postgres target namespace entries carry a type slot beyond the family-shared SqlNamespace.entries type'
153
+ >(ns.entries).type;
144
154
  if (nsEnums) {
145
155
  for (const [k, v] of Object.entries(nsEnums)) {
146
156
  allStorageTypesMap[k] = v;
@@ -171,51 +181,54 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
171
181
  // namespace coordinate — a bare-name aggregation would collapse them
172
182
  // (last-write-wins) and verify only one.
173
183
  const typeNodes: SchemaVerificationNode[] = [];
184
+ // Storage-type findings dispatch through the same control policy as tables
185
+ // and columns: each issue's disposition (fail / warn / suppress) is resolved
186
+ // from the type's effective control so an `external`/`observed` enum no longer
187
+ // hard-fails on value drift. `suppress` drops the issue entirely; the node
188
+ // status is the worst surviving disposition.
174
189
  const pushTypeNode = (
175
190
  typeName: string,
176
191
  contractPath: string,
177
192
  typeIssues: readonly SchemaIssue[],
193
+ controlPolicy: ControlPolicy,
178
194
  ): void => {
179
- if (typeIssues.length > 0) {
180
- issues.push(...typeIssues);
195
+ let status: VerificationStatus = 'pass';
196
+ let code = '';
197
+ let emitted = 0;
198
+ for (const issue of typeIssues) {
199
+ const disposition = verifierDisposition(controlPolicy, issue.kind);
200
+ if (disposition === 'suppress') continue;
201
+ issues.push(issue);
202
+ emitted += 1;
203
+ if (code === '') code = issue.kind;
204
+ if (disposition === 'fail') {
205
+ status = 'fail';
206
+ } else if (disposition === 'warn' && status !== 'fail') {
207
+ status = 'warn';
208
+ }
181
209
  }
182
210
  typeNodes.push({
183
- status: typeIssues.length > 0 ? 'fail' : 'pass',
211
+ status,
184
212
  kind: 'storageType',
185
213
  name: `type ${typeName}`,
186
214
  contractPath,
187
- code: typeIssues.length > 0 ? (typeIssues[0]?.kind ?? '') : '',
188
- message:
189
- typeIssues.length > 0
190
- ? `${typeIssues.length} issue${typeIssues.length === 1 ? '' : 's'}`
191
- : '',
215
+ code: status === 'pass' ? '' : code,
216
+ message: emitted > 0 ? `${emitted} issue${emitted === 1 ? '' : 's'}` : '',
192
217
  expected: undefined,
193
218
  actual: undefined,
194
219
  children: [],
195
220
  });
196
221
  };
197
222
 
198
- // Top-level `storage.types`: codec-typed entries via codec hooks; a
199
- // defensive top-level enum is verified under the unbound coordinate.
223
+ // Top-level `storage.types`: codec-typed entries via codec hooks.
200
224
  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)) {
225
+ if (isStorageTypeInstance(typeInstance)) {
214
226
  const hook = codecHooks.get(typeInstance.codecId);
215
227
  pushTypeNode(
216
228
  typeName,
217
229
  `storage.types.${typeName}`,
218
230
  hook?.verifyType ? hook.verifyType({ typeName, typeInstance, schema }) : [],
231
+ effectiveControlPolicy(undefined, contract.defaultControlPolicy),
219
232
  );
220
233
  }
221
234
  }
@@ -224,13 +237,13 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
224
237
  for (const nsId of Object.keys(contract.storage.namespaces)) {
225
238
  const ns = contract.storage.namespaces[nsId];
226
239
  if (!ns) continue;
227
- const nsEnums = ns.enum;
240
+ const nsEnums = ns.entries['type'];
228
241
  if (!nsEnums) continue;
229
242
  for (const [typeName, entry] of Object.entries(nsEnums)) {
230
243
  if (!isPostgresEnumStorageEntry(entry)) continue;
231
244
  pushTypeNode(
232
245
  typeName,
233
- `storage.namespaces.${nsId}.enum.${typeName}`,
246
+ `storage.namespaces.${nsId}.entries.type.${typeName}`,
234
247
  verifyEnumType({
235
248
  typeName,
236
249
  typeInstance: entry,
@@ -238,12 +251,17 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
238
251
  resolveExistingEnumValues,
239
252
  namespaceId: nsId,
240
253
  }),
254
+ effectiveControlPolicy(entry.control, contract.defaultControlPolicy),
241
255
  );
242
256
  }
243
257
  }
244
258
 
245
259
  if (typeNodes.length > 0) {
246
- const typesStatus = typeNodes.some((n) => n.status === 'fail') ? 'fail' : 'pass';
260
+ const typesStatus: VerificationStatus = typeNodes.some((n) => n.status === 'fail')
261
+ ? 'fail'
262
+ : typeNodes.some((n) => n.status === 'warn')
263
+ ? 'warn'
264
+ : 'pass';
247
265
  rootChildren.push({
248
266
  status: typesStatus,
249
267
  kind: 'storageTypes',
@@ -303,8 +321,6 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
303
321
  };
304
322
  }
305
323
 
306
- type VerificationStatus = 'pass' | 'warn' | 'fail';
307
-
308
324
  /**
309
325
  * Native verification walk for `PostgresEnumStorageEntry` instances (no codec hook).
310
326
  *
@@ -397,6 +413,7 @@ function verifySchemaTables(options: {
397
413
  normalizeDefault,
398
414
  normalizeNativeType,
399
415
  } = options;
416
+ const contractDefaultControl = contract.defaultControlPolicy;
400
417
  const issues: SchemaIssue[] = [];
401
418
  const rootChildren: SchemaVerificationNode[] = [];
402
419
  const schemaTables = schema.tables;
@@ -407,34 +424,44 @@ function verifySchemaTables(options: {
407
424
  for (const namespaceId of namespaceIds) {
408
425
  const ns = contract.storage.namespaces[namespaceId];
409
426
  if (!ns) continue;
410
- for (const [tableName, contractTableRaw] of Object.entries(ns.tables)) {
427
+ for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table)) {
411
428
  if (!(contractTableRaw instanceof StorageTable)) {
412
429
  throw new Error(
413
- `verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.tables.${tableName}`,
430
+ `verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.entries.table.${tableName}`,
414
431
  );
415
432
  }
416
433
  const contractTable = contractTableRaw;
434
+ const tableControlPolicy = effectiveControlPolicy(
435
+ contractTable.control,
436
+ contractDefaultControl,
437
+ );
417
438
  const schemaTable = schemaTables[tableName];
418
- const tablePath = `storage.namespaces.${namespaceId}.tables.${tableName}`;
439
+ const tablePath = `storage.namespaces.${namespaceId}.entries.table.${tableName}`;
419
440
 
420
441
  if (!schemaTable) {
421
- issues.push({
442
+ const issue: SchemaIssue = {
422
443
  kind: 'missing_table',
423
444
  table: tableName,
424
445
  namespaceId,
425
446
  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
- });
447
+ };
448
+ emitIssueAndNodeUnderControlPolicy(
449
+ tableControlPolicy,
450
+ issue,
451
+ {
452
+ status: 'fail',
453
+ kind: 'table',
454
+ name: `table ${tableName}`,
455
+ contractPath: tablePath,
456
+ code: 'missing_table',
457
+ message: `Table "${tableName}" is missing`,
458
+ expected: undefined,
459
+ actual: undefined,
460
+ children: [],
461
+ },
462
+ issues,
463
+ rootChildren,
464
+ );
438
465
  continue;
439
466
  }
440
467
 
@@ -444,11 +471,13 @@ function verifySchemaTables(options: {
444
471
  tableName,
445
472
  namespaceId,
446
473
  tablePath,
474
+ tableControlPolicy,
447
475
  issues,
448
476
  strict,
449
477
  typeMetadataRegistry,
450
478
  codecHooks,
451
479
  storageTypes,
480
+ contractStorage: contract.storage,
452
481
  ...ifDefined('normalizeDefault', normalizeDefault),
453
482
  ...ifDefined('normalizeNativeType', normalizeNativeType),
454
483
  });
@@ -459,29 +488,33 @@ function verifySchemaTables(options: {
459
488
  if (strict) {
460
489
  for (const tableName of Object.keys(schemaTables)) {
461
490
  const claimed = namespaceIds.some(
462
- (namespaceId) => contract.storage.namespaces[namespaceId]?.tables[tableName] !== undefined,
491
+ (namespaceId) =>
492
+ contract.storage.namespaces[namespaceId]?.entries.table[tableName] !== undefined,
463
493
  );
464
494
  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({
495
+ const extraTableControlPolicy = effectiveControlPolicy(undefined, contractDefaultControl);
496
+ const issue: SchemaIssue = {
470
497
  kind: 'extra_table',
471
498
  table: tableName,
472
499
  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
- });
500
+ };
501
+ emitIssueAndNodeUnderControlPolicy(
502
+ extraTableControlPolicy,
503
+ issue,
504
+ {
505
+ status: 'fail',
506
+ kind: 'table',
507
+ name: `table ${tableName}`,
508
+ contractPath: `storage.namespaces.*.entries.table.${tableName}`,
509
+ code: 'extra_table',
510
+ message: `Extra table "${tableName}" found`,
511
+ expected: undefined,
512
+ actual: undefined,
513
+ children: [],
514
+ },
515
+ issues,
516
+ rootChildren,
517
+ );
485
518
  }
486
519
  }
487
520
  }
@@ -495,6 +528,7 @@ function verifyTableChildren(options: {
495
528
  tableName: string;
496
529
  namespaceId: string;
497
530
  tablePath: string;
531
+ tableControlPolicy: ControlPolicy;
498
532
  issues: SchemaIssue[];
499
533
  strict: boolean;
500
534
  typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
@@ -502,6 +536,7 @@ function verifyTableChildren(options: {
502
536
  storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
503
537
  normalizeDefault?: DefaultNormalizer;
504
538
  normalizeNativeType?: NativeTypeNormalizer;
539
+ contractStorage: SqlStorage;
505
540
  }): SchemaVerificationNode[] {
506
541
  const {
507
542
  contractTable,
@@ -509,6 +544,7 @@ function verifyTableChildren(options: {
509
544
  tableName,
510
545
  namespaceId,
511
546
  tablePath,
547
+ tableControlPolicy,
512
548
  issues,
513
549
  strict,
514
550
  typeMetadataRegistry,
@@ -516,6 +552,7 @@ function verifyTableChildren(options: {
516
552
  storageTypes,
517
553
  normalizeDefault,
518
554
  normalizeNativeType,
555
+ contractStorage,
519
556
  } = options;
520
557
  const tableChildren: SchemaVerificationNode[] = [];
521
558
  const columnNodes = collectContractColumnNodes({
@@ -524,6 +561,7 @@ function verifyTableChildren(options: {
524
561
  tableName,
525
562
  namespaceId,
526
563
  tablePath,
564
+ tableControlPolicy,
527
565
  issues,
528
566
  strict,
529
567
  typeMetadataRegistry,
@@ -542,6 +580,7 @@ function verifyTableChildren(options: {
542
580
  tableName,
543
581
  namespaceId,
544
582
  tablePath,
583
+ tableControlPolicy,
545
584
  issues,
546
585
  columnNodes,
547
586
  });
@@ -553,6 +592,7 @@ function verifyTableChildren(options: {
553
592
  schemaTable.primaryKey,
554
593
  tableName,
555
594
  namespaceId,
595
+ tableControlPolicy,
556
596
  issues,
557
597
  );
558
598
  if (pkStatus === 'fail') {
@@ -567,6 +607,18 @@ function verifyTableChildren(options: {
567
607
  actual: schemaTable.primaryKey,
568
608
  children: [],
569
609
  });
610
+ } else if (pkStatus === 'warn') {
611
+ tableChildren.push({
612
+ status: 'warn',
613
+ kind: 'primaryKey',
614
+ name: `primary key: ${contractTable.primaryKey.columns.join(', ')}`,
615
+ contractPath: `${tablePath}.primaryKey`,
616
+ code: 'primary_key_mismatch',
617
+ message: 'Primary key mismatch',
618
+ expected: contractTable.primaryKey,
619
+ actual: schemaTable.primaryKey,
620
+ children: [],
621
+ });
570
622
  } else {
571
623
  tableChildren.push({
572
624
  status: 'pass',
@@ -581,23 +633,29 @@ function verifyTableChildren(options: {
581
633
  });
582
634
  }
583
635
  } else if (schemaTable.primaryKey && strict) {
584
- issues.push({
636
+ const issue: SchemaIssue = {
585
637
  kind: 'extra_primary_key',
586
638
  table: tableName,
587
639
  namespaceId,
588
640
  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
- });
641
+ };
642
+ emitIssueAndNodeUnderControlPolicy(
643
+ tableControlPolicy,
644
+ issue,
645
+ {
646
+ status: 'fail',
647
+ kind: 'primaryKey',
648
+ name: `primary key: ${schemaTable.primaryKey.columns.join(', ')}`,
649
+ contractPath: `${tablePath}.primaryKey`,
650
+ code: 'extra_primary_key',
651
+ message: 'Extra primary key found',
652
+ expected: undefined,
653
+ actual: schemaTable.primaryKey,
654
+ children: [],
655
+ },
656
+ issues,
657
+ tableChildren,
658
+ );
601
659
  }
602
660
 
603
661
  // Verify FK constraints only for FKs with constraint: true.
@@ -611,6 +669,7 @@ function verifyTableChildren(options: {
611
669
  tableName,
612
670
  namespaceId,
613
671
  tablePath,
672
+ tableControlPolicy,
614
673
  issues,
615
674
  strict,
616
675
  );
@@ -624,6 +683,7 @@ function verifyTableChildren(options: {
624
683
  tableName,
625
684
  namespaceId,
626
685
  tablePath,
686
+ tableControlPolicy,
627
687
  issues,
628
688
  strict,
629
689
  );
@@ -648,11 +708,35 @@ function verifyTableChildren(options: {
648
708
  tableName,
649
709
  namespaceId,
650
710
  tablePath,
711
+ tableControlPolicy,
651
712
  issues,
652
713
  strict,
653
714
  );
654
715
  tableChildren.push(...indexStatuses);
655
716
 
717
+ // Verify check constraints when the contract declares checks for this table OR
718
+ // when strict mode is on (so extra live checks on zero-check tables are detected).
719
+ // schemaTable.checks carries the introspected live checks (parsed value sets).
720
+ // This call is additive: verifyEnumType (the native enum path) is untouched.
721
+ const contractCheckIRs = (contractTable.checks ?? []).map((c) => ({
722
+ name: c.name,
723
+ column: c.column,
724
+ permittedValues: resolveValueSetValues(c.valueSet, contractStorage, `check "${c.name}"`),
725
+ }));
726
+ if (strict || contractCheckIRs.length > 0) {
727
+ const checkStatuses = verifyCheckConstraints(
728
+ contractCheckIRs,
729
+ schemaTable.checks ?? [],
730
+ tableName,
731
+ namespaceId,
732
+ tablePath,
733
+ tableControlPolicy,
734
+ issues,
735
+ strict,
736
+ );
737
+ tableChildren.push(...checkStatuses);
738
+ }
739
+
656
740
  return tableChildren;
657
741
  }
658
742
 
@@ -662,6 +746,7 @@ function collectContractColumnNodes(options: {
662
746
  tableName: string;
663
747
  namespaceId: string;
664
748
  tablePath: string;
749
+ tableControlPolicy: ControlPolicy;
665
750
  issues: SchemaIssue[];
666
751
  strict: boolean;
667
752
  typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
@@ -676,6 +761,7 @@ function collectContractColumnNodes(options: {
676
761
  tableName,
677
762
  namespaceId,
678
763
  tablePath,
764
+ tableControlPolicy,
679
765
  issues,
680
766
  strict,
681
767
  typeMetadataRegistry,
@@ -691,24 +777,30 @@ function collectContractColumnNodes(options: {
691
777
  const columnPath = `${tablePath}.columns.${columnName}`;
692
778
 
693
779
  if (!schemaColumn) {
694
- issues.push({
780
+ const issue: SchemaIssue = {
695
781
  kind: 'missing_column',
696
782
  table: tableName,
697
783
  namespaceId,
698
784
  column: columnName,
699
785
  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
- });
786
+ };
787
+ emitIssueAndNodeUnderControlPolicy(
788
+ tableControlPolicy,
789
+ issue,
790
+ {
791
+ status: 'fail',
792
+ kind: 'column',
793
+ name: `${columnName}: missing`,
794
+ contractPath: columnPath,
795
+ code: 'missing_column',
796
+ message: `Column "${columnName}" is missing`,
797
+ expected: undefined,
798
+ actual: undefined,
799
+ children: [],
800
+ },
801
+ issues,
802
+ columnNodes,
803
+ );
712
804
  continue;
713
805
  }
714
806
 
@@ -720,6 +812,7 @@ function collectContractColumnNodes(options: {
720
812
  contractColumn,
721
813
  schemaColumn,
722
814
  columnPath,
815
+ tableControlPolicy,
723
816
  issues,
724
817
  strict,
725
818
  typeMetadataRegistry,
@@ -740,31 +833,46 @@ function appendExtraColumnNodes(options: {
740
833
  tableName: string;
741
834
  namespaceId: string;
742
835
  tablePath: string;
836
+ tableControlPolicy: ControlPolicy;
743
837
  issues: SchemaIssue[];
744
838
  columnNodes: SchemaVerificationNode[];
745
839
  }): void {
746
- const { contractTable, schemaTable, tableName, namespaceId, tablePath, issues, columnNodes } =
747
- options;
840
+ const {
841
+ contractTable,
842
+ schemaTable,
843
+ tableName,
844
+ namespaceId,
845
+ tablePath,
846
+ tableControlPolicy,
847
+ issues,
848
+ columnNodes,
849
+ } = options;
748
850
  for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) {
749
851
  if (!contractTable.columns[columnName]) {
750
- issues.push({
852
+ const issue: SchemaIssue = {
751
853
  kind: 'extra_column',
752
854
  table: tableName,
753
855
  namespaceId,
754
856
  column: columnName,
755
857
  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
- });
858
+ };
859
+ emitIssueAndNodeUnderControlPolicy(
860
+ tableControlPolicy,
861
+ issue,
862
+ {
863
+ status: 'fail',
864
+ kind: 'column',
865
+ name: `${columnName}: extra`,
866
+ contractPath: `${tablePath}.columns.${columnName}`,
867
+ code: 'extra_column',
868
+ message: `Extra column "${columnName}" found`,
869
+ expected: undefined,
870
+ actual: nativeType,
871
+ children: [],
872
+ },
873
+ issues,
874
+ columnNodes,
875
+ );
768
876
  }
769
877
  }
770
878
  }
@@ -776,6 +884,7 @@ function verifyColumn(options: {
776
884
  contractColumn: StorageTable['columns'][string];
777
885
  schemaColumn: SqlSchemaIR['tables'][string]['columns'][string];
778
886
  columnPath: string;
887
+ tableControlPolicy: ControlPolicy;
779
888
  issues: SchemaIssue[];
780
889
  strict: boolean;
781
890
  typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
@@ -791,6 +900,7 @@ function verifyColumn(options: {
791
900
  contractColumn,
792
901
  schemaColumn,
793
902
  columnPath,
903
+ tableControlPolicy,
794
904
  issues,
795
905
  strict,
796
906
  codecHooks,
@@ -812,8 +922,10 @@ function verifyColumn(options: {
812
922
  const schemaNativeType =
813
923
  normalizeNativeType?.(schemaColumn.nativeType) ?? schemaColumn.nativeType;
814
924
 
815
- if (contractNativeType !== schemaNativeType) {
816
- issues.push({
925
+ const typesMatch = contractNativeType === schemaNativeType;
926
+
927
+ if (!typesMatch) {
928
+ const issue: SchemaIssue = {
817
929
  kind: 'type_mismatch',
818
930
  table: tableName,
819
931
  namespaceId,
@@ -821,19 +933,23 @@ function verifyColumn(options: {
821
933
  expected: contractNativeType,
822
934
  actual: schemaNativeType,
823
935
  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';
936
+ };
937
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
938
+ if (disposition !== 'suppress') {
939
+ issues.push(issue);
940
+ columnChildren.push({
941
+ status: disposition,
942
+ kind: 'type',
943
+ name: 'type',
944
+ contractPath: `${columnPath}.nativeType`,
945
+ code: 'type_mismatch',
946
+ message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
947
+ expected: contractNativeType,
948
+ actual: schemaNativeType,
949
+ children: [],
950
+ });
951
+ columnStatus = disposition;
952
+ }
837
953
  }
838
954
 
839
955
  if (resolvedContractColumn.codecId) {
@@ -869,7 +985,7 @@ function verifyColumn(options: {
869
985
  }
870
986
 
871
987
  if (contractColumn.nullable !== schemaColumn.nullable) {
872
- issues.push({
988
+ const issue: SchemaIssue = {
873
989
  kind: 'nullability_mismatch',
874
990
  table: tableName,
875
991
  namespaceId,
@@ -877,44 +993,52 @@ function verifyColumn(options: {
877
993
  expected: String(contractColumn.nullable),
878
994
  actual: String(schemaColumn.nullable),
879
995
  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';
996
+ };
997
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
998
+ if (disposition !== 'suppress') {
999
+ issues.push(issue);
1000
+ columnChildren.push({
1001
+ status: disposition,
1002
+ kind: 'nullability',
1003
+ name: 'nullability',
1004
+ contractPath: `${columnPath}.nullable`,
1005
+ code: 'nullability_mismatch',
1006
+ message: `Nullability mismatch: expected ${contractColumn.nullable ? 'nullable' : 'not null'}, got ${schemaColumn.nullable ? 'nullable' : 'not null'}`,
1007
+ expected: contractColumn.nullable,
1008
+ actual: schemaColumn.nullable,
1009
+ children: [],
1010
+ });
1011
+ columnStatus = disposition;
1012
+ }
893
1013
  }
894
1014
 
895
1015
  if (contractColumn.default) {
896
1016
  if (!schemaColumn.default) {
897
1017
  const defaultDescription = describeColumnDefault(contractColumn.default);
898
- issues.push({
1018
+ const issue: SchemaIssue = {
899
1019
  kind: 'default_missing',
900
1020
  table: tableName,
901
1021
  namespaceId,
902
1022
  column: columnName,
903
1023
  expected: defaultDescription,
904
1024
  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';
1025
+ };
1026
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1027
+ if (disposition !== 'suppress') {
1028
+ issues.push(issue);
1029
+ columnChildren.push({
1030
+ status: disposition,
1031
+ kind: 'default',
1032
+ name: 'default',
1033
+ contractPath: `${columnPath}.default`,
1034
+ code: 'default_missing',
1035
+ message: `Default missing: expected ${defaultDescription}`,
1036
+ expected: defaultDescription,
1037
+ actual: undefined,
1038
+ children: [],
1039
+ });
1040
+ columnStatus = disposition;
1041
+ }
918
1042
  } else if (
919
1043
  !columnDefaultsEqual(
920
1044
  contractColumn.default,
@@ -924,9 +1048,8 @@ function verifyColumn(options: {
924
1048
  )
925
1049
  ) {
926
1050
  const expectedDescription = describeColumnDefault(contractColumn.default);
927
- // schemaColumn.default is now a raw string, describe it as-is
928
1051
  const actualDescription = schemaColumn.default;
929
- issues.push({
1052
+ const issue: SchemaIssue = {
930
1053
  kind: 'default_mismatch',
931
1054
  table: tableName,
932
1055
  namespaceId,
@@ -934,41 +1057,49 @@ function verifyColumn(options: {
934
1057
  expected: expectedDescription,
935
1058
  actual: actualDescription,
936
1059
  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';
1060
+ };
1061
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1062
+ if (disposition !== 'suppress') {
1063
+ issues.push(issue);
1064
+ columnChildren.push({
1065
+ status: disposition,
1066
+ kind: 'default',
1067
+ name: 'default',
1068
+ contractPath: `${columnPath}.default`,
1069
+ code: 'default_mismatch',
1070
+ message: `Default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
1071
+ expected: expectedDescription,
1072
+ actual: actualDescription,
1073
+ children: [],
1074
+ });
1075
+ columnStatus = disposition;
1076
+ }
950
1077
  }
951
1078
  } else if (strict && schemaColumn.default) {
952
- issues.push({
1079
+ const issue: SchemaIssue = {
953
1080
  kind: 'extra_default',
954
1081
  table: tableName,
955
1082
  namespaceId,
956
1083
  column: columnName,
957
1084
  actual: schemaColumn.default,
958
1085
  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';
1086
+ };
1087
+ const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1088
+ if (disposition !== 'suppress') {
1089
+ issues.push(issue);
1090
+ columnChildren.push({
1091
+ status: disposition,
1092
+ kind: 'default',
1093
+ name: 'default',
1094
+ contractPath: `${columnPath}.default`,
1095
+ code: 'extra_default',
1096
+ message: `Extra default: ${schemaColumn.default}`,
1097
+ expected: undefined,
1098
+ actual: schemaColumn.default,
1099
+ children: [],
1100
+ });
1101
+ columnStatus = disposition;
1102
+ }
972
1103
  }
973
1104
 
974
1105
  // Single-pass aggregation for better performance