@prisma-next/family-sql 0.14.0-dev.49 → 0.14.0-dev.50
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-DHYFuOBy.d.mts → control-adapter-BVOj4gXI.d.mts} +34 -5
- package/dist/control-adapter-BVOj4gXI.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +1 -1
- package/dist/control.d.mts +65 -50
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +308 -56
- package/dist/control.mjs.map +1 -1
- package/dist/diff.d.mts +141 -30
- package/dist/diff.d.mts.map +1 -1
- package/dist/diff.mjs +155 -1384
- package/dist/diff.mjs.map +1 -1
- package/dist/ir.d.mts +5 -5
- 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 +1 -1
- 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-DwUSJ4PO.mjs → sql-contract-serializer-C75cfMSS.mjs} +2 -2
- package/dist/{sql-contract-serializer-DwUSJ4PO.mjs.map → sql-contract-serializer-C75cfMSS.mjs.map} +1 -1
- package/dist/{types-DBxrwN1d.d.mts → types-BW7pzb2j.d.mts} +24 -15
- package/dist/types-BW7pzb2j.d.mts.map +1 -0
- package/package.json +21 -21
- package/src/core/control-instance.ts +99 -80
- package/src/core/control-target-descriptor.ts +29 -14
- package/src/core/diff/diff-tree-normalization.ts +211 -0
- package/src/core/diff/schema-verify.ts +324 -0
- package/src/core/diff/sql-schema-diff.ts +40 -1466
- package/src/core/diff/verifier-disposition.ts +14 -42
- package/src/core/ir/sql-schema-verifier-base.ts +5 -5
- package/src/core/migrations/contract-to-schema-ir.ts +63 -20
- package/src/core/migrations/native-type-expander.ts +28 -0
- package/src/core/migrations/schema-differ.ts +30 -29
- package/src/core/migrations/types.ts +23 -14
- package/src/exports/control.ts +6 -0
- package/src/exports/diff.ts +28 -8
- package/dist/contract-to-schema-ir-S-evq8E6.mjs +0 -264
- package/dist/contract-to-schema-ir-S-evq8E6.mjs.map +0 -1
- package/dist/control-adapter-DHYFuOBy.d.mts.map +0 -1
- package/dist/sql-schema-diff-6z36dZt6.d.mts +0 -105
- package/dist/sql-schema-diff-6z36dZt6.d.mts.map +0 -1
- package/dist/types-DBxrwN1d.d.mts.map +0 -1
- package/src/core/diff/control-verify-emit.ts +0 -46
- package/src/core/diff/verify-helpers.ts +0 -832
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The differ-based SQL schema verify: post-diff filters and verdict.
|
|
3
|
+
*
|
|
4
|
+
* The generic node differ (`diffSchemas`) reports every node-level
|
|
5
|
+
* difference between the derived expected tree and the introspected actual
|
|
6
|
+
* tree. This module is the consumer side the spec assigns to the SQL
|
|
7
|
+
* family: strict-mode extras gating and control-policy disposition are
|
|
8
|
+
* reason/kind-keyed filters applied AFTER the diff — never inside it — and
|
|
9
|
+
* the verify verdict derives from the filtered issue list.
|
|
10
|
+
*
|
|
11
|
+
* `verifySqlSchemaByDiff` wraps the verdict in the issue-based result
|
|
12
|
+
* envelope — this is THE SQL schema verify (the legacy relational walk and
|
|
13
|
+
* its verification tree are retired).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Contract, ControlPolicy } from '@prisma-next/contract/types';
|
|
17
|
+
import { effectiveControlPolicy } from '@prisma-next/contract/types';
|
|
18
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
19
|
+
import type {
|
|
20
|
+
DiffSubjectGranularity,
|
|
21
|
+
SchemaDiffIssue,
|
|
22
|
+
VerifierIssueCategory,
|
|
23
|
+
VerifierOutcome,
|
|
24
|
+
VerifyDatabaseSchemaResult,
|
|
25
|
+
} from '@prisma-next/framework-components/control';
|
|
26
|
+
import { dispositionForCategory } from '@prisma-next/framework-components/control';
|
|
27
|
+
import { isStorageTypeInstance, type SqlStorage } from '@prisma-next/sql-contract/types';
|
|
28
|
+
import { RelationalSchemaNodeKind, type SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
|
|
29
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
30
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
31
|
+
import { extractCodecControlHooks } from '../assembly';
|
|
32
|
+
import type { SqlSchemaDiffFn } from '../migrations/schema-differ';
|
|
33
|
+
import type { CodecControlHooks } from '../migrations/types';
|
|
34
|
+
import { verifierDisposition } from './verifier-disposition';
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Subject-granularity classification — nodeKind → framework-neutral granularity
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
function issueNode(issue: SchemaDiffIssue): SqlSchemaIRNode | undefined {
|
|
41
|
+
const node = issue.expected ?? issue.actual;
|
|
42
|
+
if (node === undefined) return undefined;
|
|
43
|
+
return blindCast<
|
|
44
|
+
SqlSchemaIRNode,
|
|
45
|
+
'every node in a SQL schema diff tree is a SqlSchemaIRNode; nodeKind is its identity'
|
|
46
|
+
>(node);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolves an issue's framework-neutral {@link DiffSubjectGranularity} on
|
|
51
|
+
* demand, from the issue's node's `nodeKind` via the target-provided
|
|
52
|
+
* `granularityOf` map. The node carries only its `nodeKind` identity, never a
|
|
53
|
+
* classification, and nothing is stamped onto the issue — every consumer
|
|
54
|
+
* (the family verdict below, the framework aggregate's unclaimed-elements
|
|
55
|
+
* sweep via {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable})
|
|
56
|
+
* calls this the same way, resolved by the family/target that owns the node
|
|
57
|
+
* vocabulary. `undefined` for an issue with no node.
|
|
58
|
+
*/
|
|
59
|
+
export function classifyDiffSubjectGranularity(
|
|
60
|
+
issue: SchemaDiffIssue,
|
|
61
|
+
granularityOf: (nodeKind: string) => DiffSubjectGranularity,
|
|
62
|
+
): DiffSubjectGranularity | undefined {
|
|
63
|
+
const node = issueNode(issue);
|
|
64
|
+
return node === undefined ? undefined : granularityOf(node.nodeKind);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolves an issue's storage `entityKind` on demand, from the issue's
|
|
69
|
+
* node's `nodeKind` via the target-provided `entityKindOf` map — the sibling
|
|
70
|
+
* of {@link classifyDiffSubjectGranularity}, called the same way by the same
|
|
71
|
+
* consumers (via
|
|
72
|
+
* {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}).
|
|
73
|
+
* `undefined` for an issue with no node, or for a node kind with no storage
|
|
74
|
+
* entity of its own.
|
|
75
|
+
*/
|
|
76
|
+
export function classifyDiffEntityKind(
|
|
77
|
+
issue: SchemaDiffIssue,
|
|
78
|
+
entityKindOf: (nodeKind: string) => string | undefined,
|
|
79
|
+
): string | undefined {
|
|
80
|
+
const node = issueNode(issue);
|
|
81
|
+
return node === undefined ? undefined : entityKindOf(node.nodeKind);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Issue classification — subject granularity + reason → target-neutral category
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Re-keys the legacy `classifySqlVerifierIssueKind` category mapping on the
|
|
90
|
+
* issue's {@link DiffSubjectGranularity} (resolved via `granularityOf`) + the
|
|
91
|
+
* issue reason. The vocabulary maps one-to-one: an undeclared live entity or
|
|
92
|
+
* namespace is `extraTopLevelObject`, an undeclared live field
|
|
93
|
+
* `extraNestedElement`, undeclared auxiliaries (constraints, indexes,
|
|
94
|
+
* defaults) and structural leaves (policies) `extraAuxiliary`; a value-set
|
|
95
|
+
* drift on a check node is `valueDrift`; every other paired divergence is
|
|
96
|
+
* `declaredIncompatible`; anything the database lacks is `declaredMissing`.
|
|
97
|
+
* `granularityOf` is the target's classifier, so target and extension node
|
|
98
|
+
* kinds classify without the family importing them.
|
|
99
|
+
*/
|
|
100
|
+
export function classifySqlDiffIssue(
|
|
101
|
+
issue: SchemaDiffIssue,
|
|
102
|
+
granularityOf: (nodeKind: string) => DiffSubjectGranularity,
|
|
103
|
+
): VerifierIssueCategory {
|
|
104
|
+
if (issue.reason === 'not-found') {
|
|
105
|
+
return 'declaredMissing';
|
|
106
|
+
}
|
|
107
|
+
if (issue.reason === 'not-expected') {
|
|
108
|
+
const granularity = classifyDiffSubjectGranularity(issue, granularityOf);
|
|
109
|
+
if (granularity === 'entity' || granularity === 'namespace') {
|
|
110
|
+
return 'extraTopLevelObject';
|
|
111
|
+
}
|
|
112
|
+
if (granularity === 'field') {
|
|
113
|
+
return 'extraNestedElement';
|
|
114
|
+
}
|
|
115
|
+
return 'extraAuxiliary';
|
|
116
|
+
}
|
|
117
|
+
if (issueNode(issue)?.nodeKind === RelationalSchemaNodeKind.check) {
|
|
118
|
+
return 'valueDrift';
|
|
119
|
+
}
|
|
120
|
+
return 'declaredIncompatible';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Whether a `not-expected` issue is a strict-mode-only finding. The legacy
|
|
125
|
+
* walk detected every relational extra (namespaces, entities, fields, and
|
|
126
|
+
* their auxiliaries) only under `--strict`; the structural diff (roots, RLS
|
|
127
|
+
* policies, roles) was never strict-gated — its extras fail in both modes.
|
|
128
|
+
* Keyed on the issue's granularity, resolved via `granularityOf`.
|
|
129
|
+
*/
|
|
130
|
+
function isStrictOnlyExtra(
|
|
131
|
+
issue: SchemaDiffIssue,
|
|
132
|
+
granularityOf: (nodeKind: string) => DiffSubjectGranularity,
|
|
133
|
+
): boolean {
|
|
134
|
+
const granularity = classifyDiffSubjectGranularity(issue, granularityOf);
|
|
135
|
+
return (
|
|
136
|
+
granularity === 'namespace' ||
|
|
137
|
+
granularity === 'entity' ||
|
|
138
|
+
granularity === 'field' ||
|
|
139
|
+
granularity === 'auxiliary'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// The post-diff filter + verdict
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
export interface SqlDiffVerdictInput {
|
|
148
|
+
/** The full, ownership-scoped diff issue list from the target's differ. */
|
|
149
|
+
readonly issues: readonly SchemaDiffIssue[];
|
|
150
|
+
/** Resolves a diff issue's subject table's declared control policy directly from the contract. */
|
|
151
|
+
readonly resolveControlPolicy: (issue: SchemaDiffIssue) => ControlPolicy | undefined;
|
|
152
|
+
readonly strict: boolean;
|
|
153
|
+
readonly defaultControlPolicy: ControlPolicy | undefined;
|
|
154
|
+
/** The target's classifier: a diff issue node's `nodeKind` → its subject granularity. */
|
|
155
|
+
readonly granularityOf: (nodeKind: string) => DiffSubjectGranularity;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface SqlDiffVerdict {
|
|
159
|
+
readonly failures: readonly SchemaDiffIssue[];
|
|
160
|
+
readonly warnings: readonly SchemaDiffIssue[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Applies the two consumer filters to a diff issue list: strict gating
|
|
165
|
+
* (relational `not-expected` findings drop in lenient mode) and
|
|
166
|
+
* control-policy disposition (each surviving issue grades against its
|
|
167
|
+
* subject table's effective policy; suppressed issues drop, `observed`
|
|
168
|
+
* subjects warn). The verify verdict is `failures.length === 0`.
|
|
169
|
+
*/
|
|
170
|
+
export function computeSqlDiffVerdict(input: SqlDiffVerdictInput): SqlDiffVerdict {
|
|
171
|
+
const failures: SchemaDiffIssue[] = [];
|
|
172
|
+
const warnings: SchemaDiffIssue[] = [];
|
|
173
|
+
for (const issue of input.issues) {
|
|
174
|
+
if (
|
|
175
|
+
!input.strict &&
|
|
176
|
+
issue.reason === 'not-expected' &&
|
|
177
|
+
isStrictOnlyExtra(issue, input.granularityOf)
|
|
178
|
+
) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const tablePolicy = input.resolveControlPolicy(issue);
|
|
182
|
+
const policy = effectiveControlPolicy(tablePolicy, input.defaultControlPolicy);
|
|
183
|
+
const disposition: VerifierOutcome = dispositionForCategory(
|
|
184
|
+
policy,
|
|
185
|
+
classifySqlDiffIssue(issue, input.granularityOf),
|
|
186
|
+
);
|
|
187
|
+
if (disposition === 'suppress') continue;
|
|
188
|
+
if (disposition === 'warn') {
|
|
189
|
+
warnings.push(issue);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
failures.push(issue);
|
|
193
|
+
}
|
|
194
|
+
return { failures, warnings };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// Storage-types check — the codec verifyType hook path
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
export interface StorageTypeVerdictInput {
|
|
202
|
+
readonly contract: Contract<SqlStorage>;
|
|
203
|
+
/**
|
|
204
|
+
* Expected/actual namespace-node pairs the target's differ input produced:
|
|
205
|
+
* for a namespaced tree, one entry per expected namespace with a
|
|
206
|
+
* non-empty table set, paired by DDL schema name (absent actual side for
|
|
207
|
+
* a schema the database lacks); a flat tree is the sole pair.
|
|
208
|
+
*/
|
|
209
|
+
readonly namespacePairs: ReadonlyArray<{
|
|
210
|
+
readonly actual: SqlSchemaIRNode | undefined;
|
|
211
|
+
}>;
|
|
212
|
+
readonly codecHooks: ReadonlyMap<string, CodecControlHooks>;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Runs the codec `verifyType` hooks the way the legacy walk did: once per
|
|
217
|
+
* contract namespace with tables, against that namespace's paired actual
|
|
218
|
+
* node (the hook reads namespace-scoped state such as
|
|
219
|
+
* `nativeEnumTypeNames` off it). Issue dispositions grade against the
|
|
220
|
+
* contract default policy, matching the legacy `pushTypeNode` semantics.
|
|
221
|
+
*/
|
|
222
|
+
export interface StorageTypeVerdict {
|
|
223
|
+
readonly failures: readonly SchemaDiffIssue[];
|
|
224
|
+
readonly warnings: readonly SchemaDiffIssue[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function computeStorageTypeVerdict(input: StorageTypeVerdictInput): StorageTypeVerdict {
|
|
228
|
+
const failures: SchemaDiffIssue[] = [];
|
|
229
|
+
const warnings: SchemaDiffIssue[] = [];
|
|
230
|
+
const policy = effectiveControlPolicy(undefined, input.contract.defaultControlPolicy);
|
|
231
|
+
for (const pair of input.namespacePairs) {
|
|
232
|
+
if (pair.actual === undefined) continue;
|
|
233
|
+
for (const [typeName, typeInstance] of Object.entries(input.contract.storage.types ?? {})) {
|
|
234
|
+
if (!isStorageTypeInstance(typeInstance)) continue;
|
|
235
|
+
const hook = input.codecHooks.get(typeInstance.codecId);
|
|
236
|
+
if (!hook?.verifyType) continue;
|
|
237
|
+
const typeIssues = hook.verifyType({ typeName, typeInstance, schema: pair.actual });
|
|
238
|
+
for (const issue of typeIssues) {
|
|
239
|
+
const disposition = verifierDisposition(policy, issue);
|
|
240
|
+
if (disposition === 'suppress') continue;
|
|
241
|
+
if (disposition === 'warn') {
|
|
242
|
+
warnings.push(issue);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
failures.push(issue);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { failures, warnings };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// The issue-based verify envelope
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
export interface VerifySqlSchemaByDiffInput {
|
|
257
|
+
readonly contract: Contract<SqlStorage>;
|
|
258
|
+
readonly schema: SqlSchemaIRNode;
|
|
259
|
+
readonly strict: boolean;
|
|
260
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
261
|
+
/** The target's full-tree node diff (`diffSchema` descriptor hook). */
|
|
262
|
+
readonly diffSchema: SqlSchemaDiffFn;
|
|
263
|
+
/** The target's classifier: a diff issue node's `nodeKind` → its subject granularity. */
|
|
264
|
+
readonly granularityOf: (nodeKind: string) => DiffSubjectGranularity;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* THE SQL schema verify: runs the target's full-tree node diff, grades it
|
|
269
|
+
* through the family's post-diff filters (strict gating + control-policy
|
|
270
|
+
* disposition) plus the codec `verifyType` hook findings, and wraps the
|
|
271
|
+
* verdict in the issue-based result envelope. `ok` holds exactly when both
|
|
272
|
+
* issue lists are empty — the lists carry the verdict's failures.
|
|
273
|
+
*/
|
|
274
|
+
export function verifySqlSchemaByDiff(
|
|
275
|
+
input: VerifySqlSchemaByDiffInput,
|
|
276
|
+
): VerifyDatabaseSchemaResult {
|
|
277
|
+
const startTime = Date.now();
|
|
278
|
+
const verdictDiff = input.diffSchema({
|
|
279
|
+
contract: input.contract,
|
|
280
|
+
schema: input.schema,
|
|
281
|
+
frameworkComponents: input.frameworkComponents,
|
|
282
|
+
});
|
|
283
|
+
const diffVerdict = computeSqlDiffVerdict({
|
|
284
|
+
issues: verdictDiff.issues,
|
|
285
|
+
resolveControlPolicy: verdictDiff.resolveControlPolicy,
|
|
286
|
+
strict: input.strict,
|
|
287
|
+
defaultControlPolicy: input.contract.defaultControlPolicy,
|
|
288
|
+
granularityOf: input.granularityOf,
|
|
289
|
+
});
|
|
290
|
+
const storageTypeVerdict = computeStorageTypeVerdict({
|
|
291
|
+
contract: input.contract,
|
|
292
|
+
namespacePairs: verdictDiff.namespacePairs,
|
|
293
|
+
codecHooks: extractCodecControlHooks(input.frameworkComponents),
|
|
294
|
+
});
|
|
295
|
+
const failCount = diffVerdict.failures.length + storageTypeVerdict.failures.length;
|
|
296
|
+
const ok = failCount === 0;
|
|
297
|
+
const profileHash =
|
|
298
|
+
'profileHash' in input.contract && typeof input.contract.profileHash === 'string'
|
|
299
|
+
? input.contract.profileHash
|
|
300
|
+
: undefined;
|
|
301
|
+
return {
|
|
302
|
+
ok,
|
|
303
|
+
...(ok ? {} : { code: 'PN-SCHEMA-0001' }),
|
|
304
|
+
summary: ok
|
|
305
|
+
? 'Database schema satisfies contract'
|
|
306
|
+
: `Database schema does not satisfy contract (${failCount} failure${failCount === 1 ? '' : 's'})`,
|
|
307
|
+
contract: {
|
|
308
|
+
storageHash: input.contract.storage.storageHash,
|
|
309
|
+
...ifDefined('profileHash', profileHash),
|
|
310
|
+
},
|
|
311
|
+
target: {
|
|
312
|
+
expected: input.contract.target,
|
|
313
|
+
actual: input.contract.target,
|
|
314
|
+
},
|
|
315
|
+
schema: {
|
|
316
|
+
issues: [...diffVerdict.failures, ...storageTypeVerdict.failures],
|
|
317
|
+
warnings: {
|
|
318
|
+
issues: [...diffVerdict.warnings, ...storageTypeVerdict.warnings],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
meta: { strict: input.strict },
|
|
322
|
+
timings: { total: Date.now() - startTime },
|
|
323
|
+
};
|
|
324
|
+
}
|