@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
|
@@ -2,6 +2,7 @@ import type { ColumnDefault, Contract } from '@prisma-next/contract/types';
|
|
|
2
2
|
import type { MigrationPlannerConflict } from '@prisma-next/framework-components/control';
|
|
3
3
|
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
4
4
|
import {
|
|
5
|
+
type CheckConstraint,
|
|
5
6
|
type ForeignKey,
|
|
6
7
|
type Index,
|
|
7
8
|
isPostgresEnumStorageEntry,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
18
19
|
import type {
|
|
19
20
|
SqlAnnotations,
|
|
21
|
+
SqlCheckConstraintIRInput,
|
|
20
22
|
SqlColumnIR,
|
|
21
23
|
SqlForeignKeyIR,
|
|
22
24
|
SqlIndexIR,
|
|
@@ -24,6 +26,7 @@ import type {
|
|
|
24
26
|
SqlTableIR,
|
|
25
27
|
SqlUniqueIR,
|
|
26
28
|
} from '@prisma-next/sql-schema-ir/types';
|
|
29
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
27
30
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -151,6 +154,58 @@ function resolveColumnTypeMetadata(
|
|
|
151
154
|
);
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Resolves a `ValueSetRef` to its permitted values from the contract storage.
|
|
159
|
+
*
|
|
160
|
+
* Throws when the referenced namespace or value-set is absent — this indicates
|
|
161
|
+
* the contract was built incorrectly (the check and the value-set must be
|
|
162
|
+
* co-emitted by the lowering step). Used by `convertCheck` (schema-IR
|
|
163
|
+
* projection), `verifyCheckConstraints` (verification), and
|
|
164
|
+
* `checkConstraintPlanCallStrategy` (migration planning) so all three agree on
|
|
165
|
+
* the resolved values and the error behavior on a missing reference.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveValueSetValues(
|
|
168
|
+
ref: { readonly namespaceId: string; readonly name: string },
|
|
169
|
+
storage: SqlStorage,
|
|
170
|
+
contextLabel: string,
|
|
171
|
+
): readonly string[] {
|
|
172
|
+
const ns = storage.namespaces[ref.namespaceId];
|
|
173
|
+
if (!ns) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`resolveValueSetValues: namespace "${ref.namespaceId}" not found in storage (${contextLabel})`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const valueSet = ns.entries.valueSet?.[ref.name];
|
|
179
|
+
if (!valueSet) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`resolveValueSetValues: value-set "${ref.name}" not found in namespace "${ref.namespaceId}" (${contextLabel})`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return valueSet.values;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Projects a `CheckConstraint` IR into an `SqlCheckConstraintIRInput` by
|
|
189
|
+
* resolving the permitted values from the storage value-set it references.
|
|
190
|
+
*
|
|
191
|
+
* The `CheckConstraint.valueSet` ref points to
|
|
192
|
+
* `storage.namespaces[namespaceId].entries.valueSet[name]`. The resolved
|
|
193
|
+
* values are lifted directly from `StorageValueSet.values` so verification
|
|
194
|
+
* compares value sets, not SQL predicate strings.
|
|
195
|
+
*
|
|
196
|
+
* Throws if the referenced namespace or value-set is absent — this
|
|
197
|
+
* indicates the contract was built incorrectly (the check and the
|
|
198
|
+
* value-set must be co-emitted by the lowering step).
|
|
199
|
+
*/
|
|
200
|
+
function convertCheck(check: CheckConstraint, storage: SqlStorage): SqlCheckConstraintIRInput {
|
|
201
|
+
const permittedValues = resolveValueSetValues(check.valueSet, storage, `check "${check.name}"`);
|
|
202
|
+
return {
|
|
203
|
+
name: check.name,
|
|
204
|
+
column: check.column,
|
|
205
|
+
permittedValues,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
154
209
|
function convertUnique(unique: UniqueConstraint): SqlUniqueIR {
|
|
155
210
|
return {
|
|
156
211
|
columns: unique.columns,
|
|
@@ -184,6 +239,7 @@ function convertTable(
|
|
|
184
239
|
storageTypes: ResolvedStorageTypes,
|
|
185
240
|
expandNativeType: NativeTypeExpander | undefined,
|
|
186
241
|
renderDefault: DefaultRenderer | undefined,
|
|
242
|
+
storage: SqlStorage,
|
|
187
243
|
): SqlTableIR {
|
|
188
244
|
const columns: Record<string, SqlColumnIR> = {};
|
|
189
245
|
for (const [colName, colDef] of Object.entries(table.columns)) {
|
|
@@ -214,13 +270,19 @@ function convertTable(
|
|
|
214
270
|
satisfiedIndexColumns.add(key);
|
|
215
271
|
}
|
|
216
272
|
|
|
273
|
+
const checks: SqlCheckConstraintIRInput[] | undefined =
|
|
274
|
+
table.checks && table.checks.length > 0
|
|
275
|
+
? table.checks.map((c) => convertCheck(c, storage))
|
|
276
|
+
: undefined;
|
|
277
|
+
|
|
217
278
|
return {
|
|
218
279
|
name,
|
|
219
280
|
columns,
|
|
220
281
|
...ifDefined('primaryKey', table.primaryKey),
|
|
221
|
-
foreignKeys: table.foreignKeys.map(convertForeignKey),
|
|
282
|
+
foreignKeys: table.foreignKeys.filter((fk) => fk.constraint !== false).map(convertForeignKey),
|
|
222
283
|
uniques: table.uniques.map(convertUnique),
|
|
223
284
|
indexes: [...table.indexes.map(convertIndex), ...fkBackingIndexes],
|
|
285
|
+
...ifDefined('checks', checks),
|
|
224
286
|
};
|
|
225
287
|
}
|
|
226
288
|
|
|
@@ -250,11 +312,11 @@ export function detectDestructiveChanges(
|
|
|
250
312
|
for (const namespaceId of namespaceIds) {
|
|
251
313
|
const fromNs = from.namespaces[namespaceId];
|
|
252
314
|
const toNs = to.namespaces[namespaceId];
|
|
253
|
-
const fromTables = fromNs?.
|
|
315
|
+
const fromTables = fromNs?.entries.table;
|
|
254
316
|
if (!fromTables) continue;
|
|
255
317
|
|
|
256
318
|
for (const tableName of Object.keys(fromTables)) {
|
|
257
|
-
const toTableRaw = toNs?.
|
|
319
|
+
const toTableRaw = toNs?.entries.table[tableName];
|
|
258
320
|
if (!(toTableRaw instanceof StorageTable)) {
|
|
259
321
|
conflicts.push({
|
|
260
322
|
kind: 'tableRemoved',
|
|
@@ -327,20 +389,23 @@ export function contractToSchemaIR(
|
|
|
327
389
|
...((storage.types ?? {}) as ResolvedStorageTypes),
|
|
328
390
|
};
|
|
329
391
|
for (const ns of Object.values(storage.namespaces)) {
|
|
330
|
-
const nsEnums =
|
|
392
|
+
const nsEnums = ns.entries['type'];
|
|
331
393
|
if (nsEnums) {
|
|
332
394
|
for (const [k, v] of Object.entries(nsEnums)) {
|
|
333
|
-
allTypes[k] =
|
|
395
|
+
allTypes[k] = blindCast<
|
|
396
|
+
PostgresEnumStorageEntry | StorageTypeInstance,
|
|
397
|
+
'entries.type holds postgres-specific enum entries at runtime'
|
|
398
|
+
>(v);
|
|
334
399
|
}
|
|
335
400
|
}
|
|
336
401
|
}
|
|
337
402
|
const storageTypes = allTypes as ResolvedStorageTypes;
|
|
338
403
|
const tables: Record<string, SqlTableIR> = {};
|
|
339
404
|
for (const ns of Object.values(storage.namespaces)) {
|
|
340
|
-
for (const [tableName, tableDefRaw] of Object.entries(ns.
|
|
405
|
+
for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table)) {
|
|
341
406
|
if (!(tableDefRaw instanceof StorageTable)) {
|
|
342
407
|
throw new Error(
|
|
343
|
-
`contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.
|
|
408
|
+
`contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.entries.table.${tableName}`,
|
|
344
409
|
);
|
|
345
410
|
}
|
|
346
411
|
const tableDef = tableDefRaw;
|
|
@@ -355,6 +420,7 @@ export function contractToSchemaIR(
|
|
|
355
420
|
storageTypes,
|
|
356
421
|
options.expandNativeType,
|
|
357
422
|
options.renderDefault,
|
|
423
|
+
storage,
|
|
358
424
|
);
|
|
359
425
|
}
|
|
360
426
|
}
|
|
@@ -395,7 +461,7 @@ function deriveAnnotations(
|
|
|
395
461
|
|
|
396
462
|
// Top-level `storage.types`: codec-typed entries (vector, decimal, …) keyed
|
|
397
463
|
// by bare `nativeType` (unchanged). Post-S1.B enums live in
|
|
398
|
-
// `namespaces[*].
|
|
464
|
+
// `namespaces[*].entries.type`, not here; a defensive top-level enum is still
|
|
399
465
|
// namespace/schema-qualified via the resolver under the unbound coordinate
|
|
400
466
|
// so it never collides on a bare name.
|
|
401
467
|
for (const typeInstance of Object.values((storage.types ?? {}) as ResolvedStorageTypes)) {
|
|
@@ -415,7 +481,7 @@ function deriveAnnotations(
|
|
|
415
481
|
// `readExistingEnumValues` read side, so two namespaces sharing an enum name
|
|
416
482
|
// (or native type) resolve to distinct live-database types.
|
|
417
483
|
for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {
|
|
418
|
-
const nsEnums =
|
|
484
|
+
const nsEnums = ns.entries['type'];
|
|
419
485
|
if (!nsEnums) continue;
|
|
420
486
|
for (const entry of Object.values(nsEnums)) {
|
|
421
487
|
if (!isPostgresEnumStorageEntry(entry)) continue;
|
|
@@ -0,0 +1,322 @@
|
|
|
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
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
8
|
+
import type { SqlPlannerConflict } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The target object a control policy governs for a single planner call,
|
|
12
|
+
* resolved from the target's own IR. `undefined` means the call's target
|
|
13
|
+
* object could not be positively established — a fail-closed signal: any
|
|
14
|
+
* policy stricter than `managed` drops such a call rather than emitting it.
|
|
15
|
+
*/
|
|
16
|
+
export interface ControlPolicySubject {
|
|
17
|
+
readonly namespaceId: string;
|
|
18
|
+
readonly explicitNodeControlPolicy?: ControlPolicy;
|
|
19
|
+
readonly table?: string;
|
|
20
|
+
readonly column?: string;
|
|
21
|
+
readonly typeName?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the call creates a whole, previously-absent top-level storage
|
|
24
|
+
* object (e.g. a table or an enum/type), as opposed to modifying an
|
|
25
|
+
* existing object. This is the only thing `tolerated` permits: it is a
|
|
26
|
+
* create-if-absent policy, so an op that touches an existing object — add
|
|
27
|
+
* column, add index/constraint, alter, drop — is never allowed under it.
|
|
28
|
+
*/
|
|
29
|
+
readonly createsNewObject: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The control policy that governs a single call. The `external` default is an
|
|
34
|
+
* un-overridable namespace floor: when the contract default is `external`, no
|
|
35
|
+
* per-object `managed` override can escalate DDL above the floor, so the
|
|
36
|
+
* policy is forced to `external` regardless of the node's own declaration.
|
|
37
|
+
* Every other default defers to the node's effective control policy.
|
|
38
|
+
*/
|
|
39
|
+
export function controlPolicyForCall(
|
|
40
|
+
subject: ControlPolicySubject | undefined,
|
|
41
|
+
defaultControlPolicy: ControlPolicy | undefined,
|
|
42
|
+
): ControlPolicy {
|
|
43
|
+
if (defaultControlPolicy === 'external') {
|
|
44
|
+
return 'external';
|
|
45
|
+
}
|
|
46
|
+
return effectiveControlPolicy(subject?.explicitNodeControlPolicy, defaultControlPolicy);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether a call is allowed to emit under a given control policy.
|
|
51
|
+
*
|
|
52
|
+
* - `managed` — full lifecycle, every op allowed.
|
|
53
|
+
* - `tolerated` — create-if-absent only: allowed iff the call creates a whole
|
|
54
|
+
* new top-level object (and its subject was positively resolved). Anything
|
|
55
|
+
* that modifies an existing object, and anything whose subject could not be
|
|
56
|
+
* resolved, is suppressed.
|
|
57
|
+
* - `external` / `observed` — no DDL at all.
|
|
58
|
+
*/
|
|
59
|
+
function callAllowedUnderControlPolicy(
|
|
60
|
+
policy: ControlPolicy,
|
|
61
|
+
subject: ControlPolicySubject | undefined,
|
|
62
|
+
): boolean {
|
|
63
|
+
switch (policy) {
|
|
64
|
+
case 'managed':
|
|
65
|
+
return true;
|
|
66
|
+
case 'tolerated':
|
|
67
|
+
return subject?.createsNewObject === true;
|
|
68
|
+
case 'external':
|
|
69
|
+
case 'observed':
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function defaultSubjectLabel(
|
|
75
|
+
factoryName: string,
|
|
76
|
+
subject: ControlPolicySubject | undefined,
|
|
77
|
+
): string {
|
|
78
|
+
if (subject?.table) {
|
|
79
|
+
return `${factoryName}(${subject.table})`;
|
|
80
|
+
}
|
|
81
|
+
if (subject?.typeName) {
|
|
82
|
+
return `${factoryName}(${subject.typeName})`;
|
|
83
|
+
}
|
|
84
|
+
return factoryName;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function suppressionSummary(
|
|
88
|
+
subjectLabel: string,
|
|
89
|
+
subject: ControlPolicySubject | undefined,
|
|
90
|
+
effectivePolicy: ControlPolicy,
|
|
91
|
+
): string {
|
|
92
|
+
const namespace = subject?.namespaceId ?? 'unknown';
|
|
93
|
+
const declared = subject?.explicitNodeControlPolicy;
|
|
94
|
+
if (effectivePolicy === 'external' && declared === 'managed') {
|
|
95
|
+
return `control policy suppressed: ${subjectLabel} — namespace '${namespace}' has effective control 'external' but table declared 'managed'`;
|
|
96
|
+
}
|
|
97
|
+
const declaredSuffix = declared ? ` but table declared '${declared}'` : '';
|
|
98
|
+
return `control policy suppressed: ${subjectLabel} — namespace '${namespace}' has effective control '${effectivePolicy}'${declaredSuffix}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildSubjectSuppressionWarning(
|
|
102
|
+
subject: ControlPolicySubject | undefined,
|
|
103
|
+
effectivePolicy: ControlPolicy,
|
|
104
|
+
factoryName: string,
|
|
105
|
+
formatSubjectLabel: (factoryName: string, subject: ControlPolicySubject | undefined) => string,
|
|
106
|
+
): SqlPlannerConflict {
|
|
107
|
+
const subjectLabel = formatSubjectLabel(factoryName, subject);
|
|
108
|
+
return {
|
|
109
|
+
kind: 'controlPolicySuppressedCall',
|
|
110
|
+
summary: suppressionSummary(subjectLabel, subject, effectivePolicy),
|
|
111
|
+
location: {
|
|
112
|
+
...ifDefined('namespace', subject?.namespaceId),
|
|
113
|
+
...ifDefined('table', subject?.table),
|
|
114
|
+
...ifDefined('column', subject?.column),
|
|
115
|
+
...ifDefined('type', subject?.typeName),
|
|
116
|
+
},
|
|
117
|
+
meta: {
|
|
118
|
+
controlPolicy: effectivePolicy,
|
|
119
|
+
factoryName,
|
|
120
|
+
...ifDefined('declaredControlPolicy', subject?.explicitNodeControlPolicy),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function defaultModificationFactoryNameForSubject(subject: ControlPolicySubject): string {
|
|
126
|
+
if (subject.table) return 'alterTable';
|
|
127
|
+
if (subject.typeName) return 'alterType';
|
|
128
|
+
return 'alterSchema';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Partition the calls produced for a single set of subjects into those the
|
|
133
|
+
* effective control policy permits (`kept`) and a list of
|
|
134
|
+
* {@link SqlPlannerConflict} warnings describing the suppressed calls.
|
|
135
|
+
*
|
|
136
|
+
* **Prefer {@link partitionIssuesByControlPolicy}** for the schema-issue
|
|
137
|
+
* pipeline: it filters subjects out of the planner's *input* so the planner
|
|
138
|
+
* never has to reason about un-modeled state on `external`/`observed`
|
|
139
|
+
* subjects. This call-level helper remains for paths that bypass the issue
|
|
140
|
+
* pipeline — currently the codec-emitted field-event ops, which originate
|
|
141
|
+
* from declared contract fields rather than from introspected schema state
|
|
142
|
+
* and therefore cannot trip the diff engine.
|
|
143
|
+
*/
|
|
144
|
+
export function partitionCallsByControlPolicy<TCall>(options: {
|
|
145
|
+
readonly calls: readonly TCall[];
|
|
146
|
+
readonly contract: Contract<SqlStorage>;
|
|
147
|
+
readonly resolveControlPolicySubject: (call: TCall) => ControlPolicySubject | undefined;
|
|
148
|
+
readonly resolveFactoryName: (call: TCall) => string;
|
|
149
|
+
readonly formatSubjectLabel?: (
|
|
150
|
+
factoryName: string,
|
|
151
|
+
subject: ControlPolicySubject | undefined,
|
|
152
|
+
) => string;
|
|
153
|
+
}): {
|
|
154
|
+
readonly kept: readonly TCall[];
|
|
155
|
+
readonly warnings: readonly SqlPlannerConflict[];
|
|
156
|
+
} {
|
|
157
|
+
const defaultControlPolicy = options.contract.defaultControlPolicy;
|
|
158
|
+
const formatSubjectLabel = options.formatSubjectLabel ?? defaultSubjectLabel;
|
|
159
|
+
const kept: TCall[] = [];
|
|
160
|
+
const warnings: SqlPlannerConflict[] = [];
|
|
161
|
+
|
|
162
|
+
for (const call of options.calls) {
|
|
163
|
+
const subject = options.resolveControlPolicySubject(call);
|
|
164
|
+
const policy = controlPolicyForCall(subject, defaultControlPolicy);
|
|
165
|
+
if (callAllowedUnderControlPolicy(policy, subject)) {
|
|
166
|
+
kept.push(call);
|
|
167
|
+
} else {
|
|
168
|
+
const factoryName = options.resolveFactoryName(call);
|
|
169
|
+
warnings.push(
|
|
170
|
+
buildSubjectSuppressionWarning(subject, policy, factoryName, formatSubjectLabel),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Object.freeze({
|
|
176
|
+
kept: Object.freeze(kept),
|
|
177
|
+
warnings: Object.freeze(warnings),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Partition a list of schema-issue-shaped inputs by the effective control
|
|
183
|
+
* policy of each issue's subject *before* the planner is invoked.
|
|
184
|
+
*
|
|
185
|
+
* `plannable` is the list of issues whose subject's effective policy permits
|
|
186
|
+
* the planner to act on them (`managed`, or `tolerated` for whole-object
|
|
187
|
+
* creation issues only). Issues for `external`/`observed` subjects, and
|
|
188
|
+
* non-creation issues for `tolerated` subjects, are dropped from the planner's
|
|
189
|
+
* input entirely — they never enter introspection-driven planning, never feed
|
|
190
|
+
* the diff engine, and never produce DDL calls that would have to be
|
|
191
|
+
* post-filtered. This sidesteps a class of failure where the diff engine
|
|
192
|
+
* cannot reason about the live shape of a subject the user marked as
|
|
193
|
+
* out-of-scope (`external`).
|
|
194
|
+
*
|
|
195
|
+
* `warnings` is one {@link SqlPlannerConflict} per suppressed subject (not per
|
|
196
|
+
* suppressed issue). `factoryName` is inferred from the subject's issue mix:
|
|
197
|
+
* if any of the subject's issues is whole-object creation, the warning takes
|
|
198
|
+
* the corresponding creation factoryName (e.g. `createTable`,
|
|
199
|
+
* `createEnumType`, `createSchema`); otherwise it falls back to
|
|
200
|
+
* `defaultModificationFactoryName(subject)` — a synthetic label that names
|
|
201
|
+
* the *kind* of mutation that would have run, since no concrete DDL call was
|
|
202
|
+
* generated.
|
|
203
|
+
*
|
|
204
|
+
* Unresolved-subject issues (`resolveControlPolicySubject` returns
|
|
205
|
+
* `undefined`) emit one warning each; they cannot be deduplicated because
|
|
206
|
+
* they carry no subject coordinate.
|
|
207
|
+
*/
|
|
208
|
+
export function partitionIssuesByControlPolicy<TIssue>(options: {
|
|
209
|
+
readonly issues: readonly TIssue[];
|
|
210
|
+
readonly contract: Contract<SqlStorage>;
|
|
211
|
+
/**
|
|
212
|
+
* Resolve the subject targeted by this issue (or `undefined` to fail-closed:
|
|
213
|
+
* any policy stricter than `managed` drops the issue).
|
|
214
|
+
*/
|
|
215
|
+
readonly resolveControlPolicySubject: (issue: TIssue) => ControlPolicySubject | undefined;
|
|
216
|
+
/**
|
|
217
|
+
* Resolve a creation factoryName for this issue if it represents the
|
|
218
|
+
* absence of the whole top-level object (e.g. `'createTable'` for a
|
|
219
|
+
* missing-table issue). When the issue describes a modification to an
|
|
220
|
+
* existing object, return `undefined`. Both decisions feed off this signal:
|
|
221
|
+
*
|
|
222
|
+
* 1. Under `tolerated`, only issues whose `resolveCreationFactoryName`
|
|
223
|
+
* returns a value flow into the planner (create-if-absent).
|
|
224
|
+
* 2. Subjects that have at least one creation-flavoured issue use the
|
|
225
|
+
* resolved creation factoryName for their suppression warning;
|
|
226
|
+
* otherwise they fall back to `defaultModificationFactoryName`.
|
|
227
|
+
*/
|
|
228
|
+
readonly resolveCreationFactoryName: (issue: TIssue) => string | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* Default modification factoryName for a suppressed subject whose issues
|
|
231
|
+
* are all non-creation (the subject exists but has a different shape).
|
|
232
|
+
* Defaults to `'alterTable'` / `'alterType'` / `'alterSchema'` based on the
|
|
233
|
+
* subject's populated coordinates.
|
|
234
|
+
*/
|
|
235
|
+
readonly defaultModificationFactoryName?: (subject: ControlPolicySubject) => string;
|
|
236
|
+
readonly formatSubjectLabel?: (
|
|
237
|
+
factoryName: string,
|
|
238
|
+
subject: ControlPolicySubject | undefined,
|
|
239
|
+
) => string;
|
|
240
|
+
}): {
|
|
241
|
+
readonly plannable: readonly TIssue[];
|
|
242
|
+
readonly warnings: readonly SqlPlannerConflict[];
|
|
243
|
+
} {
|
|
244
|
+
const defaultControlPolicy = options.contract.defaultControlPolicy;
|
|
245
|
+
const formatSubjectLabel = options.formatSubjectLabel ?? defaultSubjectLabel;
|
|
246
|
+
const inferModificationFactoryName =
|
|
247
|
+
options.defaultModificationFactoryName ?? defaultModificationFactoryNameForSubject;
|
|
248
|
+
|
|
249
|
+
const plannable: TIssue[] = [];
|
|
250
|
+
// Resolved-subject suppressions are deduplicated by subject key so we emit
|
|
251
|
+
// one warning per suppressed subject, not one per suppressed issue.
|
|
252
|
+
// `creationFactoryName` upgrades from `undefined` to a concrete creation
|
|
253
|
+
// name the first time we see a creation-flavoured issue for the subject.
|
|
254
|
+
const suppressedSubjects = new Map<
|
|
255
|
+
string,
|
|
256
|
+
{
|
|
257
|
+
readonly subject: ControlPolicySubject;
|
|
258
|
+
readonly policy: ControlPolicy;
|
|
259
|
+
creationFactoryName?: string;
|
|
260
|
+
}
|
|
261
|
+
>();
|
|
262
|
+
const unresolvedSuppressions: SqlPlannerConflict[] = [];
|
|
263
|
+
|
|
264
|
+
for (const issue of options.issues) {
|
|
265
|
+
const subject = options.resolveControlPolicySubject(issue);
|
|
266
|
+
const policy = controlPolicyForCall(subject, defaultControlPolicy);
|
|
267
|
+
const creationFactoryName = options.resolveCreationFactoryName(issue);
|
|
268
|
+
|
|
269
|
+
if (policy === 'managed') {
|
|
270
|
+
plannable.push(issue);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (
|
|
274
|
+
policy === 'tolerated' &&
|
|
275
|
+
subject !== undefined &&
|
|
276
|
+
creationFactoryName !== undefined &&
|
|
277
|
+
subject.createsNewObject
|
|
278
|
+
) {
|
|
279
|
+
plannable.push(issue);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (subject === undefined) {
|
|
284
|
+
const factoryName = creationFactoryName ?? 'unknown';
|
|
285
|
+
unresolvedSuppressions.push(
|
|
286
|
+
buildSubjectSuppressionWarning(undefined, policy, factoryName, formatSubjectLabel),
|
|
287
|
+
);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const key = subjectKey(subject);
|
|
292
|
+
const existing = suppressedSubjects.get(key);
|
|
293
|
+
if (existing) {
|
|
294
|
+
if (existing.creationFactoryName === undefined && creationFactoryName !== undefined) {
|
|
295
|
+
existing.creationFactoryName = creationFactoryName;
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
suppressedSubjects.set(key, {
|
|
299
|
+
subject,
|
|
300
|
+
policy,
|
|
301
|
+
...ifDefined('creationFactoryName', creationFactoryName),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const warnings: SqlPlannerConflict[] = [...unresolvedSuppressions];
|
|
307
|
+
for (const entry of suppressedSubjects.values()) {
|
|
308
|
+
const factoryName = entry.creationFactoryName ?? inferModificationFactoryName(entry.subject);
|
|
309
|
+
warnings.push(
|
|
310
|
+
buildSubjectSuppressionWarning(entry.subject, entry.policy, factoryName, formatSubjectLabel),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return Object.freeze({
|
|
315
|
+
plannable: Object.freeze(plannable),
|
|
316
|
+
warnings: Object.freeze(warnings),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function subjectKey(subject: ControlPolicySubject): string {
|
|
321
|
+
return `${subject.namespaceId}\u0000${subject.table ?? ''}\u0000${subject.typeName ?? ''}`;
|
|
322
|
+
}
|
|
@@ -77,8 +77,8 @@ export function planFieldEventOperations(
|
|
|
77
77
|
for (const namespaceId of namespaceIds) {
|
|
78
78
|
const priorNs = priorContract?.storage.namespaces[namespaceId];
|
|
79
79
|
const newNs = newContract.storage.namespaces[namespaceId];
|
|
80
|
-
const priorTables = priorNs?.
|
|
81
|
-
const newTables = newNs?.
|
|
80
|
+
const priorTables = priorNs?.entries.table;
|
|
81
|
+
const newTables = newNs?.entries.table;
|
|
82
82
|
|
|
83
83
|
const tableNames = unionSorted(
|
|
84
84
|
priorTables ? Object.keys(priorTables) : [],
|
|
@@ -111,10 +111,26 @@ export function createMigrationPlan<TTargetDetails>(
|
|
|
111
111
|
|
|
112
112
|
export function plannerSuccess<TTargetDetails>(
|
|
113
113
|
plan: SqlMigrationPlan<TTargetDetails>,
|
|
114
|
+
warnings?: readonly SqlPlannerConflict[],
|
|
114
115
|
): SqlPlannerSuccessResult<TTargetDetails> {
|
|
115
116
|
return Object.freeze({
|
|
116
117
|
kind: 'success',
|
|
117
118
|
plan,
|
|
119
|
+
...(warnings && warnings.length > 0
|
|
120
|
+
? {
|
|
121
|
+
warnings: Object.freeze(
|
|
122
|
+
warnings.map((conflict) =>
|
|
123
|
+
Object.freeze({
|
|
124
|
+
kind: conflict.kind,
|
|
125
|
+
summary: conflict.summary,
|
|
126
|
+
...(conflict.why ? { why: conflict.why } : {}),
|
|
127
|
+
...(conflict.location ? { location: Object.freeze({ ...conflict.location }) } : {}),
|
|
128
|
+
...(conflict.meta ? { meta: cloneRecord(conflict.meta) } : {}),
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
118
134
|
});
|
|
119
135
|
}
|
|
120
136
|
|
|
@@ -4,7 +4,6 @@ import type {
|
|
|
4
4
|
ContractSerializer,
|
|
5
5
|
ContractSpace,
|
|
6
6
|
ControlAdapterDescriptor,
|
|
7
|
-
ControlDriverInstance,
|
|
8
7
|
ControlExtensionDescriptor,
|
|
9
8
|
MigratableTargetDescriptor,
|
|
10
9
|
MigrationOperationPolicy,
|
|
@@ -22,7 +21,9 @@ import type {
|
|
|
22
21
|
SchemaIssue,
|
|
23
22
|
SchemaVerifier,
|
|
24
23
|
} from '@prisma-next/framework-components/control';
|
|
24
|
+
import type { AggregateMigrationEdgeRef } from '@prisma-next/migration-tools/aggregate';
|
|
25
25
|
import type {
|
|
26
|
+
SqlControlDriverInstance,
|
|
26
27
|
SqlStorage,
|
|
27
28
|
StorageColumn,
|
|
28
29
|
StorageTable,
|
|
@@ -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>>;
|
|
@@ -115,7 +117,7 @@ export interface CodecControlHooks<TTargetDetails = unknown> {
|
|
|
115
117
|
readonly schemaName?: string;
|
|
116
118
|
}) => readonly SchemaIssue[];
|
|
117
119
|
introspectTypes?: (options: {
|
|
118
|
-
readonly driver:
|
|
120
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
119
121
|
readonly schemaName?: string;
|
|
120
122
|
}) => Promise<Record<string, StorageTypeInstance>>;
|
|
121
123
|
/**
|
|
@@ -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
|
|
|
@@ -268,9 +270,11 @@ export type SqlPlannerConflictKind =
|
|
|
268
270
|
| 'indexIncompatible'
|
|
269
271
|
| 'foreignKeyConflict'
|
|
270
272
|
| 'missingButNonAdditive'
|
|
271
|
-
| 'unsupportedOperation'
|
|
273
|
+
| 'unsupportedOperation'
|
|
274
|
+
| 'controlPolicySuppressedCall';
|
|
272
275
|
|
|
273
276
|
export interface SqlPlannerConflictLocation {
|
|
277
|
+
readonly namespace?: string;
|
|
274
278
|
readonly table?: string;
|
|
275
279
|
readonly column?: string;
|
|
276
280
|
readonly index?: string;
|
|
@@ -349,7 +353,7 @@ export interface SqlMigrationRunnerExecuteCallbacks<TTargetDetails> {
|
|
|
349
353
|
|
|
350
354
|
export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
|
|
351
355
|
readonly plan: SqlMigrationPlan<TTargetDetails>;
|
|
352
|
-
readonly driver:
|
|
356
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
353
357
|
/**
|
|
354
358
|
* Logical contract space this plan applies to. When omitted the
|
|
355
359
|
* runner derives the space from {@link SqlMigrationPlan.spaceId};
|
|
@@ -384,12 +388,18 @@ export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
|
|
|
384
388
|
* All components must have matching familyId ('sql') and targetId.
|
|
385
389
|
*/
|
|
386
390
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
391
|
+
/**
|
|
392
|
+
* Per-edge breakdown from graph-walk planning. When present, the runner
|
|
393
|
+
* writes one ledger row per edge instead of one collapsed row per apply.
|
|
394
|
+
*/
|
|
395
|
+
readonly migrationEdges: readonly AggregateMigrationEdgeRef[];
|
|
387
396
|
}
|
|
388
397
|
|
|
389
398
|
export type SqlMigrationRunnerErrorCode =
|
|
390
399
|
| 'DESTINATION_CONTRACT_MISMATCH'
|
|
391
400
|
| 'LEGACY_MARKER_SHAPE'
|
|
392
401
|
| 'MARKER_ORIGIN_MISMATCH'
|
|
402
|
+
| 'MARKER_CAS_FAILURE'
|
|
393
403
|
| 'POLICY_VIOLATION'
|
|
394
404
|
| 'PRECHECK_FAILED'
|
|
395
405
|
| 'POSTCHECK_FAILED'
|
|
@@ -424,7 +434,7 @@ export interface SqlMigrationRunner<TTargetDetails> {
|
|
|
424
434
|
* (the connection the outer transaction is open on).
|
|
425
435
|
*/
|
|
426
436
|
execute(options: {
|
|
427
|
-
readonly driver:
|
|
437
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
428
438
|
readonly perSpaceOptions: ReadonlyArray<SqlMigrationRunnerExecuteOptions<TTargetDetails>>;
|
|
429
439
|
}): Promise<MigrationRunnerResult>;
|
|
430
440
|
|
|
@@ -463,7 +473,7 @@ export interface SqlControlTargetDescriptor<
|
|
|
463
473
|
* the base, the target-specific dispatch on the subclass.
|
|
464
474
|
*/
|
|
465
475
|
readonly schemaVerifier: SchemaVerifier<TContract, SqlSchemaIR>;
|
|
466
|
-
createPlanner(
|
|
476
|
+
createPlanner(adapter: SqlControlAdapter<TTargetId>): SqlMigrationPlanner<TTargetDetails>;
|
|
467
477
|
createRunner(family: SqlControlFamilyInstance): SqlMigrationRunner<TTargetDetails>;
|
|
468
478
|
}
|
|
469
479
|
|
|
@@ -12,7 +12,11 @@ import type {
|
|
|
12
12
|
PslSpan,
|
|
13
13
|
PslTypesBlock,
|
|
14
14
|
} from '@prisma-next/framework-components/psl-ast';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
makePslNamespace,
|
|
17
|
+
makePslNamespaceEntries,
|
|
18
|
+
UNSPECIFIED_PSL_NAMESPACE_ID,
|
|
19
|
+
} from '@prisma-next/framework-components/psl-ast';
|
|
16
20
|
import type { SqlColumnIR, SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
|
|
17
21
|
import type { DefaultMappingOptions } from './default-mapping';
|
|
18
22
|
import { mapDefault } from './default-mapping';
|
|
@@ -164,14 +168,12 @@ function buildPslDocumentAst(schemaIR: SqlSchemaIR, options: PslPrinterOptions):
|
|
|
164
168
|
kind: 'document',
|
|
165
169
|
sourceId: '<sql-schema-ir>',
|
|
166
170
|
namespaces: [
|
|
167
|
-
{
|
|
171
|
+
makePslNamespace({
|
|
168
172
|
kind: 'namespace',
|
|
169
173
|
name: UNSPECIFIED_PSL_NAMESPACE_ID,
|
|
170
|
-
|
|
171
|
-
enums,
|
|
172
|
-
compositeTypes: [],
|
|
174
|
+
entries: makePslNamespaceEntries(sortedModels, enums, [], []),
|
|
173
175
|
span: SYNTHETIC_SPAN,
|
|
174
|
-
},
|
|
176
|
+
}),
|
|
175
177
|
],
|
|
176
178
|
...(types ? { types } : {}),
|
|
177
179
|
span: SYNTHETIC_SPAN,
|