@prisma-next/family-sql 0.12.0-dev.2 → 0.12.0-dev.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/control-adapter-8tV9WgWK.d.mts +134 -0
- package/dist/control-adapter-8tV9WgWK.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +2 -109
- package/dist/control.d.mts +31 -3
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +62 -14
- package/dist/control.mjs.map +1 -1
- package/dist/migration.d.mts +1 -1
- package/dist/runtime.d.mts +3 -1
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +3 -1
- package/dist/runtime.mjs.map +1 -1
- package/dist/schema-verify.d.mts +2 -1
- package/dist/schema-verify.d.mts.map +1 -1
- package/dist/schema-verify.mjs +1 -1
- package/dist/{types-CeeCStqw.d.mts → types-BQiqw6kR.d.mts} +14 -5
- package/dist/types-BQiqw6kR.d.mts.map +1 -0
- package/dist/{verify-sql-schema-CN7pPoTC.d.mts → verify-sql-schema-Cj3c2t5Z.d.mts} +8 -2
- package/dist/verify-sql-schema-Cj3c2t5Z.d.mts.map +1 -0
- package/dist/{verify-sql-schema-CYLsGCFO.mjs → verify-sql-schema-fljAiO-C.mjs} +403 -310
- package/dist/verify-sql-schema-fljAiO-C.mjs.map +1 -0
- package/package.json +21 -21
- package/src/core/control-adapter.ts +44 -3
- package/src/core/control-instance.ts +40 -41
- package/src/core/default-namespace.ts +9 -0
- package/src/core/migrations/control-policy.ts +89 -0
- package/src/core/migrations/types.ts +8 -1
- package/src/core/schema-verify/control-verify-emit.ts +46 -0
- package/src/core/schema-verify/verifier-disposition.ts +53 -0
- package/src/core/schema-verify/verify-helpers.ts +151 -110
- package/src/core/schema-verify/verify-sql-schema.ts +291 -155
- package/src/exports/control.ts +2 -0
- package/src/exports/runtime.ts +7 -0
- package/dist/control-adapter.d.mts.map +0 -1
- package/dist/types-CeeCStqw.d.mts.map +0 -1
- package/dist/verify-sql-schema-CN7pPoTC.d.mts.map +0 -1
- package/dist/verify-sql-schema-CYLsGCFO.mjs.map +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Contract,
|
|
3
|
+
type ControlPolicy,
|
|
4
|
+
effectiveControlPolicy,
|
|
5
|
+
} from '@prisma-next/contract/types';
|
|
6
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The target object a control policy governs for a single planner call,
|
|
10
|
+
* resolved from the target's own IR. `undefined` means the call's target
|
|
11
|
+
* object could not be positively established — a fail-closed signal: any
|
|
12
|
+
* policy stricter than `managed` drops such a call rather than emitting it.
|
|
13
|
+
*/
|
|
14
|
+
export interface ControlPolicySubject {
|
|
15
|
+
readonly namespaceId: string;
|
|
16
|
+
readonly explicitNodeControlPolicy?: ControlPolicy;
|
|
17
|
+
readonly table?: string;
|
|
18
|
+
readonly column?: string;
|
|
19
|
+
readonly typeName?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the call creates a whole, previously-absent top-level storage
|
|
22
|
+
* object (e.g. a table or an enum/type), as opposed to modifying an
|
|
23
|
+
* existing object. This is the only thing `tolerated` permits: it is a
|
|
24
|
+
* create-if-absent policy, so an op that touches an existing object — add
|
|
25
|
+
* column, add index/constraint, alter, drop — is never allowed under it.
|
|
26
|
+
*/
|
|
27
|
+
readonly createsNewObject: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The control policy that governs a single call. The `external` default is an
|
|
32
|
+
* un-overridable namespace floor: when the contract default is `external`, no
|
|
33
|
+
* per-object `managed` override can escalate DDL above the floor, so the
|
|
34
|
+
* policy is forced to `external` regardless of the node's own declaration.
|
|
35
|
+
* Every other default defers to the node's effective control policy.
|
|
36
|
+
*/
|
|
37
|
+
function controlPolicyForCall(
|
|
38
|
+
subject: ControlPolicySubject | undefined,
|
|
39
|
+
defaultControlPolicy: ControlPolicy | undefined,
|
|
40
|
+
): ControlPolicy {
|
|
41
|
+
if (defaultControlPolicy === 'external') {
|
|
42
|
+
return 'external';
|
|
43
|
+
}
|
|
44
|
+
return effectiveControlPolicy(subject?.explicitNodeControlPolicy, defaultControlPolicy);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a call is allowed to emit under a given control policy.
|
|
49
|
+
*
|
|
50
|
+
* - `managed` — full lifecycle, every op allowed.
|
|
51
|
+
* - `tolerated` — create-if-absent only: allowed iff the call creates a whole
|
|
52
|
+
* new top-level object (and its subject was positively resolved). Anything
|
|
53
|
+
* that modifies an existing object, and anything whose subject could not be
|
|
54
|
+
* resolved, is suppressed.
|
|
55
|
+
* - `external` / `observed` — no DDL at all.
|
|
56
|
+
*/
|
|
57
|
+
function callAllowedUnderControlPolicy(
|
|
58
|
+
policy: ControlPolicy,
|
|
59
|
+
subject: ControlPolicySubject | undefined,
|
|
60
|
+
): boolean {
|
|
61
|
+
switch (policy) {
|
|
62
|
+
case 'managed':
|
|
63
|
+
return true;
|
|
64
|
+
case 'tolerated':
|
|
65
|
+
return subject?.createsNewObject === true;
|
|
66
|
+
case 'external':
|
|
67
|
+
case 'observed':
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function filterCallsByControlPolicy<TCall>(options: {
|
|
73
|
+
readonly calls: readonly TCall[];
|
|
74
|
+
readonly contract: Contract<SqlStorage>;
|
|
75
|
+
readonly resolveControlPolicySubject: (call: TCall) => ControlPolicySubject | undefined;
|
|
76
|
+
}): readonly TCall[] {
|
|
77
|
+
const defaultControlPolicy = options.contract.defaultControlPolicy;
|
|
78
|
+
const kept: TCall[] = [];
|
|
79
|
+
|
|
80
|
+
for (const call of options.calls) {
|
|
81
|
+
const subject = options.resolveControlPolicySubject(call);
|
|
82
|
+
const policy = controlPolicyForCall(subject, defaultControlPolicy);
|
|
83
|
+
if (callAllowedUnderControlPolicy(policy, subject)) {
|
|
84
|
+
kept.push(call);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Object.freeze(kept);
|
|
89
|
+
}
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
SchemaIssue,
|
|
23
23
|
SchemaVerifier,
|
|
24
24
|
} from '@prisma-next/framework-components/control';
|
|
25
|
+
import type { AggregateMigrationEdgeRef } from '@prisma-next/migration-tools/aggregate';
|
|
25
26
|
import type {
|
|
26
27
|
SqlStorage,
|
|
27
28
|
StorageColumn,
|
|
@@ -31,6 +32,7 @@ import type {
|
|
|
31
32
|
import type { SqlOperationDescriptors } from '@prisma-next/sql-operations';
|
|
32
33
|
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
33
34
|
import type { Result } from '@prisma-next/utils/result';
|
|
35
|
+
import type { SqlControlAdapter } from '../control-adapter';
|
|
34
36
|
import type { SqlControlFamilyInstance } from '../control-instance';
|
|
35
37
|
|
|
36
38
|
export type AnyRecord = Readonly<Record<string, unknown>>;
|
|
@@ -174,7 +176,7 @@ export interface SqlControlExtensionDescriptor<TTargetId extends string>
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
export interface SqlControlAdapterDescriptor<TTargetId extends string>
|
|
177
|
-
extends ControlAdapterDescriptor<'sql', TTargetId
|
|
179
|
+
extends ControlAdapterDescriptor<'sql', TTargetId, SqlControlAdapter<TTargetId>> {
|
|
178
180
|
readonly queryOperations?: () => SqlOperationDescriptors;
|
|
179
181
|
}
|
|
180
182
|
|
|
@@ -384,6 +386,11 @@ export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
|
|
|
384
386
|
* All components must have matching familyId ('sql') and targetId.
|
|
385
387
|
*/
|
|
386
388
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
389
|
+
/**
|
|
390
|
+
* Per-edge breakdown from graph-walk planning. When present, the runner
|
|
391
|
+
* writes one ledger row per edge instead of one collapsed row per apply.
|
|
392
|
+
*/
|
|
393
|
+
readonly migrationEdges: readonly AggregateMigrationEdgeRef[];
|
|
387
394
|
}
|
|
388
395
|
|
|
389
396
|
export type SqlMigrationRunnerErrorCode =
|
|
@@ -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,53 @@
|
|
|
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
|
+
return 'valueDrift';
|
|
37
|
+
case 'type_mismatch':
|
|
38
|
+
case 'nullability_mismatch':
|
|
39
|
+
case 'primary_key_mismatch':
|
|
40
|
+
case 'foreign_key_mismatch':
|
|
41
|
+
case 'unique_constraint_mismatch':
|
|
42
|
+
case 'index_mismatch':
|
|
43
|
+
case 'default_mismatch':
|
|
44
|
+
return 'declaredIncompatible';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function verifierDisposition(
|
|
49
|
+
controlPolicy: ControlPolicy,
|
|
50
|
+
issueKind: SchemaIssue['kind'],
|
|
51
|
+
): VerifierOutcome {
|
|
52
|
+
return dispositionForCategory(controlPolicy, classifySqlVerifierIssueKind(issueKind));
|
|
53
|
+
}
|
|
@@ -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,
|
|
@@ -15,6 +16,10 @@ import type {
|
|
|
15
16
|
UniqueConstraint,
|
|
16
17
|
} from '@prisma-next/sql-contract/types';
|
|
17
18
|
import type { SqlForeignKeyIR, SqlIndexIR, SqlUniqueIR } from '@prisma-next/sql-schema-ir/types';
|
|
19
|
+
import {
|
|
20
|
+
emitIssueAndNodeUnderControlPolicy,
|
|
21
|
+
emitIssueUnderControlPolicy,
|
|
22
|
+
} from './control-verify-emit';
|
|
18
23
|
|
|
19
24
|
function indexOptionsLooselyEqual(
|
|
20
25
|
a: Record<string, unknown> | undefined,
|
|
@@ -135,34 +140,34 @@ export function verifyPrimaryKey(
|
|
|
135
140
|
schemaPK: PrimaryKey | undefined,
|
|
136
141
|
tableName: string,
|
|
137
142
|
namespaceId: string,
|
|
143
|
+
tableControlPolicy: ControlPolicy,
|
|
138
144
|
issues: SchemaIssue[],
|
|
139
|
-
): 'pass' | 'fail' {
|
|
145
|
+
): 'pass' | 'warn' | 'fail' {
|
|
140
146
|
if (!schemaPK) {
|
|
141
|
-
|
|
147
|
+
const issue: SchemaIssue = {
|
|
142
148
|
kind: 'primary_key_mismatch',
|
|
143
149
|
table: tableName,
|
|
144
150
|
namespaceId,
|
|
145
151
|
expected: contractPK.columns.join(', '),
|
|
146
152
|
message: `Table "${tableName}" is missing primary key`,
|
|
147
|
-
}
|
|
148
|
-
|
|
153
|
+
};
|
|
154
|
+
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, issue, issues);
|
|
155
|
+
return outcome === 'suppress' ? 'pass' : outcome;
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
|
|
152
|
-
|
|
159
|
+
const issue: SchemaIssue = {
|
|
153
160
|
kind: 'primary_key_mismatch',
|
|
154
161
|
table: tableName,
|
|
155
162
|
namespaceId,
|
|
156
163
|
expected: contractPK.columns.join(', '),
|
|
157
164
|
actual: schemaPK.columns.join(', '),
|
|
158
165
|
message: `Table "${tableName}" has primary key mismatch: expected columns [${contractPK.columns.join(', ')}], got [${schemaPK.columns.join(', ')}]`,
|
|
159
|
-
}
|
|
160
|
-
|
|
166
|
+
};
|
|
167
|
+
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, issue, issues);
|
|
168
|
+
return outcome === 'suppress' ? 'pass' : outcome;
|
|
161
169
|
}
|
|
162
170
|
|
|
163
|
-
// Name differences are ignored for semantic satisfaction.
|
|
164
|
-
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
165
|
-
|
|
166
171
|
return 'pass';
|
|
167
172
|
}
|
|
168
173
|
|
|
@@ -179,6 +184,7 @@ export function verifyForeignKeys(
|
|
|
179
184
|
tableName: string,
|
|
180
185
|
namespaceId: string,
|
|
181
186
|
tablePath: string,
|
|
187
|
+
tableControlPolicy: ControlPolicy,
|
|
182
188
|
issues: SchemaIssue[],
|
|
183
189
|
strict: boolean,
|
|
184
190
|
): SchemaVerificationNode[] {
|
|
@@ -207,52 +213,62 @@ export function verifyForeignKeys(
|
|
|
207
213
|
});
|
|
208
214
|
|
|
209
215
|
if (!matchingFK) {
|
|
210
|
-
|
|
216
|
+
const issue: SchemaIssue = {
|
|
211
217
|
kind: 'foreign_key_mismatch',
|
|
212
218
|
table: tableName,
|
|
213
219
|
namespaceId,
|
|
214
220
|
expected: `${contractFK.source.columns.join(', ')} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(', ')})`,
|
|
215
221
|
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
|
-
|
|
222
|
+
};
|
|
223
|
+
emitIssueAndNodeUnderControlPolicy(
|
|
224
|
+
tableControlPolicy,
|
|
225
|
+
issue,
|
|
226
|
+
{
|
|
227
|
+
status: 'fail',
|
|
228
|
+
kind: 'foreignKey',
|
|
229
|
+
name: `foreignKey(${contractFK.source.columns.join(', ')})`,
|
|
230
|
+
contractPath: fkPath,
|
|
231
|
+
code: 'foreign_key_mismatch',
|
|
232
|
+
message: 'Foreign key missing',
|
|
233
|
+
expected: contractFK,
|
|
234
|
+
actual: undefined,
|
|
235
|
+
children: [],
|
|
236
|
+
},
|
|
237
|
+
issues,
|
|
238
|
+
nodes,
|
|
239
|
+
);
|
|
228
240
|
} else {
|
|
229
241
|
const actionMismatches = getReferentialActionMismatches(contractFK, matchingFK);
|
|
230
242
|
if (actionMismatches.length > 0) {
|
|
231
243
|
const combinedMessage = actionMismatches.map((m) => m.message).join('; ');
|
|
232
244
|
const combinedExpected = actionMismatches.map((m) => m.expected).join(', ');
|
|
233
245
|
const combinedActual = actionMismatches.map((m) => m.actual).join(', ');
|
|
234
|
-
|
|
246
|
+
const issue: SchemaIssue = {
|
|
235
247
|
kind: 'foreign_key_mismatch',
|
|
236
248
|
table: tableName,
|
|
237
249
|
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
250
|
indexOrConstraint: matchingFK.name ?? `fk(${contractFK.source.columns.join(',')})`,
|
|
241
251
|
expected: combinedExpected,
|
|
242
252
|
actual: combinedActual,
|
|
243
253
|
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
|
-
|
|
254
|
+
};
|
|
255
|
+
emitIssueAndNodeUnderControlPolicy(
|
|
256
|
+
tableControlPolicy,
|
|
257
|
+
issue,
|
|
258
|
+
{
|
|
259
|
+
status: 'fail',
|
|
260
|
+
kind: 'foreignKey',
|
|
261
|
+
name: `foreignKey(${contractFK.source.columns.join(', ')})`,
|
|
262
|
+
contractPath: fkPath,
|
|
263
|
+
code: 'foreign_key_mismatch',
|
|
264
|
+
message: combinedMessage,
|
|
265
|
+
expected: contractFK,
|
|
266
|
+
actual: matchingFK,
|
|
267
|
+
children: [],
|
|
268
|
+
},
|
|
269
|
+
issues,
|
|
270
|
+
nodes,
|
|
271
|
+
);
|
|
256
272
|
} else {
|
|
257
273
|
nodes.push({
|
|
258
274
|
status: 'pass',
|
|
@@ -286,24 +302,30 @@ export function verifyForeignKeys(
|
|
|
286
302
|
});
|
|
287
303
|
|
|
288
304
|
if (!matchingFK) {
|
|
289
|
-
|
|
305
|
+
const issue: SchemaIssue = {
|
|
290
306
|
kind: 'extra_foreign_key',
|
|
291
307
|
table: tableName,
|
|
292
308
|
namespaceId,
|
|
293
309
|
indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(',')})`,
|
|
294
310
|
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
|
-
|
|
311
|
+
};
|
|
312
|
+
emitIssueAndNodeUnderControlPolicy(
|
|
313
|
+
tableControlPolicy,
|
|
314
|
+
issue,
|
|
315
|
+
{
|
|
316
|
+
status: 'fail',
|
|
317
|
+
kind: 'foreignKey',
|
|
318
|
+
name: `foreignKey(${schemaFK.columns.join(', ')})`,
|
|
319
|
+
contractPath: `${tablePath}.foreignKeys[${schemaFK.columns.join(',')}]`,
|
|
320
|
+
code: 'extra_foreign_key',
|
|
321
|
+
message: 'Extra foreign key found',
|
|
322
|
+
expected: undefined,
|
|
323
|
+
actual: schemaFK,
|
|
324
|
+
children: [],
|
|
325
|
+
},
|
|
326
|
+
issues,
|
|
327
|
+
nodes,
|
|
328
|
+
);
|
|
307
329
|
}
|
|
308
330
|
}
|
|
309
331
|
}
|
|
@@ -329,6 +351,7 @@ export function verifyUniqueConstraints(
|
|
|
329
351
|
tableName: string,
|
|
330
352
|
namespaceId: string,
|
|
331
353
|
tablePath: string,
|
|
354
|
+
tableControlPolicy: ControlPolicy,
|
|
332
355
|
issues: SchemaIssue[],
|
|
333
356
|
strict: boolean,
|
|
334
357
|
): SchemaVerificationNode[] {
|
|
@@ -349,27 +372,31 @@ export function verifyUniqueConstraints(
|
|
|
349
372
|
schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
|
|
350
373
|
|
|
351
374
|
if (!matchingUnique && !matchingUniqueIndex) {
|
|
352
|
-
|
|
375
|
+
const issue: SchemaIssue = {
|
|
353
376
|
kind: 'unique_constraint_mismatch',
|
|
354
377
|
table: tableName,
|
|
355
378
|
namespaceId,
|
|
356
379
|
expected: contractUnique.columns.join(', '),
|
|
357
380
|
message: `Table "${tableName}" is missing unique constraint: ${contractUnique.columns.join(', ')}`,
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
381
|
+
};
|
|
382
|
+
emitIssueAndNodeUnderControlPolicy(
|
|
383
|
+
tableControlPolicy,
|
|
384
|
+
issue,
|
|
385
|
+
{
|
|
386
|
+
status: 'fail',
|
|
387
|
+
kind: 'unique',
|
|
388
|
+
name: `unique(${contractUnique.columns.join(', ')})`,
|
|
389
|
+
contractPath: uniquePath,
|
|
390
|
+
code: 'unique_constraint_mismatch',
|
|
391
|
+
message: 'Unique constraint missing',
|
|
392
|
+
expected: contractUnique,
|
|
393
|
+
actual: undefined,
|
|
394
|
+
children: [],
|
|
395
|
+
},
|
|
396
|
+
issues,
|
|
397
|
+
nodes,
|
|
398
|
+
);
|
|
370
399
|
} else {
|
|
371
|
-
// Name differences are ignored for semantic satisfaction.
|
|
372
|
-
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
373
400
|
nodes.push({
|
|
374
401
|
status: 'pass',
|
|
375
402
|
kind: 'unique',
|
|
@@ -384,7 +411,6 @@ export function verifyUniqueConstraints(
|
|
|
384
411
|
}
|
|
385
412
|
}
|
|
386
413
|
|
|
387
|
-
// Check for extra uniques in strict mode
|
|
388
414
|
if (strict) {
|
|
389
415
|
for (const schemaUnique of schemaUniques) {
|
|
390
416
|
const matchingUnique = contractUniques.find((u) =>
|
|
@@ -392,24 +418,30 @@ export function verifyUniqueConstraints(
|
|
|
392
418
|
);
|
|
393
419
|
|
|
394
420
|
if (!matchingUnique) {
|
|
395
|
-
|
|
421
|
+
const issue: SchemaIssue = {
|
|
396
422
|
kind: 'extra_unique_constraint',
|
|
397
423
|
table: tableName,
|
|
398
424
|
namespaceId,
|
|
399
425
|
indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(',')})`,
|
|
400
426
|
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
|
-
|
|
427
|
+
};
|
|
428
|
+
emitIssueAndNodeUnderControlPolicy(
|
|
429
|
+
tableControlPolicy,
|
|
430
|
+
issue,
|
|
431
|
+
{
|
|
432
|
+
status: 'fail',
|
|
433
|
+
kind: 'unique',
|
|
434
|
+
name: `unique(${schemaUnique.columns.join(', ')})`,
|
|
435
|
+
contractPath: `${tablePath}.uniques[${schemaUnique.columns.join(',')}]`,
|
|
436
|
+
code: 'extra_unique_constraint',
|
|
437
|
+
message: 'Extra unique constraint found',
|
|
438
|
+
expected: undefined,
|
|
439
|
+
actual: schemaUnique,
|
|
440
|
+
children: [],
|
|
441
|
+
},
|
|
442
|
+
issues,
|
|
443
|
+
nodes,
|
|
444
|
+
);
|
|
413
445
|
}
|
|
414
446
|
}
|
|
415
447
|
}
|
|
@@ -435,6 +467,7 @@ export function verifyIndexes(
|
|
|
435
467
|
tableName: string,
|
|
436
468
|
namespaceId: string,
|
|
437
469
|
tablePath: string,
|
|
470
|
+
tableControlPolicy: ControlPolicy,
|
|
438
471
|
issues: SchemaIssue[],
|
|
439
472
|
strict: boolean,
|
|
440
473
|
): SchemaVerificationNode[] {
|
|
@@ -461,27 +494,31 @@ export function verifyIndexes(
|
|
|
461
494
|
schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
|
|
462
495
|
|
|
463
496
|
if (!matchingIndex && !matchingUniqueConstraint) {
|
|
464
|
-
|
|
497
|
+
const issue: SchemaIssue = {
|
|
465
498
|
kind: 'index_mismatch',
|
|
466
499
|
table: tableName,
|
|
467
500
|
namespaceId,
|
|
468
501
|
expected: contractIndex.columns.join(', '),
|
|
469
502
|
message: `Table "${tableName}" is missing index: ${contractIndex.columns.join(', ')}`,
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
503
|
+
};
|
|
504
|
+
emitIssueAndNodeUnderControlPolicy(
|
|
505
|
+
tableControlPolicy,
|
|
506
|
+
issue,
|
|
507
|
+
{
|
|
508
|
+
status: 'fail',
|
|
509
|
+
kind: 'index',
|
|
510
|
+
name: `index(${contractIndex.columns.join(', ')})`,
|
|
511
|
+
contractPath: indexPath,
|
|
512
|
+
code: 'index_mismatch',
|
|
513
|
+
message: 'Index missing',
|
|
514
|
+
expected: contractIndex,
|
|
515
|
+
actual: undefined,
|
|
516
|
+
children: [],
|
|
517
|
+
},
|
|
518
|
+
issues,
|
|
519
|
+
nodes,
|
|
520
|
+
);
|
|
482
521
|
} else {
|
|
483
|
-
// Name differences are ignored for semantic satisfaction.
|
|
484
|
-
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
485
522
|
nodes.push({
|
|
486
523
|
status: 'pass',
|
|
487
524
|
kind: 'index',
|
|
@@ -496,10 +533,8 @@ export function verifyIndexes(
|
|
|
496
533
|
}
|
|
497
534
|
}
|
|
498
535
|
|
|
499
|
-
// Check for extra indexes in strict mode
|
|
500
536
|
if (strict) {
|
|
501
537
|
for (const schemaIndex of schemaIndexes) {
|
|
502
|
-
// Skip unique indexes (they're handled as unique constraints)
|
|
503
538
|
if (schemaIndex.unique) {
|
|
504
539
|
continue;
|
|
505
540
|
}
|
|
@@ -510,24 +545,30 @@ export function verifyIndexes(
|
|
|
510
545
|
);
|
|
511
546
|
|
|
512
547
|
if (!matchingIndex) {
|
|
513
|
-
|
|
548
|
+
const issue: SchemaIssue = {
|
|
514
549
|
kind: 'extra_index',
|
|
515
550
|
table: tableName,
|
|
516
551
|
namespaceId,
|
|
517
552
|
indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(',')})`,
|
|
518
553
|
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(', ')}`,
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
554
|
+
};
|
|
555
|
+
emitIssueAndNodeUnderControlPolicy(
|
|
556
|
+
tableControlPolicy,
|
|
557
|
+
issue,
|
|
558
|
+
{
|
|
559
|
+
status: 'fail',
|
|
560
|
+
kind: 'index',
|
|
561
|
+
name: `index(${schemaIndex.columns.join(', ')})`,
|
|
562
|
+
contractPath: `${tablePath}.indexes[${schemaIndex.columns.join(',')}]`,
|
|
563
|
+
code: 'extra_index',
|
|
564
|
+
message: 'Extra index found',
|
|
565
|
+
expected: undefined,
|
|
566
|
+
actual: schemaIndex,
|
|
567
|
+
children: [],
|
|
568
|
+
},
|
|
569
|
+
issues,
|
|
570
|
+
nodes,
|
|
571
|
+
);
|
|
531
572
|
}
|
|
532
573
|
}
|
|
533
574
|
}
|