@prisma-next/family-sql 0.12.0 → 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-r7BP5n3l.mjs → timestamp-now-generator-CloimujU.mjs} +2 -1
- package/dist/{timestamp-now-generator-r7BP5n3l.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/core/timestamp-now-generator.ts +1 -0
- 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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
SchemaIssue,
|
|
4
|
+
SchemaVerificationNode,
|
|
5
|
+
VerifierOutcome,
|
|
6
|
+
} from '@prisma-next/framework-components/control';
|
|
7
|
+
import { verifierDisposition } from './verifier-disposition';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Grades `issue` under `controlPolicy` and, unless suppressed, pushes both the
|
|
11
|
+
* issue and a status-stamped verification node. Returns the resolved outcome so
|
|
12
|
+
* the caller never re-grades the same issue.
|
|
13
|
+
*/
|
|
14
|
+
export function emitIssueAndNodeUnderControlPolicy(
|
|
15
|
+
controlPolicy: ControlPolicy,
|
|
16
|
+
issue: SchemaIssue,
|
|
17
|
+
node: SchemaVerificationNode,
|
|
18
|
+
issues: SchemaIssue[],
|
|
19
|
+
nodes: SchemaVerificationNode[],
|
|
20
|
+
): VerifierOutcome {
|
|
21
|
+
const disposition = verifierDisposition(controlPolicy, issue.kind);
|
|
22
|
+
if (disposition === 'suppress') {
|
|
23
|
+
return disposition;
|
|
24
|
+
}
|
|
25
|
+
issues.push(issue);
|
|
26
|
+
nodes.push({ ...node, status: disposition });
|
|
27
|
+
return disposition;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Grades `issue` under `controlPolicy` and, unless suppressed, pushes the issue
|
|
32
|
+
* (no verification node). Returns the resolved outcome so the caller maps it to
|
|
33
|
+
* a node status itself without re-grading.
|
|
34
|
+
*/
|
|
35
|
+
export function emitIssueUnderControlPolicy(
|
|
36
|
+
controlPolicy: ControlPolicy,
|
|
37
|
+
issue: SchemaIssue,
|
|
38
|
+
issues: SchemaIssue[],
|
|
39
|
+
): VerifierOutcome {
|
|
40
|
+
const disposition = verifierDisposition(controlPolicy, issue.kind);
|
|
41
|
+
if (disposition === 'suppress') {
|
|
42
|
+
return disposition;
|
|
43
|
+
}
|
|
44
|
+
issues.push(issue);
|
|
45
|
+
return disposition;
|
|
46
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
SchemaIssue,
|
|
4
|
+
VerifierIssueCategory,
|
|
5
|
+
VerifierOutcome,
|
|
6
|
+
} from '@prisma-next/framework-components/control';
|
|
7
|
+
import { dispositionForCategory } from '@prisma-next/framework-components/control';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Classifies the relational verifier issue kinds the SQL family emits (tables,
|
|
11
|
+
* columns, constraints, indexes, defaults, enum types) into the target-neutral
|
|
12
|
+
* categories the framework grades. The relational vocabulary lives here, in the
|
|
13
|
+
* SQL domain — the framework never switches over `extra_foreign_key` and friends.
|
|
14
|
+
*/
|
|
15
|
+
export function classifySqlVerifierIssueKind(kind: SchemaIssue['kind']): VerifierIssueCategory {
|
|
16
|
+
switch (kind) {
|
|
17
|
+
case 'extra_column':
|
|
18
|
+
return 'extraNestedElement';
|
|
19
|
+
case 'extra_primary_key':
|
|
20
|
+
case 'extra_foreign_key':
|
|
21
|
+
case 'extra_unique_constraint':
|
|
22
|
+
case 'extra_index':
|
|
23
|
+
case 'extra_validator':
|
|
24
|
+
case 'extra_default':
|
|
25
|
+
return 'extraAuxiliary';
|
|
26
|
+
case 'extra_table':
|
|
27
|
+
return 'extraTopLevelObject';
|
|
28
|
+
case 'missing_schema':
|
|
29
|
+
case 'missing_table':
|
|
30
|
+
case 'missing_column':
|
|
31
|
+
case 'type_missing':
|
|
32
|
+
case 'default_missing':
|
|
33
|
+
return 'declaredMissing';
|
|
34
|
+
case 'type_values_mismatch':
|
|
35
|
+
case 'enum_values_changed':
|
|
36
|
+
case 'check_mismatch':
|
|
37
|
+
return 'valueDrift';
|
|
38
|
+
case 'type_mismatch':
|
|
39
|
+
case 'nullability_mismatch':
|
|
40
|
+
case 'primary_key_mismatch':
|
|
41
|
+
case 'foreign_key_mismatch':
|
|
42
|
+
case 'unique_constraint_mismatch':
|
|
43
|
+
case 'index_mismatch':
|
|
44
|
+
case 'default_mismatch':
|
|
45
|
+
return 'declaredIncompatible';
|
|
46
|
+
case 'check_missing':
|
|
47
|
+
return 'declaredMissing';
|
|
48
|
+
case 'check_removed':
|
|
49
|
+
return 'extraAuxiliary';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function verifierDisposition(
|
|
54
|
+
controlPolicy: ControlPolicy,
|
|
55
|
+
issueKind: SchemaIssue['kind'],
|
|
56
|
+
): VerifierOutcome {
|
|
57
|
+
return dispositionForCategory(controlPolicy, classifySqlVerifierIssueKind(issueKind));
|
|
58
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* These functions verify schema IR against contract requirements.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
6
7
|
import type {
|
|
7
8
|
SchemaIssue,
|
|
8
9
|
SchemaVerificationNode,
|
|
@@ -14,7 +15,16 @@ import type {
|
|
|
14
15
|
PrimaryKey,
|
|
15
16
|
UniqueConstraint,
|
|
16
17
|
} from '@prisma-next/sql-contract/types';
|
|
17
|
-
import type {
|
|
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';
|
|
18
28
|
|
|
19
29
|
function indexOptionsLooselyEqual(
|
|
20
30
|
a: Record<string, unknown> | undefined,
|
|
@@ -135,34 +145,34 @@ export function verifyPrimaryKey(
|
|
|
135
145
|
schemaPK: PrimaryKey | undefined,
|
|
136
146
|
tableName: string,
|
|
137
147
|
namespaceId: string,
|
|
148
|
+
tableControlPolicy: ControlPolicy,
|
|
138
149
|
issues: SchemaIssue[],
|
|
139
|
-
): 'pass' | 'fail' {
|
|
150
|
+
): 'pass' | 'warn' | 'fail' {
|
|
140
151
|
if (!schemaPK) {
|
|
141
|
-
|
|
152
|
+
const issue: SchemaIssue = {
|
|
142
153
|
kind: 'primary_key_mismatch',
|
|
143
154
|
table: tableName,
|
|
144
155
|
namespaceId,
|
|
145
156
|
expected: contractPK.columns.join(', '),
|
|
146
157
|
message: `Table "${tableName}" is missing primary key`,
|
|
147
|
-
}
|
|
148
|
-
|
|
158
|
+
};
|
|
159
|
+
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, issue, issues);
|
|
160
|
+
return outcome === 'suppress' ? 'pass' : outcome;
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
|
|
152
|
-
|
|
164
|
+
const issue: SchemaIssue = {
|
|
153
165
|
kind: 'primary_key_mismatch',
|
|
154
166
|
table: tableName,
|
|
155
167
|
namespaceId,
|
|
156
168
|
expected: contractPK.columns.join(', '),
|
|
157
169
|
actual: schemaPK.columns.join(', '),
|
|
158
170
|
message: `Table "${tableName}" has primary key mismatch: expected columns [${contractPK.columns.join(', ')}], got [${schemaPK.columns.join(', ')}]`,
|
|
159
|
-
}
|
|
160
|
-
|
|
171
|
+
};
|
|
172
|
+
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, issue, issues);
|
|
173
|
+
return outcome === 'suppress' ? 'pass' : outcome;
|
|
161
174
|
}
|
|
162
175
|
|
|
163
|
-
// Name differences are ignored for semantic satisfaction.
|
|
164
|
-
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
165
|
-
|
|
166
176
|
return 'pass';
|
|
167
177
|
}
|
|
168
178
|
|
|
@@ -179,6 +189,7 @@ export function verifyForeignKeys(
|
|
|
179
189
|
tableName: string,
|
|
180
190
|
namespaceId: string,
|
|
181
191
|
tablePath: string,
|
|
192
|
+
tableControlPolicy: ControlPolicy,
|
|
182
193
|
issues: SchemaIssue[],
|
|
183
194
|
strict: boolean,
|
|
184
195
|
): SchemaVerificationNode[] {
|
|
@@ -207,52 +218,62 @@ export function verifyForeignKeys(
|
|
|
207
218
|
});
|
|
208
219
|
|
|
209
220
|
if (!matchingFK) {
|
|
210
|
-
|
|
221
|
+
const issue: SchemaIssue = {
|
|
211
222
|
kind: 'foreign_key_mismatch',
|
|
212
223
|
table: tableName,
|
|
213
224
|
namespaceId,
|
|
214
225
|
expected: `${contractFK.source.columns.join(', ')} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(', ')})`,
|
|
215
226
|
message: `Table "${tableName}" is missing foreign key: ${contractFK.source.columns.join(', ')} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(', ')})`,
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
);
|
|
228
245
|
} else {
|
|
229
246
|
const actionMismatches = getReferentialActionMismatches(contractFK, matchingFK);
|
|
230
247
|
if (actionMismatches.length > 0) {
|
|
231
248
|
const combinedMessage = actionMismatches.map((m) => m.message).join('; ');
|
|
232
249
|
const combinedExpected = actionMismatches.map((m) => m.expected).join(', ');
|
|
233
250
|
const combinedActual = actionMismatches.map((m) => m.actual).join(', ');
|
|
234
|
-
|
|
251
|
+
const issue: SchemaIssue = {
|
|
235
252
|
kind: 'foreign_key_mismatch',
|
|
236
253
|
table: tableName,
|
|
237
254
|
namespaceId,
|
|
238
|
-
// Set indexOrConstraint so the planner classifies this as a non-additive
|
|
239
|
-
// conflict (existing FK with wrong actions cannot be fixed additively).
|
|
240
255
|
indexOrConstraint: matchingFK.name ?? `fk(${contractFK.source.columns.join(',')})`,
|
|
241
256
|
expected: combinedExpected,
|
|
242
257
|
actual: combinedActual,
|
|
243
258
|
message: `Table "${tableName}" foreign key ${contractFK.source.columns.join(', ')} -> ${contractFK.target.tableName}: ${combinedMessage}`,
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
);
|
|
256
277
|
} else {
|
|
257
278
|
nodes.push({
|
|
258
279
|
status: 'pass',
|
|
@@ -286,24 +307,30 @@ export function verifyForeignKeys(
|
|
|
286
307
|
});
|
|
287
308
|
|
|
288
309
|
if (!matchingFK) {
|
|
289
|
-
|
|
310
|
+
const issue: SchemaIssue = {
|
|
290
311
|
kind: 'extra_foreign_key',
|
|
291
312
|
table: tableName,
|
|
292
313
|
namespaceId,
|
|
293
314
|
indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(',')})`,
|
|
294
315
|
message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(', ')} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(', ')})`,
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
);
|
|
307
334
|
}
|
|
308
335
|
}
|
|
309
336
|
}
|
|
@@ -329,6 +356,7 @@ export function verifyUniqueConstraints(
|
|
|
329
356
|
tableName: string,
|
|
330
357
|
namespaceId: string,
|
|
331
358
|
tablePath: string,
|
|
359
|
+
tableControlPolicy: ControlPolicy,
|
|
332
360
|
issues: SchemaIssue[],
|
|
333
361
|
strict: boolean,
|
|
334
362
|
): SchemaVerificationNode[] {
|
|
@@ -349,27 +377,31 @@ export function verifyUniqueConstraints(
|
|
|
349
377
|
schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
|
|
350
378
|
|
|
351
379
|
if (!matchingUnique && !matchingUniqueIndex) {
|
|
352
|
-
|
|
380
|
+
const issue: SchemaIssue = {
|
|
353
381
|
kind: 'unique_constraint_mismatch',
|
|
354
382
|
table: tableName,
|
|
355
383
|
namespaceId,
|
|
356
384
|
expected: contractUnique.columns.join(', '),
|
|
357
385
|
message: `Table "${tableName}" is missing unique constraint: ${contractUnique.columns.join(', ')}`,
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
);
|
|
370
404
|
} else {
|
|
371
|
-
// Name differences are ignored for semantic satisfaction.
|
|
372
|
-
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
373
405
|
nodes.push({
|
|
374
406
|
status: 'pass',
|
|
375
407
|
kind: 'unique',
|
|
@@ -384,7 +416,6 @@ export function verifyUniqueConstraints(
|
|
|
384
416
|
}
|
|
385
417
|
}
|
|
386
418
|
|
|
387
|
-
// Check for extra uniques in strict mode
|
|
388
419
|
if (strict) {
|
|
389
420
|
for (const schemaUnique of schemaUniques) {
|
|
390
421
|
const matchingUnique = contractUniques.find((u) =>
|
|
@@ -392,24 +423,30 @@ export function verifyUniqueConstraints(
|
|
|
392
423
|
);
|
|
393
424
|
|
|
394
425
|
if (!matchingUnique) {
|
|
395
|
-
|
|
426
|
+
const issue: SchemaIssue = {
|
|
396
427
|
kind: 'extra_unique_constraint',
|
|
397
428
|
table: tableName,
|
|
398
429
|
namespaceId,
|
|
399
430
|
indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(',')})`,
|
|
400
431
|
message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(', ')}`,
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
+
);
|
|
413
450
|
}
|
|
414
451
|
}
|
|
415
452
|
}
|
|
@@ -435,6 +472,7 @@ export function verifyIndexes(
|
|
|
435
472
|
tableName: string,
|
|
436
473
|
namespaceId: string,
|
|
437
474
|
tablePath: string,
|
|
475
|
+
tableControlPolicy: ControlPolicy,
|
|
438
476
|
issues: SchemaIssue[],
|
|
439
477
|
strict: boolean,
|
|
440
478
|
): SchemaVerificationNode[] {
|
|
@@ -461,27 +499,31 @@ export function verifyIndexes(
|
|
|
461
499
|
schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
|
|
462
500
|
|
|
463
501
|
if (!matchingIndex && !matchingUniqueConstraint) {
|
|
464
|
-
|
|
502
|
+
const issue: SchemaIssue = {
|
|
465
503
|
kind: 'index_mismatch',
|
|
466
504
|
table: tableName,
|
|
467
505
|
namespaceId,
|
|
468
506
|
expected: contractIndex.columns.join(', '),
|
|
469
507
|
message: `Table "${tableName}" is missing index: ${contractIndex.columns.join(', ')}`,
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
);
|
|
482
526
|
} else {
|
|
483
|
-
// Name differences are ignored for semantic satisfaction.
|
|
484
|
-
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
485
527
|
nodes.push({
|
|
486
528
|
status: 'pass',
|
|
487
529
|
kind: 'index',
|
|
@@ -496,10 +538,8 @@ export function verifyIndexes(
|
|
|
496
538
|
}
|
|
497
539
|
}
|
|
498
540
|
|
|
499
|
-
// Check for extra indexes in strict mode
|
|
500
541
|
if (strict) {
|
|
501
542
|
for (const schemaIndex of schemaIndexes) {
|
|
502
|
-
// Skip unique indexes (they're handled as unique constraints)
|
|
503
543
|
if (schemaIndex.unique) {
|
|
504
544
|
continue;
|
|
505
545
|
}
|
|
@@ -510,24 +550,30 @@ export function verifyIndexes(
|
|
|
510
550
|
);
|
|
511
551
|
|
|
512
552
|
if (!matchingIndex) {
|
|
513
|
-
|
|
553
|
+
const issue: SchemaIssue = {
|
|
514
554
|
kind: 'extra_index',
|
|
515
555
|
table: tableName,
|
|
516
556
|
namespaceId,
|
|
517
557
|
indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(',')})`,
|
|
518
558
|
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(', ')}`,
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
+
);
|
|
531
577
|
}
|
|
532
578
|
}
|
|
533
579
|
}
|
|
@@ -619,3 +665,156 @@ function getReferentialActionMismatches(
|
|
|
619
665
|
function normalizeReferentialAction(action: string | undefined): string | undefined {
|
|
620
666
|
return action === 'noAction' ? undefined : action;
|
|
621
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
|
+
}
|