@prisma-next/family-sql 0.12.0-dev.9 → 0.13.0-dev.1
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/{authoring-type-constructors-F4JpCJl7.mjs → authoring-type-constructors-D4lQ-qpj.mjs} +1 -1
- package/dist/{authoring-type-constructors-F4JpCJl7.mjs.map → authoring-type-constructors-D4lQ-qpj.mjs.map} +1 -1
- package/dist/control-adapter-CgIL9Vtx.d.mts +182 -0
- package/dist/control-adapter-CgIL9Vtx.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +2 -109
- package/dist/control.d.mts +132 -4
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +277 -215
- package/dist/control.mjs.map +1 -1
- package/dist/ir.d.mts +4 -5
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +1 -1
- package/dist/migration.d.mts +1 -1
- package/dist/migration.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/runtime.d.mts +4 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +4 -2
- 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/{sql-contract-serializer-8axtK4lg.mjs → sql-contract-serializer-CY7qnms7.mjs} +18 -36
- package/dist/sql-contract-serializer-CY7qnms7.mjs.map +1 -0
- package/dist/{timestamp-now-generator-BkjCQIde.mjs → timestamp-now-generator-CloimujU.mjs} +1 -1
- package/dist/{timestamp-now-generator-BkjCQIde.mjs.map → timestamp-now-generator-CloimujU.mjs.map} +1 -1
- package/dist/{types-CeeCStqw.d.mts → types-CbwQCzXY.d.mts} +70 -16
- package/dist/types-CbwQCzXY.d.mts.map +1 -0
- package/dist/{verify-Crewz6hG.mjs → verify-C-G0obRm.mjs} +1 -1
- package/dist/{verify-Crewz6hG.mjs.map → verify-C-G0obRm.mjs.map} +1 -1
- package/dist/{verify-sql-schema-CN7pPoTC.d.mts → verify-sql-schema-DcMaT5Zj.d.mts} +1 -1
- package/dist/{verify-sql-schema-CN7pPoTC.d.mts.map → verify-sql-schema-DcMaT5Zj.d.mts.map} +1 -1
- package/dist/{verify-sql-schema-CYLsGCFO.mjs → verify-sql-schema-DlAgBiT_.mjs} +756 -319
- package/dist/verify-sql-schema-DlAgBiT_.mjs.map +1 -0
- package/dist/verify.mjs +1 -1
- package/package.json +23 -23
- package/src/core/control-adapter.ts +116 -7
- package/src/core/control-instance.ts +269 -66
- package/src/core/default-namespace.ts +9 -0
- package/src/core/ir/sql-contract-serializer-base.ts +72 -56
- package/src/core/migrations/contract-to-schema-ir.ts +75 -9
- package/src/core/migrations/control-policy.ts +322 -0
- package/src/core/migrations/field-event-planner.ts +2 -2
- package/src/core/migrations/plan-helpers.ts +16 -0
- package/src/core/migrations/types.ts +17 -7
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +8 -6
- package/src/core/schema-verify/control-verify-emit.ts +46 -0
- package/src/core/schema-verify/verifier-disposition.ts +58 -0
- package/src/core/schema-verify/verify-helpers.ts +310 -111
- package/src/core/schema-verify/verify-sql-schema.ts +309 -178
- package/src/exports/control-adapter.ts +5 -1
- package/src/exports/control.ts +7 -0
- package/src/exports/runtime.ts +7 -0
- package/dist/control-adapter.d.mts.map +0 -1
- package/dist/sql-contract-serializer-8axtK4lg.mjs.map +0 -1
- package/dist/types-CeeCStqw.d.mts.map +0 -1
- 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
|
-
|
|
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 =
|
|
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
|
-
|
|
180
|
-
|
|
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
|
|
211
|
+
status,
|
|
184
212
|
kind: 'storageType',
|
|
185
213
|
name: `type ${typeName}`,
|
|
186
214
|
contractPath,
|
|
187
|
-
code:
|
|
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
|
|
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 (
|
|
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.
|
|
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}.
|
|
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')
|
|
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.
|
|
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}.
|
|
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}.
|
|
439
|
+
const tablePath = `storage.namespaces.${namespaceId}.entries.table.${tableName}`;
|
|
419
440
|
|
|
420
441
|
if (!schemaTable) {
|
|
421
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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) =>
|
|
491
|
+
(namespaceId) =>
|
|
492
|
+
contract.storage.namespaces[namespaceId]?.entries.table[tableName] !== undefined,
|
|
463
493
|
);
|
|
464
494
|
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({
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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 {
|
|
747
|
-
|
|
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
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
816
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|