@prisma-next/family-sql 0.14.0 → 0.15.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-CjFfO6LM.mjs → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
- package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
- package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-BQgad8Zc.d.mts} +30 -8
- package/dist/control-adapter-BQgad8Zc.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +1 -1
- package/dist/control.d.mts +279 -34
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +364 -1005
- package/dist/control.mjs.map +1 -1
- package/dist/diff.d.mts +102 -0
- package/dist/diff.d.mts.map +1 -0
- package/dist/diff.mjs +14 -0
- package/dist/diff.mjs.map +1 -0
- package/dist/ir.d.mts +26 -9
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +2 -2
- package/dist/ir.mjs.map +1 -1
- package/dist/migration.d.mts +11 -2
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +7 -0
- package/dist/migration.mjs.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/psl-infer.d.mts +119 -0
- package/dist/psl-infer.d.mts.map +1 -0
- package/dist/psl-infer.mjs +366 -0
- package/dist/psl-infer.mjs.map +1 -0
- package/dist/schema-differ-DnoopSXm.d.mts +45 -0
- package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
- package/dist/schema-verify-W3r631Jh.mjs +226 -0
- package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
- package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-C75cfMSS.mjs} +46 -20
- package/dist/sql-contract-serializer-C75cfMSS.mjs.map +1 -0
- package/dist/{types-kgstZ_Zd.d.mts → types-BPv_y7iS.d.mts} +36 -142
- package/dist/types-BPv_y7iS.d.mts.map +1 -0
- package/package.json +26 -26
- package/src/core/authoring-entity-types.ts +12 -37
- package/src/core/control-adapter.ts +11 -5
- package/src/core/control-instance.ts +212 -42
- package/src/core/control-target-descriptor.ts +98 -0
- package/src/core/diff/schema-verify.ts +324 -0
- package/src/core/diff/sql-schema-diff.ts +41 -0
- package/src/core/diff/verifier-disposition.ts +30 -0
- package/src/core/ir/sql-contract-serializer-base.ts +70 -56
- package/src/core/ir/sql-contract-serializer.ts +5 -7
- package/src/core/ir/sql-schema-verifier-base.ts +5 -5
- package/src/core/migrations/contract-to-schema-ir.ts +137 -51
- package/src/core/migrations/control-policy.ts +66 -115
- package/src/core/migrations/field-event-planner.ts +2 -2
- package/src/core/migrations/native-type-expander.ts +28 -0
- package/src/core/migrations/schema-differ.ts +41 -0
- package/src/core/migrations/types.ts +33 -37
- package/src/core/psl-contract-infer/name-transforms.ts +15 -0
- package/src/core/psl-contract-infer/printer-config.ts +14 -6
- package/src/core/psl-contract-infer/relation-inference.ts +30 -1
- package/src/core/sql-migration.ts +12 -1
- package/src/exports/control.ts +12 -2
- package/src/exports/diff.ts +25 -0
- package/src/exports/psl-infer.ts +40 -0
- package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
- package/dist/control-adapter-Cmw9LvEP.d.mts.map +0 -1
- package/dist/schema-verify.d.mts +0 -39
- package/dist/schema-verify.d.mts.map +0 -1
- package/dist/schema-verify.mjs +0 -2
- package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
- package/dist/test-utils.d.mts +0 -2
- package/dist/test-utils.mjs +0 -2
- package/dist/types-kgstZ_Zd.d.mts.map +0 -1
- package/dist/verify-sql-schema-thU-jKpf.d.mts +0 -66
- package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
- package/dist/verify-sql-schema-xT4udQLQ.mjs +0 -1501
- package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
- package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
- package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
- package/src/core/schema-verify/control-verify-emit.ts +0 -46
- package/src/core/schema-verify/verifier-disposition.ts +0 -58
- package/src/core/schema-verify/verify-helpers.ts +0 -820
- package/src/core/schema-verify/verify-sql-schema.ts +0 -1311
- package/src/exports/schema-verify.ts +0 -18
- package/src/exports/test-utils.ts +0 -9
|
@@ -1,820 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure verification helper functions for SQL schema verification.
|
|
3
|
-
* These functions verify schema IR against contract requirements.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
7
|
-
import type {
|
|
8
|
-
SchemaIssue,
|
|
9
|
-
SchemaVerificationNode,
|
|
10
|
-
} from '@prisma-next/framework-components/control';
|
|
11
|
-
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
12
|
-
import type {
|
|
13
|
-
ForeignKey,
|
|
14
|
-
Index,
|
|
15
|
-
PrimaryKey,
|
|
16
|
-
UniqueConstraint,
|
|
17
|
-
} from '@prisma-next/sql-contract/types';
|
|
18
|
-
import type {
|
|
19
|
-
SqlCheckConstraintIR,
|
|
20
|
-
SqlForeignKeyIR,
|
|
21
|
-
SqlIndexIR,
|
|
22
|
-
SqlUniqueIR,
|
|
23
|
-
} from '@prisma-next/sql-schema-ir/types';
|
|
24
|
-
import {
|
|
25
|
-
emitIssueAndNodeUnderControlPolicy,
|
|
26
|
-
emitIssueUnderControlPolicy,
|
|
27
|
-
} from './control-verify-emit';
|
|
28
|
-
|
|
29
|
-
function indexOptionsLooselyEqual(
|
|
30
|
-
a: Record<string, unknown> | undefined,
|
|
31
|
-
b: Record<string, unknown> | undefined,
|
|
32
|
-
): boolean {
|
|
33
|
-
const aKeys = a ? Object.keys(a).sort() : [];
|
|
34
|
-
const bKeys = b ? Object.keys(b).sort() : [];
|
|
35
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
36
|
-
for (let i = 0; i < aKeys.length; i += 1) {
|
|
37
|
-
if (aKeys[i] !== bKeys[i]) return false;
|
|
38
|
-
}
|
|
39
|
-
if (aKeys.length === 0) return true;
|
|
40
|
-
for (const key of aKeys) {
|
|
41
|
-
// Postgres introspection returns reloptions values as raw strings (e.g.
|
|
42
|
-
// `'70'`, `'false'`), while contract option leaves are typed (number,
|
|
43
|
-
// boolean, string). Compare via String() so a contract `fillfactor: 70`
|
|
44
|
-
// matches an introspected `fillfactor: '70'` without a spurious mismatch.
|
|
45
|
-
if (
|
|
46
|
-
String((a as Record<string, unknown>)[key]) !== String((b as Record<string, unknown>)[key])
|
|
47
|
-
) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function indexExtrasMatch(
|
|
55
|
-
contractIndex: Index,
|
|
56
|
-
schemaIndex: { readonly type?: string; readonly options?: Record<string, unknown> },
|
|
57
|
-
): boolean {
|
|
58
|
-
if ((contractIndex.type ?? null) !== (schemaIndex.type ?? null)) return false;
|
|
59
|
-
return indexOptionsLooselyEqual(contractIndex.options, schemaIndex.options);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Compares two arrays of strings for equality (order-sensitive).
|
|
64
|
-
*/
|
|
65
|
-
export function arraysEqual(a: readonly string[], b: readonly string[]): boolean {
|
|
66
|
-
if (a.length !== b.length) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
for (let i = 0; i < a.length; i++) {
|
|
70
|
-
if (a[i] !== b[i]) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ============================================================================
|
|
78
|
-
// Semantic Satisfaction Predicates
|
|
79
|
-
// ============================================================================
|
|
80
|
-
// These predicates implement the "stronger satisfies weaker" logic for storage
|
|
81
|
-
// objects. They are used by both verification and migration planning to ensure
|
|
82
|
-
// consistent behavior across the control plane.
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Checks if a unique constraint requirement is satisfied by the given columns.
|
|
86
|
-
*
|
|
87
|
-
* Semantic satisfaction: a unique constraint requirement can be satisfied by:
|
|
88
|
-
* - A unique constraint with the same columns, OR
|
|
89
|
-
* - A unique index with the same columns
|
|
90
|
-
*
|
|
91
|
-
* @param uniques - The unique constraints in the schema table
|
|
92
|
-
* @param indexes - The indexes in the schema table
|
|
93
|
-
* @param columns - The columns required by the unique constraint
|
|
94
|
-
* @returns true if the requirement is satisfied
|
|
95
|
-
*/
|
|
96
|
-
export function isUniqueConstraintSatisfied(
|
|
97
|
-
uniques: readonly SqlUniqueIR[],
|
|
98
|
-
indexes: readonly SqlIndexIR[],
|
|
99
|
-
columns: readonly string[],
|
|
100
|
-
): boolean {
|
|
101
|
-
// Check for matching unique constraint
|
|
102
|
-
const hasConstraint = uniques.some((unique) => arraysEqual(unique.columns, columns));
|
|
103
|
-
if (hasConstraint) {
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
// Check for matching unique index (semantic satisfaction)
|
|
107
|
-
return indexes.some((index) => index.unique && arraysEqual(index.columns, columns));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Checks if an index requirement is satisfied by the given columns.
|
|
112
|
-
*
|
|
113
|
-
* Semantic satisfaction: a non-unique index requirement can be satisfied by:
|
|
114
|
-
* - Any index (unique or non-unique) with the same columns, OR
|
|
115
|
-
* - A unique constraint with the same columns (stronger satisfies weaker)
|
|
116
|
-
*
|
|
117
|
-
* @param indexes - The indexes in the schema table
|
|
118
|
-
* @param uniques - The unique constraints in the schema table
|
|
119
|
-
* @param columns - The columns required by the index
|
|
120
|
-
* @returns true if the requirement is satisfied
|
|
121
|
-
*/
|
|
122
|
-
export function isIndexSatisfied(
|
|
123
|
-
indexes: readonly SqlIndexIR[],
|
|
124
|
-
uniques: readonly SqlUniqueIR[],
|
|
125
|
-
columns: readonly string[],
|
|
126
|
-
): boolean {
|
|
127
|
-
// Check for any matching index (unique or non-unique)
|
|
128
|
-
const hasMatchingIndex = indexes.some((index) => arraysEqual(index.columns, columns));
|
|
129
|
-
if (hasMatchingIndex) {
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
// Check for matching unique constraint (semantic satisfaction)
|
|
133
|
-
return uniques.some((unique) => arraysEqual(unique.columns, columns));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Verifies primary key matches between contract and schema.
|
|
138
|
-
* Returns 'pass' or 'fail'.
|
|
139
|
-
*
|
|
140
|
-
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
141
|
-
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
142
|
-
*/
|
|
143
|
-
export function verifyPrimaryKey(
|
|
144
|
-
contractPK: PrimaryKey,
|
|
145
|
-
schemaPK: PrimaryKey | undefined,
|
|
146
|
-
tableName: string,
|
|
147
|
-
namespaceId: string,
|
|
148
|
-
tableControlPolicy: ControlPolicy,
|
|
149
|
-
issues: SchemaIssue[],
|
|
150
|
-
): 'pass' | 'warn' | 'fail' {
|
|
151
|
-
if (!schemaPK) {
|
|
152
|
-
const issue: SchemaIssue = {
|
|
153
|
-
kind: 'primary_key_mismatch',
|
|
154
|
-
table: tableName,
|
|
155
|
-
namespaceId,
|
|
156
|
-
expected: contractPK.columns.join(', '),
|
|
157
|
-
message: `Table "${tableName}" is missing primary key`,
|
|
158
|
-
};
|
|
159
|
-
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, issue, issues);
|
|
160
|
-
return outcome === 'suppress' ? 'pass' : outcome;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
|
|
164
|
-
const issue: SchemaIssue = {
|
|
165
|
-
kind: 'primary_key_mismatch',
|
|
166
|
-
table: tableName,
|
|
167
|
-
namespaceId,
|
|
168
|
-
expected: contractPK.columns.join(', '),
|
|
169
|
-
actual: schemaPK.columns.join(', '),
|
|
170
|
-
message: `Table "${tableName}" has primary key mismatch: expected columns [${contractPK.columns.join(', ')}], got [${schemaPK.columns.join(', ')}]`,
|
|
171
|
-
};
|
|
172
|
-
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, issue, issues);
|
|
173
|
-
return outcome === 'suppress' ? 'pass' : outcome;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return 'pass';
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Verifies foreign keys match between contract and schema.
|
|
181
|
-
* Returns verification nodes for the tree.
|
|
182
|
-
*
|
|
183
|
-
* Uses semantic satisfaction: identity is based on (table + columns + referenced table + referenced columns).
|
|
184
|
-
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
185
|
-
*/
|
|
186
|
-
export function verifyForeignKeys(
|
|
187
|
-
contractFKs: readonly ForeignKey[],
|
|
188
|
-
schemaFKs: readonly SqlForeignKeyIR[],
|
|
189
|
-
tableName: string,
|
|
190
|
-
namespaceId: string,
|
|
191
|
-
tablePath: string,
|
|
192
|
-
tableControlPolicy: ControlPolicy,
|
|
193
|
-
issues: SchemaIssue[],
|
|
194
|
-
strict: boolean,
|
|
195
|
-
): SchemaVerificationNode[] {
|
|
196
|
-
const nodes: SchemaVerificationNode[] = [];
|
|
197
|
-
|
|
198
|
-
// Check each contract FK exists in schema
|
|
199
|
-
for (const contractFK of contractFKs) {
|
|
200
|
-
const fkPath = `${tablePath}.foreignKeys[${contractFK.source.columns.join(',')}]`;
|
|
201
|
-
const matchingFK = schemaFKs.find((fk) => {
|
|
202
|
-
// When the schema FK carries referencedSchema (populated by the Postgres
|
|
203
|
-
// adapter for cross-schema FKs), compare the full (namespace, table) pair
|
|
204
|
-
// against the contract FK's target coordinate. When referencedSchema is
|
|
205
|
-
// absent (same-schema FKs, older introspection, or hand-crafted test
|
|
206
|
-
// fixtures), fall back to table-name-only comparison so same-namespace
|
|
207
|
-
// contracts continue to verify correctly.
|
|
208
|
-
const tablesMatch =
|
|
209
|
-
fk.referencedSchema !== undefined && contractFK.target.namespaceId !== UNBOUND_NAMESPACE_ID
|
|
210
|
-
? fk.referencedSchema === contractFK.target.namespaceId &&
|
|
211
|
-
fk.referencedTable === contractFK.target.tableName
|
|
212
|
-
: fk.referencedTable === contractFK.target.tableName;
|
|
213
|
-
return (
|
|
214
|
-
arraysEqual(fk.columns, contractFK.source.columns) &&
|
|
215
|
-
tablesMatch &&
|
|
216
|
-
arraysEqual(fk.referencedColumns, contractFK.target.columns)
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
if (!matchingFK) {
|
|
221
|
-
const issue: SchemaIssue = {
|
|
222
|
-
kind: 'foreign_key_mismatch',
|
|
223
|
-
table: tableName,
|
|
224
|
-
namespaceId,
|
|
225
|
-
expected: `${contractFK.source.columns.join(', ')} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(', ')})`,
|
|
226
|
-
message: `Table "${tableName}" is missing foreign key: ${contractFK.source.columns.join(', ')} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(', ')})`,
|
|
227
|
-
};
|
|
228
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
229
|
-
tableControlPolicy,
|
|
230
|
-
issue,
|
|
231
|
-
{
|
|
232
|
-
status: 'fail',
|
|
233
|
-
kind: 'foreignKey',
|
|
234
|
-
name: `foreignKey(${contractFK.source.columns.join(', ')})`,
|
|
235
|
-
contractPath: fkPath,
|
|
236
|
-
code: 'foreign_key_mismatch',
|
|
237
|
-
message: 'Foreign key missing',
|
|
238
|
-
expected: contractFK,
|
|
239
|
-
actual: undefined,
|
|
240
|
-
children: [],
|
|
241
|
-
},
|
|
242
|
-
issues,
|
|
243
|
-
nodes,
|
|
244
|
-
);
|
|
245
|
-
} else {
|
|
246
|
-
const actionMismatches = getReferentialActionMismatches(contractFK, matchingFK);
|
|
247
|
-
if (actionMismatches.length > 0) {
|
|
248
|
-
const combinedMessage = actionMismatches.map((m) => m.message).join('; ');
|
|
249
|
-
const combinedExpected = actionMismatches.map((m) => m.expected).join(', ');
|
|
250
|
-
const combinedActual = actionMismatches.map((m) => m.actual).join(', ');
|
|
251
|
-
const issue: SchemaIssue = {
|
|
252
|
-
kind: 'foreign_key_mismatch',
|
|
253
|
-
table: tableName,
|
|
254
|
-
namespaceId,
|
|
255
|
-
indexOrConstraint: matchingFK.name ?? `fk(${contractFK.source.columns.join(',')})`,
|
|
256
|
-
expected: combinedExpected,
|
|
257
|
-
actual: combinedActual,
|
|
258
|
-
message: `Table "${tableName}" foreign key ${contractFK.source.columns.join(', ')} -> ${contractFK.target.tableName}: ${combinedMessage}`,
|
|
259
|
-
};
|
|
260
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
261
|
-
tableControlPolicy,
|
|
262
|
-
issue,
|
|
263
|
-
{
|
|
264
|
-
status: 'fail',
|
|
265
|
-
kind: 'foreignKey',
|
|
266
|
-
name: `foreignKey(${contractFK.source.columns.join(', ')})`,
|
|
267
|
-
contractPath: fkPath,
|
|
268
|
-
code: 'foreign_key_mismatch',
|
|
269
|
-
message: combinedMessage,
|
|
270
|
-
expected: contractFK,
|
|
271
|
-
actual: matchingFK,
|
|
272
|
-
children: [],
|
|
273
|
-
},
|
|
274
|
-
issues,
|
|
275
|
-
nodes,
|
|
276
|
-
);
|
|
277
|
-
} else {
|
|
278
|
-
nodes.push({
|
|
279
|
-
status: 'pass',
|
|
280
|
-
kind: 'foreignKey',
|
|
281
|
-
name: `foreignKey(${contractFK.source.columns.join(', ')})`,
|
|
282
|
-
contractPath: fkPath,
|
|
283
|
-
code: '',
|
|
284
|
-
message: '',
|
|
285
|
-
expected: undefined,
|
|
286
|
-
actual: undefined,
|
|
287
|
-
children: [],
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Check for extra FKs in strict mode
|
|
294
|
-
if (strict) {
|
|
295
|
-
for (const schemaFK of schemaFKs) {
|
|
296
|
-
const matchingFK = contractFKs.find((fk) => {
|
|
297
|
-
const tablesMatch =
|
|
298
|
-
schemaFK.referencedSchema !== undefined && fk.target.namespaceId !== UNBOUND_NAMESPACE_ID
|
|
299
|
-
? schemaFK.referencedSchema === fk.target.namespaceId &&
|
|
300
|
-
schemaFK.referencedTable === fk.target.tableName
|
|
301
|
-
: schemaFK.referencedTable === fk.target.tableName;
|
|
302
|
-
return (
|
|
303
|
-
arraysEqual(fk.source.columns, schemaFK.columns) &&
|
|
304
|
-
tablesMatch &&
|
|
305
|
-
arraysEqual(fk.target.columns, schemaFK.referencedColumns)
|
|
306
|
-
);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
if (!matchingFK) {
|
|
310
|
-
const issue: SchemaIssue = {
|
|
311
|
-
kind: 'extra_foreign_key',
|
|
312
|
-
table: tableName,
|
|
313
|
-
namespaceId,
|
|
314
|
-
indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(',')})`,
|
|
315
|
-
message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(', ')} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(', ')})`,
|
|
316
|
-
};
|
|
317
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
318
|
-
tableControlPolicy,
|
|
319
|
-
issue,
|
|
320
|
-
{
|
|
321
|
-
status: 'fail',
|
|
322
|
-
kind: 'foreignKey',
|
|
323
|
-
name: `foreignKey(${schemaFK.columns.join(', ')})`,
|
|
324
|
-
contractPath: `${tablePath}.foreignKeys[${schemaFK.columns.join(',')}]`,
|
|
325
|
-
code: 'extra_foreign_key',
|
|
326
|
-
message: 'Extra foreign key found',
|
|
327
|
-
expected: undefined,
|
|
328
|
-
actual: schemaFK,
|
|
329
|
-
children: [],
|
|
330
|
-
},
|
|
331
|
-
issues,
|
|
332
|
-
nodes,
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return nodes;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Verifies unique constraints match between contract and schema.
|
|
343
|
-
* Returns verification nodes for the tree.
|
|
344
|
-
*
|
|
345
|
-
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
346
|
-
* A unique constraint requirement can be satisfied by either:
|
|
347
|
-
* - A unique constraint with the same columns, or
|
|
348
|
-
* - A unique index with the same columns
|
|
349
|
-
*
|
|
350
|
-
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
351
|
-
*/
|
|
352
|
-
export function verifyUniqueConstraints(
|
|
353
|
-
contractUniques: readonly UniqueConstraint[],
|
|
354
|
-
schemaUniques: readonly SqlUniqueIR[],
|
|
355
|
-
schemaIndexes: readonly SqlIndexIR[],
|
|
356
|
-
tableName: string,
|
|
357
|
-
namespaceId: string,
|
|
358
|
-
tablePath: string,
|
|
359
|
-
tableControlPolicy: ControlPolicy,
|
|
360
|
-
issues: SchemaIssue[],
|
|
361
|
-
strict: boolean,
|
|
362
|
-
): SchemaVerificationNode[] {
|
|
363
|
-
const nodes: SchemaVerificationNode[] = [];
|
|
364
|
-
|
|
365
|
-
// Check each contract unique exists in schema
|
|
366
|
-
for (const contractUnique of contractUniques) {
|
|
367
|
-
const uniquePath = `${tablePath}.uniques[${contractUnique.columns.join(',')}]`;
|
|
368
|
-
|
|
369
|
-
// First check for a matching unique constraint
|
|
370
|
-
const matchingUnique = schemaUniques.find((u) =>
|
|
371
|
-
arraysEqual(u.columns, contractUnique.columns),
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
// If no matching constraint, check for a unique index with the same columns
|
|
375
|
-
const matchingUniqueIndex =
|
|
376
|
-
!matchingUnique &&
|
|
377
|
-
schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
|
|
378
|
-
|
|
379
|
-
if (!matchingUnique && !matchingUniqueIndex) {
|
|
380
|
-
const issue: SchemaIssue = {
|
|
381
|
-
kind: 'unique_constraint_mismatch',
|
|
382
|
-
table: tableName,
|
|
383
|
-
namespaceId,
|
|
384
|
-
expected: contractUnique.columns.join(', '),
|
|
385
|
-
message: `Table "${tableName}" is missing unique constraint: ${contractUnique.columns.join(', ')}`,
|
|
386
|
-
};
|
|
387
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
388
|
-
tableControlPolicy,
|
|
389
|
-
issue,
|
|
390
|
-
{
|
|
391
|
-
status: 'fail',
|
|
392
|
-
kind: 'unique',
|
|
393
|
-
name: `unique(${contractUnique.columns.join(', ')})`,
|
|
394
|
-
contractPath: uniquePath,
|
|
395
|
-
code: 'unique_constraint_mismatch',
|
|
396
|
-
message: 'Unique constraint missing',
|
|
397
|
-
expected: contractUnique,
|
|
398
|
-
actual: undefined,
|
|
399
|
-
children: [],
|
|
400
|
-
},
|
|
401
|
-
issues,
|
|
402
|
-
nodes,
|
|
403
|
-
);
|
|
404
|
-
} else {
|
|
405
|
-
nodes.push({
|
|
406
|
-
status: 'pass',
|
|
407
|
-
kind: 'unique',
|
|
408
|
-
name: `unique(${contractUnique.columns.join(', ')})`,
|
|
409
|
-
contractPath: uniquePath,
|
|
410
|
-
code: '',
|
|
411
|
-
message: '',
|
|
412
|
-
expected: undefined,
|
|
413
|
-
actual: undefined,
|
|
414
|
-
children: [],
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (strict) {
|
|
420
|
-
for (const schemaUnique of schemaUniques) {
|
|
421
|
-
const matchingUnique = contractUniques.find((u) =>
|
|
422
|
-
arraysEqual(u.columns, schemaUnique.columns),
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
if (!matchingUnique) {
|
|
426
|
-
const issue: SchemaIssue = {
|
|
427
|
-
kind: 'extra_unique_constraint',
|
|
428
|
-
table: tableName,
|
|
429
|
-
namespaceId,
|
|
430
|
-
indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(',')})`,
|
|
431
|
-
message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(', ')}`,
|
|
432
|
-
};
|
|
433
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
434
|
-
tableControlPolicy,
|
|
435
|
-
issue,
|
|
436
|
-
{
|
|
437
|
-
status: 'fail',
|
|
438
|
-
kind: 'unique',
|
|
439
|
-
name: `unique(${schemaUnique.columns.join(', ')})`,
|
|
440
|
-
contractPath: `${tablePath}.uniques[${schemaUnique.columns.join(',')}]`,
|
|
441
|
-
code: 'extra_unique_constraint',
|
|
442
|
-
message: 'Extra unique constraint found',
|
|
443
|
-
expected: undefined,
|
|
444
|
-
actual: schemaUnique,
|
|
445
|
-
children: [],
|
|
446
|
-
},
|
|
447
|
-
issues,
|
|
448
|
-
nodes,
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return nodes;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Verifies indexes match between contract and schema.
|
|
459
|
-
* Returns verification nodes for the tree.
|
|
460
|
-
*
|
|
461
|
-
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
462
|
-
* A non-unique index requirement can be satisfied by either:
|
|
463
|
-
* - A non-unique index with the same columns, or
|
|
464
|
-
* - A unique index with the same columns (stronger satisfies weaker)
|
|
465
|
-
*
|
|
466
|
-
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
467
|
-
*/
|
|
468
|
-
export function verifyIndexes(
|
|
469
|
-
contractIndexes: readonly Index[],
|
|
470
|
-
schemaIndexes: readonly SqlIndexIR[],
|
|
471
|
-
schemaUniques: readonly SqlUniqueIR[],
|
|
472
|
-
tableName: string,
|
|
473
|
-
namespaceId: string,
|
|
474
|
-
tablePath: string,
|
|
475
|
-
tableControlPolicy: ControlPolicy,
|
|
476
|
-
issues: SchemaIssue[],
|
|
477
|
-
strict: boolean,
|
|
478
|
-
): SchemaVerificationNode[] {
|
|
479
|
-
const nodes: SchemaVerificationNode[] = [];
|
|
480
|
-
|
|
481
|
-
// Check each contract index exists in schema
|
|
482
|
-
for (const contractIndex of contractIndexes) {
|
|
483
|
-
const indexPath = `${tablePath}.indexes[${contractIndex.columns.join(',')}]`;
|
|
484
|
-
|
|
485
|
-
// Check for any matching index (unique or non-unique)
|
|
486
|
-
// A unique index can satisfy a non-unique index requirement (stronger satisfies weaker)
|
|
487
|
-
const matchingIndex = schemaIndexes.find(
|
|
488
|
-
(idx) =>
|
|
489
|
-
arraysEqual(idx.columns, contractIndex.columns) && indexExtrasMatch(contractIndex, idx),
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
// Also check if a unique constraint satisfies the index requirement.
|
|
493
|
-
// Unique constraints carry no type/options of their own, so they can only
|
|
494
|
-
// satisfy a contract index that doesn't request a specific type/options.
|
|
495
|
-
const matchingUniqueConstraint =
|
|
496
|
-
!matchingIndex &&
|
|
497
|
-
contractIndex.type === undefined &&
|
|
498
|
-
contractIndex.options === undefined &&
|
|
499
|
-
schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
|
|
500
|
-
|
|
501
|
-
if (!matchingIndex && !matchingUniqueConstraint) {
|
|
502
|
-
const issue: SchemaIssue = {
|
|
503
|
-
kind: 'index_mismatch',
|
|
504
|
-
table: tableName,
|
|
505
|
-
namespaceId,
|
|
506
|
-
expected: contractIndex.columns.join(', '),
|
|
507
|
-
message: `Table "${tableName}" is missing index: ${contractIndex.columns.join(', ')}`,
|
|
508
|
-
};
|
|
509
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
510
|
-
tableControlPolicy,
|
|
511
|
-
issue,
|
|
512
|
-
{
|
|
513
|
-
status: 'fail',
|
|
514
|
-
kind: 'index',
|
|
515
|
-
name: `index(${contractIndex.columns.join(', ')})`,
|
|
516
|
-
contractPath: indexPath,
|
|
517
|
-
code: 'index_mismatch',
|
|
518
|
-
message: 'Index missing',
|
|
519
|
-
expected: contractIndex,
|
|
520
|
-
actual: undefined,
|
|
521
|
-
children: [],
|
|
522
|
-
},
|
|
523
|
-
issues,
|
|
524
|
-
nodes,
|
|
525
|
-
);
|
|
526
|
-
} else {
|
|
527
|
-
nodes.push({
|
|
528
|
-
status: 'pass',
|
|
529
|
-
kind: 'index',
|
|
530
|
-
name: `index(${contractIndex.columns.join(', ')})`,
|
|
531
|
-
contractPath: indexPath,
|
|
532
|
-
code: '',
|
|
533
|
-
message: '',
|
|
534
|
-
expected: undefined,
|
|
535
|
-
actual: undefined,
|
|
536
|
-
children: [],
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (strict) {
|
|
542
|
-
for (const schemaIndex of schemaIndexes) {
|
|
543
|
-
if (schemaIndex.unique) {
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const matchingIndex = contractIndexes.find(
|
|
548
|
-
(idx) =>
|
|
549
|
-
arraysEqual(idx.columns, schemaIndex.columns) && indexExtrasMatch(idx, schemaIndex),
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
if (!matchingIndex) {
|
|
553
|
-
const issue: SchemaIssue = {
|
|
554
|
-
kind: 'extra_index',
|
|
555
|
-
table: tableName,
|
|
556
|
-
namespaceId,
|
|
557
|
-
indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(',')})`,
|
|
558
|
-
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(', ')}`,
|
|
559
|
-
};
|
|
560
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
561
|
-
tableControlPolicy,
|
|
562
|
-
issue,
|
|
563
|
-
{
|
|
564
|
-
status: 'fail',
|
|
565
|
-
kind: 'index',
|
|
566
|
-
name: `index(${schemaIndex.columns.join(', ')})`,
|
|
567
|
-
contractPath: `${tablePath}.indexes[${schemaIndex.columns.join(',')}]`,
|
|
568
|
-
code: 'extra_index',
|
|
569
|
-
message: 'Extra index found',
|
|
570
|
-
expected: undefined,
|
|
571
|
-
actual: schemaIndex,
|
|
572
|
-
children: [],
|
|
573
|
-
},
|
|
574
|
-
issues,
|
|
575
|
-
nodes,
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return nodes;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Computes counts of pass/warn/fail nodes by traversing the tree.
|
|
586
|
-
*/
|
|
587
|
-
export function computeCounts(node: SchemaVerificationNode): {
|
|
588
|
-
pass: number;
|
|
589
|
-
warn: number;
|
|
590
|
-
fail: number;
|
|
591
|
-
totalNodes: number;
|
|
592
|
-
} {
|
|
593
|
-
let pass = 0;
|
|
594
|
-
let warn = 0;
|
|
595
|
-
let fail = 0;
|
|
596
|
-
|
|
597
|
-
function traverse(n: SchemaVerificationNode): void {
|
|
598
|
-
if (n.status === 'pass') {
|
|
599
|
-
pass++;
|
|
600
|
-
} else if (n.status === 'warn') {
|
|
601
|
-
warn++;
|
|
602
|
-
} else if (n.status === 'fail') {
|
|
603
|
-
fail++;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (n.children) {
|
|
607
|
-
for (const child of n.children) {
|
|
608
|
-
traverse(child);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
traverse(node);
|
|
614
|
-
|
|
615
|
-
return {
|
|
616
|
-
pass,
|
|
617
|
-
warn,
|
|
618
|
-
fail,
|
|
619
|
-
totalNodes: pass + warn + fail,
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Compares referential actions between a contract FK and a schema FK.
|
|
625
|
-
* Only compares when the contract FK explicitly specifies onDelete or onUpdate.
|
|
626
|
-
* Returns all mismatches (both onDelete and onUpdate) so both are reported at once.
|
|
627
|
-
*
|
|
628
|
-
* Note: 'noAction' in the contract is semantically equivalent to undefined in the
|
|
629
|
-
* schema IR, because the introspection adapter omits 'NO ACTION' (the database default)
|
|
630
|
-
* to keep the IR sparse. We normalize both sides before comparing.
|
|
631
|
-
*/
|
|
632
|
-
function getReferentialActionMismatches(
|
|
633
|
-
contractFK: ForeignKey,
|
|
634
|
-
schemaFK: SqlForeignKeyIR,
|
|
635
|
-
): ReadonlyArray<{ expected: string; actual: string; message: string }> {
|
|
636
|
-
const mismatches: Array<{ expected: string; actual: string; message: string }> = [];
|
|
637
|
-
|
|
638
|
-
const contractOnDelete = normalizeReferentialAction(contractFK.onDelete);
|
|
639
|
-
const schemaOnDelete = normalizeReferentialAction(schemaFK.onDelete);
|
|
640
|
-
if (contractOnDelete !== undefined && contractOnDelete !== schemaOnDelete) {
|
|
641
|
-
mismatches.push({
|
|
642
|
-
expected: `onDelete: ${contractFK.onDelete}`,
|
|
643
|
-
actual: `onDelete: ${schemaFK.onDelete ?? 'noAction (default)'}`,
|
|
644
|
-
message: `onDelete mismatch: expected ${contractFK.onDelete}, got ${schemaFK.onDelete ?? 'noAction (default)'}`,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const contractOnUpdate = normalizeReferentialAction(contractFK.onUpdate);
|
|
649
|
-
const schemaOnUpdate = normalizeReferentialAction(schemaFK.onUpdate);
|
|
650
|
-
if (contractOnUpdate !== undefined && contractOnUpdate !== schemaOnUpdate) {
|
|
651
|
-
mismatches.push({
|
|
652
|
-
expected: `onUpdate: ${contractFK.onUpdate}`,
|
|
653
|
-
actual: `onUpdate: ${schemaFK.onUpdate ?? 'noAction (default)'}`,
|
|
654
|
-
message: `onUpdate mismatch: expected ${contractFK.onUpdate}, got ${schemaFK.onUpdate ?? 'noAction (default)'}`,
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return mismatches;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Normalizes a referential action value for comparison.
|
|
663
|
-
* 'noAction' is the database default and equivalent to undefined (omitted) in the sparse IR.
|
|
664
|
-
*/
|
|
665
|
-
function normalizeReferentialAction(action: string | undefined): string | undefined {
|
|
666
|
-
return action === 'noAction' ? undefined : action;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* Compares two value arrays as unordered sets.
|
|
671
|
-
* Returns true when both sides contain exactly the same values.
|
|
672
|
-
*/
|
|
673
|
-
function valueSetsEqual(a: readonly string[], b: readonly string[]): boolean {
|
|
674
|
-
const aSet = new Set(a);
|
|
675
|
-
const bSet = new Set(b);
|
|
676
|
-
if (aSet.size !== bSet.size) return false;
|
|
677
|
-
return [...aSet].every((v) => bSet.has(v));
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Verifies check constraints match between contract-projected checks and
|
|
682
|
-
* introspected live checks.
|
|
683
|
-
*
|
|
684
|
-
* Comparison is value-set-based, not SQL-string-based. Postgres rewrites
|
|
685
|
-
* `col IN ('a','b')` as `col = ANY (ARRAY['a','b'])` in
|
|
686
|
-
* `pg_get_constraintdef`, so comparing the extracted value sets (after
|
|
687
|
-
* the introspection adapter parses the predicate) avoids false mismatches
|
|
688
|
-
* from the `IN`-vs-`= ANY (ARRAY…)` rendering difference.
|
|
689
|
-
*
|
|
690
|
-
* Issues emitted:
|
|
691
|
-
* - `check_missing` — check expected by contract but absent from live DB
|
|
692
|
-
* - `check_removed` — check present in live DB but not in contract
|
|
693
|
-
* - `check_mismatch` — check present on both sides but permitted values differ
|
|
694
|
-
*
|
|
695
|
-
* `check_removed` is emitted only when `strict` is true so non-strict
|
|
696
|
-
* verification (the normal path) does not complain about extra constraints.
|
|
697
|
-
*/
|
|
698
|
-
export function verifyCheckConstraints(
|
|
699
|
-
contractChecks: ReadonlyArray<{
|
|
700
|
-
readonly name: string;
|
|
701
|
-
readonly column: string;
|
|
702
|
-
readonly permittedValues: readonly string[];
|
|
703
|
-
}>,
|
|
704
|
-
schemaChecks: ReadonlyArray<SqlCheckConstraintIR>,
|
|
705
|
-
tableName: string,
|
|
706
|
-
namespaceId: string,
|
|
707
|
-
tablePath: string,
|
|
708
|
-
tableControlPolicy: ControlPolicy,
|
|
709
|
-
issues: SchemaIssue[],
|
|
710
|
-
strict: boolean,
|
|
711
|
-
): SchemaVerificationNode[] {
|
|
712
|
-
const nodes: SchemaVerificationNode[] = [];
|
|
713
|
-
|
|
714
|
-
for (const contractCheck of contractChecks) {
|
|
715
|
-
const checkPath = `${tablePath}.checks[${contractCheck.name}]`;
|
|
716
|
-
const liveCheck = schemaChecks.find((c) => c.name === contractCheck.name);
|
|
717
|
-
|
|
718
|
-
if (!liveCheck) {
|
|
719
|
-
const issue: SchemaIssue = {
|
|
720
|
-
kind: 'check_missing',
|
|
721
|
-
table: tableName,
|
|
722
|
-
namespaceId,
|
|
723
|
-
indexOrConstraint: contractCheck.name,
|
|
724
|
-
expected: contractCheck.permittedValues.join(', '),
|
|
725
|
-
message: `Table "${tableName}" is missing check constraint "${contractCheck.name}" (column "${contractCheck.column}" IN (${contractCheck.permittedValues.join(', ')}))`,
|
|
726
|
-
};
|
|
727
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
728
|
-
tableControlPolicy,
|
|
729
|
-
issue,
|
|
730
|
-
{
|
|
731
|
-
status: 'fail',
|
|
732
|
-
kind: 'checkConstraint',
|
|
733
|
-
name: `check(${contractCheck.name})`,
|
|
734
|
-
contractPath: checkPath,
|
|
735
|
-
code: 'check_missing',
|
|
736
|
-
message: `Check constraint "${contractCheck.name}" missing`,
|
|
737
|
-
expected: contractCheck,
|
|
738
|
-
actual: undefined,
|
|
739
|
-
children: [],
|
|
740
|
-
},
|
|
741
|
-
issues,
|
|
742
|
-
nodes,
|
|
743
|
-
);
|
|
744
|
-
} else if (!valueSetsEqual(contractCheck.permittedValues, liveCheck.permittedValues)) {
|
|
745
|
-
const issue: SchemaIssue = {
|
|
746
|
-
kind: 'check_mismatch',
|
|
747
|
-
table: tableName,
|
|
748
|
-
namespaceId,
|
|
749
|
-
indexOrConstraint: contractCheck.name,
|
|
750
|
-
expected: contractCheck.permittedValues.join(', '),
|
|
751
|
-
actual: liveCheck.permittedValues.join(', '),
|
|
752
|
-
message: `Table "${tableName}" check constraint "${contractCheck.name}" has different permitted values: expected [${contractCheck.permittedValues.join(', ')}], got [${liveCheck.permittedValues.join(', ')}]`,
|
|
753
|
-
};
|
|
754
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
755
|
-
tableControlPolicy,
|
|
756
|
-
issue,
|
|
757
|
-
{
|
|
758
|
-
status: 'fail',
|
|
759
|
-
kind: 'checkConstraint',
|
|
760
|
-
name: `check(${contractCheck.name})`,
|
|
761
|
-
contractPath: checkPath,
|
|
762
|
-
code: 'check_mismatch',
|
|
763
|
-
message: `Check constraint "${contractCheck.name}" values mismatch`,
|
|
764
|
-
expected: contractCheck,
|
|
765
|
-
actual: liveCheck,
|
|
766
|
-
children: [],
|
|
767
|
-
},
|
|
768
|
-
issues,
|
|
769
|
-
nodes,
|
|
770
|
-
);
|
|
771
|
-
} else {
|
|
772
|
-
nodes.push({
|
|
773
|
-
status: 'pass',
|
|
774
|
-
kind: 'checkConstraint',
|
|
775
|
-
name: `check(${contractCheck.name})`,
|
|
776
|
-
contractPath: checkPath,
|
|
777
|
-
code: '',
|
|
778
|
-
message: '',
|
|
779
|
-
expected: undefined,
|
|
780
|
-
actual: undefined,
|
|
781
|
-
children: [],
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (strict) {
|
|
787
|
-
for (const liveCheck of schemaChecks) {
|
|
788
|
-
const matchingContract = contractChecks.find((c) => c.name === liveCheck.name);
|
|
789
|
-
if (!matchingContract) {
|
|
790
|
-
const issue: SchemaIssue = {
|
|
791
|
-
kind: 'check_removed',
|
|
792
|
-
table: tableName,
|
|
793
|
-
namespaceId,
|
|
794
|
-
indexOrConstraint: liveCheck.name,
|
|
795
|
-
actual: liveCheck.permittedValues.join(', '),
|
|
796
|
-
message: `Table "${tableName}" has extra check constraint "${liveCheck.name}" in database (not in contract)`,
|
|
797
|
-
};
|
|
798
|
-
emitIssueAndNodeUnderControlPolicy(
|
|
799
|
-
tableControlPolicy,
|
|
800
|
-
issue,
|
|
801
|
-
{
|
|
802
|
-
status: 'fail',
|
|
803
|
-
kind: 'checkConstraint',
|
|
804
|
-
name: `check(${liveCheck.name})`,
|
|
805
|
-
contractPath: `${tablePath}.checks[${liveCheck.name}]`,
|
|
806
|
-
code: 'check_removed',
|
|
807
|
-
message: `Extra check constraint "${liveCheck.name}" found`,
|
|
808
|
-
expected: undefined,
|
|
809
|
-
actual: liveCheck,
|
|
810
|
-
children: [],
|
|
811
|
-
},
|
|
812
|
-
issues,
|
|
813
|
-
nodes,
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
return nodes;
|
|
820
|
-
}
|