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