@prisma-next/family-sql 0.14.0-dev.9 → 0.14.0-dev.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{authoring-type-constructors-CjFfO6LM.mjs → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
- package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
- package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-BQgad8Zc.d.mts} +30 -8
- package/dist/control-adapter-BQgad8Zc.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +1 -1
- package/dist/control.d.mts +279 -34
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +378 -1005
- package/dist/control.mjs.map +1 -1
- package/dist/diff.d.mts +102 -0
- package/dist/diff.d.mts.map +1 -0
- package/dist/diff.mjs +14 -0
- package/dist/diff.mjs.map +1 -0
- package/dist/foreign-key-index-backing-BP71iI-Q.mjs +32 -0
- package/dist/foreign-key-index-backing-BP71iI-Q.mjs.map +1 -0
- package/dist/ir.d.mts +26 -9
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +2 -2
- package/dist/ir.mjs.map +1 -1
- package/dist/migration.d.mts +11 -2
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +7 -0
- package/dist/migration.mjs.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/psl-infer.d.mts +119 -0
- package/dist/psl-infer.d.mts.map +1 -0
- package/dist/psl-infer.mjs +366 -0
- package/dist/psl-infer.mjs.map +1 -0
- package/dist/schema-differ-DnoopSXm.d.mts +45 -0
- package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
- package/dist/schema-verify-W3r631Jh.mjs +226 -0
- package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
- package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-C75cfMSS.mjs} +46 -20
- package/dist/sql-contract-serializer-C75cfMSS.mjs.map +1 -0
- package/dist/{types-kgstZ_Zd.d.mts → types-BPv_y7iS.d.mts} +36 -142
- package/dist/types-BPv_y7iS.d.mts.map +1 -0
- package/package.json +26 -26
- package/src/core/authoring-entity-types.ts +12 -37
- package/src/core/control-adapter.ts +11 -5
- package/src/core/control-instance.ts +212 -42
- package/src/core/control-target-descriptor.ts +98 -0
- package/src/core/diff/schema-verify.ts +324 -0
- package/src/core/diff/sql-schema-diff.ts +41 -0
- package/src/core/diff/verifier-disposition.ts +30 -0
- package/src/core/foreign-key-index-backing.ts +37 -0
- package/src/core/ir/sql-contract-serializer-base.ts +70 -56
- package/src/core/ir/sql-contract-serializer.ts +5 -7
- package/src/core/ir/sql-schema-verifier-base.ts +5 -5
- package/src/core/migrations/contract-to-schema-ir.ts +141 -37
- package/src/core/migrations/control-policy.ts +66 -115
- package/src/core/migrations/field-event-planner.ts +2 -2
- package/src/core/migrations/native-type-expander.ts +28 -0
- package/src/core/migrations/schema-differ.ts +41 -0
- package/src/core/migrations/types.ts +33 -37
- package/src/core/psl-contract-infer/name-transforms.ts +15 -0
- package/src/core/psl-contract-infer/printer-config.ts +14 -6
- package/src/core/psl-contract-infer/relation-inference.ts +27 -1
- package/src/core/sql-migration.ts +12 -1
- package/src/exports/control.ts +12 -2
- package/src/exports/diff.ts +25 -0
- package/src/exports/psl-infer.ts +40 -0
- package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
- package/dist/control-adapter-Cmw9LvEP.d.mts.map +0 -1
- package/dist/schema-verify.d.mts +0 -39
- package/dist/schema-verify.d.mts.map +0 -1
- package/dist/schema-verify.mjs +0 -2
- package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
- package/dist/test-utils.d.mts +0 -2
- package/dist/test-utils.mjs +0 -2
- package/dist/types-kgstZ_Zd.d.mts.map +0 -1
- package/dist/verify-sql-schema-thU-jKpf.d.mts +0 -66
- package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
- package/dist/verify-sql-schema-xT4udQLQ.mjs +0 -1501
- package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
- package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
- package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
- package/src/core/schema-verify/control-verify-emit.ts +0 -46
- package/src/core/schema-verify/verifier-disposition.ts +0 -58
- package/src/core/schema-verify/verify-helpers.ts +0 -820
- package/src/core/schema-verify/verify-sql-schema.ts +0 -1311
- package/src/exports/schema-verify.ts +0 -18
- package/src/exports/test-utils.ts +0 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
SchemaDiffIssue,
|
|
3
3
|
SchemaVerifier,
|
|
4
4
|
SchemaVerifyOptions,
|
|
5
5
|
SchemaVerifyResult,
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
* SQL family `SchemaVerifier` abstract base. Centralises the SQL-shared
|
|
10
10
|
* walk (table-by-table + column-by-column matching keyed by
|
|
11
11
|
* `(namespace.id, name)`, FK / unique / index comparisons via the
|
|
12
|
-
*
|
|
12
|
+
* shared satisfaction helpers in `sql-schema-diff.ts`) and exposes a protected hook
|
|
13
13
|
* for target extensions (Postgres functions, RLS policies, future
|
|
14
14
|
* target-only kinds).
|
|
15
15
|
*
|
|
@@ -29,7 +29,7 @@ export abstract class SqlSchemaVerifierBase<TContract, TSchema>
|
|
|
29
29
|
implements SchemaVerifier<TContract, TSchema>
|
|
30
30
|
{
|
|
31
31
|
verifySchema(options: SchemaVerifyOptions<TContract, TSchema>): SchemaVerifyResult {
|
|
32
|
-
const issues:
|
|
32
|
+
const issues: SchemaDiffIssue[] = [];
|
|
33
33
|
issues.push(...this.verifyCommonSqlSchema(options));
|
|
34
34
|
issues.push(...this.verifyTargetExtensions(options));
|
|
35
35
|
return { ok: issues.length === 0, issues };
|
|
@@ -43,7 +43,7 @@ export abstract class SqlSchemaVerifierBase<TContract, TSchema>
|
|
|
43
43
|
*/
|
|
44
44
|
protected abstract verifyCommonSqlSchema(
|
|
45
45
|
options: SchemaVerifyOptions<TContract, TSchema>,
|
|
46
|
-
): readonly
|
|
46
|
+
): readonly SchemaDiffIssue[];
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Target-specific extensions — e.g. Postgres functions, future RLS
|
|
@@ -52,5 +52,5 @@ export abstract class SqlSchemaVerifierBase<TContract, TSchema>
|
|
|
52
52
|
*/
|
|
53
53
|
protected abstract verifyTargetExtensions(
|
|
54
54
|
options: SchemaVerifyOptions<TContract, TSchema>,
|
|
55
|
-
): readonly
|
|
55
|
+
): readonly SchemaDiffIssue[];
|
|
56
56
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ColumnDefault, Contract, JsonValue } from '@prisma-next/contract/types';
|
|
2
|
+
import type { CodecRef } from '@prisma-next/framework-components/codec';
|
|
2
3
|
import type { MigrationPlannerConflict } from '@prisma-next/framework-components/control';
|
|
3
4
|
import {
|
|
4
5
|
type CheckConstraint,
|
|
@@ -12,17 +13,19 @@ import {
|
|
|
12
13
|
type UniqueConstraint,
|
|
13
14
|
} from '@prisma-next/sql-contract/types';
|
|
14
15
|
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
15
|
-
import
|
|
16
|
-
SqlAnnotations,
|
|
17
|
-
SqlCheckConstraintIRInput,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
import {
|
|
17
|
+
type SqlAnnotations,
|
|
18
|
+
type SqlCheckConstraintIRInput,
|
|
19
|
+
type SqlColumnIRInput,
|
|
20
|
+
type SqlForeignKeyIRInput,
|
|
21
|
+
type SqlIndexIRInput,
|
|
21
22
|
SqlSchemaIR,
|
|
22
23
|
SqlTableIR,
|
|
23
|
-
|
|
24
|
+
type SqlUniqueIRInput,
|
|
24
25
|
} from '@prisma-next/sql-schema-ir/types';
|
|
26
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
25
27
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
28
|
+
import { backingIndexColumnKeys } from '../foreign-key-index-backing';
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* Target-specific callback that expands a column's base `nativeType` and optional
|
|
@@ -72,7 +75,7 @@ function convertColumn(
|
|
|
72
75
|
storageTypes: ResolvedStorageTypes,
|
|
73
76
|
expandNativeType: NativeTypeExpander | undefined,
|
|
74
77
|
renderDefault: DefaultRenderer | undefined,
|
|
75
|
-
):
|
|
78
|
+
): SqlColumnIRInput {
|
|
76
79
|
// Resolve `typeRef` so columns that delegate their `nativeType`/`codecId`/
|
|
77
80
|
// `typeParams` to a named `storage.types` entry expand the same way as
|
|
78
81
|
// columns that inline those fields. Without this resolution, a
|
|
@@ -82,21 +85,72 @@ function convertColumn(
|
|
|
82
85
|
// typeRef and produces `"vector(1536)"` — making diffs on the same
|
|
83
86
|
// contract falsely report a `type_mismatch`.
|
|
84
87
|
const resolved = resolveColumnTypeMetadata(column, storageTypes);
|
|
85
|
-
const
|
|
88
|
+
const baseNativeType = expandNativeType
|
|
86
89
|
? expandNativeType({
|
|
87
90
|
nativeType: resolved.nativeType,
|
|
88
91
|
codecId: resolved.codecId,
|
|
89
92
|
...ifDefined('typeParams', resolved.typeParams),
|
|
90
93
|
})
|
|
91
94
|
: resolved.nativeType;
|
|
95
|
+
// `many: true` columns keep `nativeType` as the bare element type (matching
|
|
96
|
+
// how the introspected/"actual" side reports it — see the postgres control
|
|
97
|
+
// adapter's own `many`-stripping normalization) and carry the array-ness in
|
|
98
|
+
// the separate `many` field instead of baking `[]` into `nativeType`.
|
|
99
|
+
// Baking it in here (`"text[]"`) made every `@@` list-typed column
|
|
100
|
+
// permanently mismatch against a live introspected column reporting
|
|
101
|
+
// `{ nativeType: "text", many: true }`, regardless of whether the two are
|
|
102
|
+
// otherwise identical — `db verify` reported every such column
|
|
103
|
+
// `not-equal`. `resolvedNativeType` still carries the full `"text[]"`
|
|
104
|
+
// form: it is the side both this and the introspected column already
|
|
105
|
+
// agree on as the comparable "expanded" type.
|
|
106
|
+
const nativeType = baseNativeType;
|
|
107
|
+
const resolvedNativeType = column.many ? `${baseNativeType}[]` : baseNativeType;
|
|
92
108
|
return {
|
|
93
109
|
name,
|
|
94
110
|
nativeType,
|
|
95
111
|
nullable: column.nullable,
|
|
112
|
+
...ifDefined('many', column.many),
|
|
96
113
|
...ifDefined(
|
|
97
114
|
'default',
|
|
98
115
|
column.default != null && renderDefault ? renderDefault(column.default, column) : undefined,
|
|
99
116
|
),
|
|
117
|
+
// Contract-derived columns are resolved by construction: the computed
|
|
118
|
+
// full native type doubles as the resolved value, and the contract's
|
|
119
|
+
// structured default is the resolved default (the introspected side
|
|
120
|
+
// stamps its normalizer's parse of the raw expression).
|
|
121
|
+
resolvedNativeType,
|
|
122
|
+
...ifDefined('resolvedDefault', column.default ?? undefined),
|
|
123
|
+
// The column's codec identity, carried the same way the query AST
|
|
124
|
+
// carries `CodecRef` (TML-2456) — the migration planner's op-builders
|
|
125
|
+
// resolve DDL rendering from this at plan time (Decision 5), instead of
|
|
126
|
+
// reading a derivation-precomputed render payload.
|
|
127
|
+
codecRef: buildColumnCodecRef(resolved, column.many),
|
|
128
|
+
codecBaseNativeType: resolved.nativeType,
|
|
129
|
+
...(column.typeRef !== undefined ? { codecNamedType: true } : {}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Builds the column's `CodecRef` from its resolved (post-`typeRef`) codec
|
|
135
|
+
* identity — the same construction the query AST and the migration DDL
|
|
136
|
+
* renderer already use (TML-2456, TML-2918).
|
|
137
|
+
*/
|
|
138
|
+
function buildColumnCodecRef(
|
|
139
|
+
resolved: Pick<StorageColumn, 'codecId' | 'nativeType' | 'typeParams'>,
|
|
140
|
+
many: boolean | undefined,
|
|
141
|
+
): CodecRef {
|
|
142
|
+
return {
|
|
143
|
+
codecId: resolved.codecId,
|
|
144
|
+
...ifDefined(
|
|
145
|
+
'typeParams',
|
|
146
|
+
resolved.typeParams !== undefined
|
|
147
|
+
? blindCast<
|
|
148
|
+
JsonValue,
|
|
149
|
+
'resolved.typeParams is JsonValue-shaped storage metadata; the narrowed (non-undefined) value lands in CodecRef.typeParams which is JsonValue'
|
|
150
|
+
>(resolved.typeParams)
|
|
151
|
+
: undefined,
|
|
152
|
+
),
|
|
153
|
+
...ifDefined('many', many),
|
|
100
154
|
};
|
|
101
155
|
}
|
|
102
156
|
|
|
@@ -192,26 +246,42 @@ function convertCheck(check: CheckConstraint, storage: SqlStorage): SqlCheckCons
|
|
|
192
246
|
};
|
|
193
247
|
}
|
|
194
248
|
|
|
195
|
-
function convertUnique(unique: UniqueConstraint):
|
|
249
|
+
function convertUnique(unique: UniqueConstraint): SqlUniqueIRInput {
|
|
196
250
|
return {
|
|
197
251
|
columns: unique.columns,
|
|
198
252
|
...ifDefined('name', unique.name),
|
|
199
253
|
};
|
|
200
254
|
}
|
|
201
255
|
|
|
202
|
-
function convertIndex(index: Index):
|
|
256
|
+
function convertIndex(index: Index): SqlIndexIRInput {
|
|
203
257
|
return {
|
|
204
258
|
columns: index.columns,
|
|
205
259
|
unique: false,
|
|
206
260
|
...ifDefined('name', index.name),
|
|
261
|
+
// Carried so the derived index node compares type/options against the
|
|
262
|
+
// introspected side (the legacy walk read them from the contract).
|
|
263
|
+
...ifDefined('type', index.type),
|
|
264
|
+
...ifDefined('options', index.options),
|
|
207
265
|
};
|
|
208
266
|
}
|
|
209
267
|
|
|
210
|
-
|
|
268
|
+
/**
|
|
269
|
+
* The FK's referenced-namespace identity comes from the target's namespace
|
|
270
|
+
* node, not the raw namespace-id string. An unbound target namespace stamps
|
|
271
|
+
* no `referencedSchema` at all — the FK node's id renders the absence as the
|
|
272
|
+
* empty segment, which is what flat (single-schema) introspection produces,
|
|
273
|
+
* so both diff sides' FK ids meet by construction. A bound namespace (or a
|
|
274
|
+
* cross-space target whose namespace lives in another contract's storage)
|
|
275
|
+
* stamps its coordinate verbatim; namespaced targets (Postgres) resolve the
|
|
276
|
+
* real DDL schema downstream.
|
|
277
|
+
*/
|
|
278
|
+
function convertForeignKey(fk: ForeignKey, storage: SqlStorage): SqlForeignKeyIRInput {
|
|
279
|
+
const targetNamespace = storage.namespaces[fk.target.namespaceId];
|
|
280
|
+
const targetIsUnbound = targetNamespace?.isUnbound === true;
|
|
211
281
|
return {
|
|
212
282
|
columns: fk.source.columns,
|
|
213
283
|
referencedTable: fk.target.tableName,
|
|
214
|
-
referencedSchema: fk.target.namespaceId,
|
|
284
|
+
...(targetIsUnbound ? {} : { referencedSchema: fk.target.namespaceId }),
|
|
215
285
|
referencedColumns: fk.target.columns,
|
|
216
286
|
...ifDefined('name', fk.name),
|
|
217
287
|
...ifDefined('onDelete', fk.onDelete),
|
|
@@ -227,7 +297,7 @@ function convertTable(
|
|
|
227
297
|
renderDefault: DefaultRenderer | undefined,
|
|
228
298
|
storage: SqlStorage,
|
|
229
299
|
): SqlTableIR {
|
|
230
|
-
const columns: Record<string,
|
|
300
|
+
const columns: Record<string, SqlColumnIRInput> = {};
|
|
231
301
|
for (const [colName, colDef] of Object.entries(table.columns)) {
|
|
232
302
|
columns[colName] = convertColumn(
|
|
233
303
|
colName,
|
|
@@ -238,12 +308,8 @@ function convertTable(
|
|
|
238
308
|
);
|
|
239
309
|
}
|
|
240
310
|
|
|
241
|
-
const satisfiedIndexColumns = new Set(
|
|
242
|
-
|
|
243
|
-
...table.uniques.map((unique) => unique.columns.join(',')),
|
|
244
|
-
...(table.primaryKey ? [table.primaryKey.columns.join(',')] : []),
|
|
245
|
-
]);
|
|
246
|
-
const fkBackingIndexes: SqlIndexIR[] = [];
|
|
311
|
+
const satisfiedIndexColumns = new Set(backingIndexColumnKeys(table));
|
|
312
|
+
const fkBackingIndexes: SqlIndexIRInput[] = [];
|
|
247
313
|
for (const fk of table.foreignKeys) {
|
|
248
314
|
if (fk.index === false) continue;
|
|
249
315
|
const key = fk.source.columns.join(',');
|
|
@@ -261,15 +327,17 @@ function convertTable(
|
|
|
261
327
|
? table.checks.map((c) => convertCheck(c, storage))
|
|
262
328
|
: undefined;
|
|
263
329
|
|
|
264
|
-
return {
|
|
330
|
+
return new SqlTableIR({
|
|
265
331
|
name,
|
|
266
332
|
columns,
|
|
267
333
|
...ifDefined('primaryKey', table.primaryKey),
|
|
268
|
-
foreignKeys: table.foreignKeys
|
|
334
|
+
foreignKeys: table.foreignKeys
|
|
335
|
+
.filter((fk) => fk.constraint !== false)
|
|
336
|
+
.map((fk) => convertForeignKey(fk, storage)),
|
|
269
337
|
uniques: table.uniques.map(convertUnique),
|
|
270
338
|
indexes: [...table.indexes.map(convertIndex), ...fkBackingIndexes],
|
|
271
339
|
...ifDefined('checks', checks),
|
|
272
|
-
};
|
|
340
|
+
});
|
|
273
341
|
}
|
|
274
342
|
|
|
275
343
|
/**
|
|
@@ -303,7 +371,7 @@ export function detectDestructiveChanges(
|
|
|
303
371
|
|
|
304
372
|
for (const tableName of Object.keys(fromTables)) {
|
|
305
373
|
const toTableRaw = toNs?.entries.table?.[tableName];
|
|
306
|
-
if (!(toTableRaw
|
|
374
|
+
if (!StorageTable.is(toTableRaw)) {
|
|
307
375
|
conflicts.push({
|
|
308
376
|
kind: 'tableRemoved',
|
|
309
377
|
summary: `Table "${tableName}" was removed`,
|
|
@@ -313,7 +381,7 @@ export function detectDestructiveChanges(
|
|
|
313
381
|
const toTable = toTableRaw;
|
|
314
382
|
|
|
315
383
|
const fromTableRaw = fromTables[tableName];
|
|
316
|
-
if (!(fromTableRaw
|
|
384
|
+
if (!StorageTable.is(fromTableRaw)) continue;
|
|
317
385
|
const fromTable = fromTableRaw;
|
|
318
386
|
|
|
319
387
|
for (const columnName of Object.keys(fromTable.columns)) {
|
|
@@ -358,6 +426,48 @@ export interface ContractToSchemaIROptions {
|
|
|
358
426
|
*
|
|
359
427
|
* Returns an empty schema IR when `contract` is `null` (new project).
|
|
360
428
|
*/
|
|
429
|
+
/**
|
|
430
|
+
* Converts the tables of a single namespace into a `SqlSchemaIR`, keyed by
|
|
431
|
+
* table name within that namespace. Unlike {@link contractToSchemaIR}, which
|
|
432
|
+
* flattens every namespace's tables into one bare-keyed record (and throws on a
|
|
433
|
+
* cross-namespace name collision), this scopes the table iteration to one
|
|
434
|
+
* namespace so the same table name can exist in two schemas.
|
|
435
|
+
*
|
|
436
|
+
* The full `storage` is still passed to `convertTable`, so value-set / enum /
|
|
437
|
+
* type resolution that legitimately spans namespaces is unaffected. Foreign
|
|
438
|
+
* keys are built purely from the FK descriptor (`fk.target`), so cross-namespace
|
|
439
|
+
* FKs survive per-namespace conversion. The `annotations` block (storage-type
|
|
440
|
+
* derived) is omitted here — the per-namespace tree consumer reads only the
|
|
441
|
+
* per-table fields.
|
|
442
|
+
*/
|
|
443
|
+
export function contractNamespaceToSchemaIR(
|
|
444
|
+
storage: SqlStorage,
|
|
445
|
+
namespaceId: string,
|
|
446
|
+
options: ContractToSchemaIROptions,
|
|
447
|
+
): SqlSchemaIR {
|
|
448
|
+
if (options.annotationNamespace.length === 0) {
|
|
449
|
+
throw new Error('annotationNamespace must be a non-empty string');
|
|
450
|
+
}
|
|
451
|
+
const namespace = storage.namespaces[namespaceId];
|
|
452
|
+
if (!namespace) {
|
|
453
|
+
return new SqlSchemaIR({ tables: {} });
|
|
454
|
+
}
|
|
455
|
+
const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
|
|
456
|
+
const tables: Record<string, SqlTableIR> = {};
|
|
457
|
+
for (const [tableName, tableDefRaw] of Object.entries(namespace.entries.table ?? {})) {
|
|
458
|
+
StorageTable.assert(tableDefRaw, `namespaces.${namespaceId}.entries.table.${tableName}`);
|
|
459
|
+
tables[tableName] = convertTable(
|
|
460
|
+
tableName,
|
|
461
|
+
tableDefRaw,
|
|
462
|
+
storageTypes,
|
|
463
|
+
options.expandNativeType,
|
|
464
|
+
options.renderDefault,
|
|
465
|
+
storage,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return new SqlSchemaIR({ tables });
|
|
469
|
+
}
|
|
470
|
+
|
|
361
471
|
export function contractToSchemaIR(
|
|
362
472
|
contract: Contract<SqlStorage> | null,
|
|
363
473
|
options: ContractToSchemaIROptions,
|
|
@@ -367,21 +477,15 @@ export function contractToSchemaIR(
|
|
|
367
477
|
}
|
|
368
478
|
|
|
369
479
|
if (!contract) {
|
|
370
|
-
return { tables: {} };
|
|
480
|
+
return new SqlSchemaIR({ tables: {} });
|
|
371
481
|
}
|
|
372
482
|
|
|
373
483
|
const storage = contract.storage;
|
|
374
|
-
const storageTypes: ResolvedStorageTypes = {
|
|
375
|
-
...((storage.types ?? {}) as ResolvedStorageTypes),
|
|
376
|
-
};
|
|
484
|
+
const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
|
|
377
485
|
const tables: Record<string, SqlTableIR> = {};
|
|
378
486
|
for (const ns of Object.values(storage.namespaces)) {
|
|
379
487
|
for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table ?? {})) {
|
|
380
|
-
|
|
381
|
-
throw new Error(
|
|
382
|
-
`contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.entries.table.${tableName}`,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
488
|
+
StorageTable.assert(tableDefRaw, `namespaces.${ns.id}.entries.table.${tableName}`);
|
|
385
489
|
const tableDef = tableDefRaw;
|
|
386
490
|
if (tables[tableName] !== undefined) {
|
|
387
491
|
throw new Error(
|
|
@@ -405,10 +509,10 @@ export function contractToSchemaIR(
|
|
|
405
509
|
options.resolveEnumNamespaceSchema,
|
|
406
510
|
);
|
|
407
511
|
|
|
408
|
-
return {
|
|
512
|
+
return new SqlSchemaIR({
|
|
409
513
|
tables,
|
|
410
514
|
...ifDefined('annotations', annotations),
|
|
411
|
-
};
|
|
515
|
+
});
|
|
412
516
|
}
|
|
413
517
|
|
|
414
518
|
function deriveAnnotations(
|
|
@@ -418,7 +522,7 @@ function deriveAnnotations(
|
|
|
418
522
|
): SqlAnnotations | undefined {
|
|
419
523
|
const storageTypes: Record<string, StorageTypeInstance> = {};
|
|
420
524
|
|
|
421
|
-
for (const typeInstance of Object.values(
|
|
525
|
+
for (const typeInstance of Object.values(storage.types ?? {})) {
|
|
422
526
|
if (isStorageTypeInstance(typeInstance)) {
|
|
423
527
|
storageTypes[typeInstance.nativeType] = typeInstance;
|
|
424
528
|
}
|
|
@@ -5,20 +5,25 @@ import {
|
|
|
5
5
|
} from '@prisma-next/contract/types';
|
|
6
6
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
7
7
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
8
|
-
import type { SqlPlannerConflict } from './types';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* The target object a control policy governs for a single planner call,
|
|
12
11
|
* resolved from the target's own IR. `undefined` means the call's target
|
|
13
12
|
* object could not be positively established — a fail-closed signal: any
|
|
14
13
|
* policy stricter than `managed` drops such a call rather than emitting it.
|
|
14
|
+
*
|
|
15
|
+
* The object identity is a generic storage coordinate — `(entityKind,
|
|
16
|
+
* entityName)`, absent for a whole-namespace subject — never a target-specific
|
|
17
|
+
* `table`/`typeName` field. The family reasons about the coordinate for policy
|
|
18
|
+
* and deduplication; the target owns any vocabulary (table vs enum) when it
|
|
19
|
+
* renders a suppression.
|
|
15
20
|
*/
|
|
16
21
|
export interface ControlPolicySubject {
|
|
17
22
|
readonly namespaceId: string;
|
|
18
|
-
readonly
|
|
19
|
-
readonly
|
|
23
|
+
readonly entityKind?: string;
|
|
24
|
+
readonly entityName?: string;
|
|
20
25
|
readonly column?: string;
|
|
21
|
-
readonly
|
|
26
|
+
readonly explicitNodeControlPolicy?: ControlPolicy;
|
|
22
27
|
/**
|
|
23
28
|
* Whether the call creates a whole, previously-absent top-level storage
|
|
24
29
|
* object (e.g. a table or an enum/type), as opposed to modifying an
|
|
@@ -29,6 +34,26 @@ export interface ControlPolicySubject {
|
|
|
29
34
|
readonly createsNewObject: boolean;
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The structured record a control-policy partition emits for one suppressed
|
|
39
|
+
* subject or call. It carries only raw data — no strings, no vocabulary — so
|
|
40
|
+
* the consumer (the target) renders the user-facing message itself:
|
|
41
|
+
*
|
|
42
|
+
* - `subject` — the suppressed subject (its coordinate + namespace), or
|
|
43
|
+
* `undefined` when the subject could not be resolved (fail-closed).
|
|
44
|
+
* - `policy` — the effective control policy that caused the suppression.
|
|
45
|
+
* - `factoryName` — the resolved creation factory name if the suppressed thing
|
|
46
|
+
* was a whole-object creation (e.g. `createTable`), else `undefined`. The
|
|
47
|
+
* family never invents a verb for a modification that produced no call.
|
|
48
|
+
* - `createsNewObject` — whether the suppressed thing was object creation.
|
|
49
|
+
*/
|
|
50
|
+
export interface SuppressionRecord {
|
|
51
|
+
readonly subject: ControlPolicySubject | undefined;
|
|
52
|
+
readonly policy: ControlPolicy;
|
|
53
|
+
readonly factoryName: string | undefined;
|
|
54
|
+
readonly createsNewObject: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
/**
|
|
33
58
|
* The control policy that governs a single call. The `external` default is an
|
|
34
59
|
* un-overridable namespace floor: when the contract default is `external`, no
|
|
@@ -71,67 +96,10 @@ function callAllowedUnderControlPolicy(
|
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
|
|
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
99
|
/**
|
|
132
100
|
* Partition the calls produced for a single set of subjects into those the
|
|
133
101
|
* effective control policy permits (`kept`) and a list of
|
|
134
|
-
* {@link
|
|
102
|
+
* {@link SuppressionRecord}s describing the suppressed calls.
|
|
135
103
|
*
|
|
136
104
|
* **Prefer {@link partitionIssuesByControlPolicy}** for the schema-issue
|
|
137
105
|
* pipeline: it filters subjects out of the planner's *input* so the planner
|
|
@@ -146,18 +114,13 @@ export function partitionCallsByControlPolicy<TCall>(options: {
|
|
|
146
114
|
readonly contract: Contract<SqlStorage>;
|
|
147
115
|
readonly resolveControlPolicySubject: (call: TCall) => ControlPolicySubject | undefined;
|
|
148
116
|
readonly resolveFactoryName: (call: TCall) => string;
|
|
149
|
-
readonly formatSubjectLabel?: (
|
|
150
|
-
factoryName: string,
|
|
151
|
-
subject: ControlPolicySubject | undefined,
|
|
152
|
-
) => string;
|
|
153
117
|
}): {
|
|
154
118
|
readonly kept: readonly TCall[];
|
|
155
|
-
readonly
|
|
119
|
+
readonly suppressions: readonly SuppressionRecord[];
|
|
156
120
|
} {
|
|
157
121
|
const defaultControlPolicy = options.contract.defaultControlPolicy;
|
|
158
|
-
const formatSubjectLabel = options.formatSubjectLabel ?? defaultSubjectLabel;
|
|
159
122
|
const kept: TCall[] = [];
|
|
160
|
-
const
|
|
123
|
+
const suppressions: SuppressionRecord[] = [];
|
|
161
124
|
|
|
162
125
|
for (const call of options.calls) {
|
|
163
126
|
const subject = options.resolveControlPolicySubject(call);
|
|
@@ -165,16 +128,18 @@ export function partitionCallsByControlPolicy<TCall>(options: {
|
|
|
165
128
|
if (callAllowedUnderControlPolicy(policy, subject)) {
|
|
166
129
|
kept.push(call);
|
|
167
130
|
} else {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
131
|
+
suppressions.push({
|
|
132
|
+
subject,
|
|
133
|
+
policy,
|
|
134
|
+
factoryName: options.resolveFactoryName(call),
|
|
135
|
+
createsNewObject: subject?.createsNewObject ?? false,
|
|
136
|
+
});
|
|
172
137
|
}
|
|
173
138
|
}
|
|
174
139
|
|
|
175
140
|
return Object.freeze({
|
|
176
141
|
kept: Object.freeze(kept),
|
|
177
|
-
|
|
142
|
+
suppressions: Object.freeze(suppressions),
|
|
178
143
|
});
|
|
179
144
|
}
|
|
180
145
|
|
|
@@ -192,18 +157,15 @@ export function partitionCallsByControlPolicy<TCall>(options: {
|
|
|
192
157
|
* cannot reason about the live shape of a subject the user marked as
|
|
193
158
|
* out-of-scope (`external`).
|
|
194
159
|
*
|
|
195
|
-
* `
|
|
196
|
-
* suppressed issue). `factoryName` is
|
|
197
|
-
*
|
|
198
|
-
* the
|
|
199
|
-
*
|
|
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.
|
|
160
|
+
* `suppressions` is one {@link SuppressionRecord} per suppressed subject (not
|
|
161
|
+
* per suppressed issue). Its `factoryName` is the creation factory name when any
|
|
162
|
+
* of the subject's issues is whole-object creation (e.g. `createTable`), else
|
|
163
|
+
* `undefined` — the family never invents a modification verb for an op that
|
|
164
|
+
* produced no call; the target renders the message.
|
|
203
165
|
*
|
|
204
166
|
* Unresolved-subject issues (`resolveControlPolicySubject` returns
|
|
205
|
-
* `undefined`) emit one
|
|
206
|
-
*
|
|
167
|
+
* `undefined`) emit one record each; they cannot be deduplicated because they
|
|
168
|
+
* carry no subject coordinate.
|
|
207
169
|
*/
|
|
208
170
|
export function partitionIssuesByControlPolicy<TIssue>(options: {
|
|
209
171
|
readonly issues: readonly TIssue[];
|
|
@@ -221,34 +183,19 @@ export function partitionIssuesByControlPolicy<TIssue>(options: {
|
|
|
221
183
|
*
|
|
222
184
|
* 1. Under `tolerated`, only issues whose `resolveCreationFactoryName`
|
|
223
185
|
* returns a value flow into the planner (create-if-absent).
|
|
224
|
-
* 2.
|
|
225
|
-
*
|
|
226
|
-
* otherwise they fall back to `defaultModificationFactoryName`.
|
|
186
|
+
* 2. A suppressed subject's record carries the resolved creation factoryName
|
|
187
|
+
* when it has at least one creation-flavoured issue, else `undefined`.
|
|
227
188
|
*/
|
|
228
189
|
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
190
|
}): {
|
|
241
191
|
readonly plannable: readonly TIssue[];
|
|
242
|
-
readonly
|
|
192
|
+
readonly suppressions: readonly SuppressionRecord[];
|
|
243
193
|
} {
|
|
244
194
|
const defaultControlPolicy = options.contract.defaultControlPolicy;
|
|
245
|
-
const formatSubjectLabel = options.formatSubjectLabel ?? defaultSubjectLabel;
|
|
246
|
-
const inferModificationFactoryName =
|
|
247
|
-
options.defaultModificationFactoryName ?? defaultModificationFactoryNameForSubject;
|
|
248
195
|
|
|
249
196
|
const plannable: TIssue[] = [];
|
|
250
197
|
// Resolved-subject suppressions are deduplicated by subject key so we emit
|
|
251
|
-
// one
|
|
198
|
+
// one record per suppressed subject, not one per suppressed issue.
|
|
252
199
|
// `creationFactoryName` upgrades from `undefined` to a concrete creation
|
|
253
200
|
// name the first time we see a creation-flavoured issue for the subject.
|
|
254
201
|
const suppressedSubjects = new Map<
|
|
@@ -259,7 +206,7 @@ export function partitionIssuesByControlPolicy<TIssue>(options: {
|
|
|
259
206
|
creationFactoryName?: string;
|
|
260
207
|
}
|
|
261
208
|
>();
|
|
262
|
-
const unresolvedSuppressions:
|
|
209
|
+
const unresolvedSuppressions: SuppressionRecord[] = [];
|
|
263
210
|
|
|
264
211
|
for (const issue of options.issues) {
|
|
265
212
|
const subject = options.resolveControlPolicySubject(issue);
|
|
@@ -281,10 +228,12 @@ export function partitionIssuesByControlPolicy<TIssue>(options: {
|
|
|
281
228
|
}
|
|
282
229
|
|
|
283
230
|
if (subject === undefined) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
231
|
+
unresolvedSuppressions.push({
|
|
232
|
+
subject: undefined,
|
|
233
|
+
policy,
|
|
234
|
+
factoryName: creationFactoryName,
|
|
235
|
+
createsNewObject: false,
|
|
236
|
+
});
|
|
288
237
|
continue;
|
|
289
238
|
}
|
|
290
239
|
|
|
@@ -303,20 +252,22 @@ export function partitionIssuesByControlPolicy<TIssue>(options: {
|
|
|
303
252
|
}
|
|
304
253
|
}
|
|
305
254
|
|
|
306
|
-
const
|
|
255
|
+
const suppressions: SuppressionRecord[] = [...unresolvedSuppressions];
|
|
307
256
|
for (const entry of suppressedSubjects.values()) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
257
|
+
suppressions.push({
|
|
258
|
+
subject: entry.subject,
|
|
259
|
+
policy: entry.policy,
|
|
260
|
+
factoryName: entry.creationFactoryName,
|
|
261
|
+
createsNewObject: entry.subject.createsNewObject,
|
|
262
|
+
});
|
|
312
263
|
}
|
|
313
264
|
|
|
314
265
|
return Object.freeze({
|
|
315
266
|
plannable: Object.freeze(plannable),
|
|
316
|
-
|
|
267
|
+
suppressions: Object.freeze(suppressions),
|
|
317
268
|
});
|
|
318
269
|
}
|
|
319
270
|
|
|
320
271
|
function subjectKey(subject: ControlPolicySubject): string {
|
|
321
|
-
return `${subject.namespaceId}\u0000${subject.
|
|
272
|
+
return `${subject.namespaceId}\u0000${subject.entityKind ?? ''}\u0000${subject.entityName ?? ''}`;
|
|
322
273
|
}
|
|
@@ -88,8 +88,8 @@ export function planFieldEventOperations(
|
|
|
88
88
|
for (const tableName of tableNames) {
|
|
89
89
|
const priorTableRaw = priorTables?.[tableName];
|
|
90
90
|
const newTableRaw = newTables?.[tableName];
|
|
91
|
-
const priorTable = priorTableRaw
|
|
92
|
-
const newTable = newTableRaw
|
|
91
|
+
const priorTable = StorageTable.is(priorTableRaw) ? priorTableRaw : undefined;
|
|
92
|
+
const newTable = StorageTable.is(newTableRaw) ? newTableRaw : undefined;
|
|
93
93
|
const fieldNames = unionSorted(
|
|
94
94
|
priorTable ? Object.keys(priorTable.columns) : [],
|
|
95
95
|
newTable ? Object.keys(newTable.columns) : [],
|