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