@prisma-next/family-sql 0.14.0-dev.39 → 0.14.0-dev.40
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/contract-to-schema-ir-S-evq8E6.mjs +264 -0
- package/dist/contract-to-schema-ir-S-evq8E6.mjs.map +1 -0
- package/dist/{control-adapter-B2q9WN1V.d.mts → control-adapter-DHYFuOBy.d.mts} +14 -17
- package/dist/{control-adapter-B2q9WN1V.d.mts.map → control-adapter-DHYFuOBy.d.mts.map} +1 -1
- package/dist/control-adapter.d.mts +1 -1
- package/dist/control.d.mts +17 -2
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +42 -943
- package/dist/control.mjs.map +1 -1
- package/dist/{schema-verify.d.mts → diff.d.mts} +4 -4
- package/dist/diff.d.mts.map +1 -0
- package/dist/{verify-sql-schema-CEY7b78t.mjs → diff.mjs} +154 -244
- package/dist/diff.mjs.map +1 -0
- package/dist/migration.d.mts +1 -1
- package/dist/psl-infer.d.mts +80 -0
- package/dist/psl-infer.d.mts.map +1 -0
- package/dist/psl-infer.mjs +334 -0
- package/dist/psl-infer.mjs.map +1 -0
- package/dist/{verify-sql-schema-B8uEjw2s.d.mts → sql-schema-diff-6z36dZt6.d.mts} +43 -4
- package/dist/sql-schema-diff-6z36dZt6.d.mts.map +1 -0
- package/dist/{types-BxsFkHYv.d.mts → types-CkOIJXxU.d.mts} +83 -16
- package/dist/types-CkOIJXxU.d.mts.map +1 -0
- package/package.json +23 -22
- package/src/core/control-adapter.ts +12 -24
- package/src/core/control-instance.ts +113 -45
- package/src/core/{schema-verify/verify-sql-schema.ts → diff/sql-schema-diff.ts} +196 -8
- package/src/core/{schema-verify → diff}/verify-helpers.ts +12 -0
- package/src/core/migrations/contract-to-schema-ir.ts +48 -13
- package/src/core/migrations/field-event-planner.ts +3 -8
- package/src/core/migrations/schema-differ.ts +40 -0
- package/src/core/migrations/types.ts +43 -2
- package/src/core/psl-contract-infer/printer-config.ts +0 -6
- package/src/exports/control.ts +1 -0
- package/src/exports/diff.ts +18 -0
- package/src/exports/psl-infer.ts +36 -0
- package/dist/schema-verify.d.mts.map +0 -1
- package/dist/schema-verify.mjs +0 -2
- package/dist/types-BxsFkHYv.d.mts.map +0 -1
- package/dist/verify-sql-schema-B8uEjw2s.d.mts.map +0 -1
- package/dist/verify-sql-schema-CEY7b78t.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/exports/schema-verify.ts +0 -18
- /package/src/core/{schema-verify → diff}/control-verify-emit.ts +0 -0
- /package/src/core/{schema-verify → diff}/verifier-disposition.ts +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { i as verifySqlSchema, n as NativeTypeNormalizer, r as VerifySqlSchemaOptions } from "./
|
|
1
|
+
import { a as verifySqlSchemaTree, i as verifySqlSchema, n as NativeTypeNormalizer, r as VerifySqlSchemaOptions } from "./sql-schema-diff-6z36dZt6.mjs";
|
|
2
2
|
import { ControlPolicy } from "@prisma-next/contract/types";
|
|
3
3
|
import { SchemaIssue } from "@prisma-next/framework-components/control";
|
|
4
4
|
import { SqlIndexIR, SqlUniqueIR } from "@prisma-next/sql-schema-ir/types";
|
|
5
5
|
|
|
6
|
-
//#region src/core/
|
|
6
|
+
//#region src/core/diff/verify-helpers.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Compares two arrays of strings for equality (order-sensitive).
|
|
9
9
|
*/
|
|
@@ -35,5 +35,5 @@ declare function isUniqueConstraintSatisfied(uniques: readonly SqlUniqueIR[], in
|
|
|
35
35
|
*/
|
|
36
36
|
declare function isIndexSatisfied(indexes: readonly SqlIndexIR[], uniques: readonly SqlUniqueIR[], columns: readonly string[]): boolean;
|
|
37
37
|
//#endregion
|
|
38
|
-
export { type NativeTypeNormalizer, type VerifySqlSchemaOptions, arraysEqual, isIndexSatisfied, isUniqueConstraintSatisfied, verifySqlSchema };
|
|
39
|
-
//# sourceMappingURL=
|
|
38
|
+
export { type NativeTypeNormalizer, type VerifySqlSchemaOptions, arraysEqual, isIndexSatisfied, isUniqueConstraintSatisfied, verifySqlSchema, verifySqlSchemaTree };
|
|
39
|
+
//# sourceMappingURL=diff.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.mts","names":[],"sources":["../src/core/diff/verify-helpers.ts"],"mappings":";;;;;;;;;iBAgEgB,WAAA,CAAY,CAAA,qBAAsB,CAAoB;AA+BtE;;;;;;;;;;AAG4B;AAuB5B;AA1BA,iBAAgB,2BAAA,CACd,OAAA,WAAkB,WAAA,IAClB,OAAA,WAAkB,UAAU,IAC5B,OAAA;;;;;;;;;AA0B0B;;;;iBAHZ,gBAAA,CACd,OAAA,WAAkB,UAAA,IAClB,OAAA,WAAkB,WAAW,IAC7B,OAAA"}
|
|
@@ -1,242 +1,12 @@
|
|
|
1
|
+
import { a as extractCodecControlHooks, i as resolveValueSetValues } from "./contract-to-schema-ir-S-evq8E6.mjs";
|
|
2
|
+
import { blindCast } from "@prisma-next/utils/casts";
|
|
1
3
|
import { effectiveControlPolicy } from "@prisma-next/contract/types";
|
|
2
|
-
import {
|
|
4
|
+
import { dispositionForCategory } from "@prisma-next/framework-components/control";
|
|
3
5
|
import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
|
|
4
|
-
import { defaultIndexName } from "@prisma-next/sql-schema-ir/naming";
|
|
5
6
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
6
|
-
import {
|
|
7
|
+
import { StorageTable, isStorageTypeInstance } from "@prisma-next/sql-contract/types";
|
|
7
8
|
import { canonicalStringify } from "@prisma-next/utils/canonical-stringify";
|
|
8
|
-
//#region src/core/
|
|
9
|
-
function hasCodecControlHooks(descriptor) {
|
|
10
|
-
if (typeof descriptor !== "object" || descriptor === null) return false;
|
|
11
|
-
const hooks = descriptor.types?.codecTypes?.controlPlaneHooks;
|
|
12
|
-
return hooks !== null && hooks !== void 0 && typeof hooks === "object";
|
|
13
|
-
}
|
|
14
|
-
function extractCodecControlHooks(descriptors) {
|
|
15
|
-
const hooks = /* @__PURE__ */ new Map();
|
|
16
|
-
const owners = /* @__PURE__ */ new Map();
|
|
17
|
-
for (const descriptor of descriptors) {
|
|
18
|
-
if (typeof descriptor !== "object" || descriptor === null) continue;
|
|
19
|
-
if (!hasCodecControlHooks(descriptor)) continue;
|
|
20
|
-
const controlPlaneHooks = descriptor.types.codecTypes.controlPlaneHooks;
|
|
21
|
-
for (const [codecId, hook] of Object.entries(controlPlaneHooks)) {
|
|
22
|
-
assertUniqueCodecOwner({
|
|
23
|
-
codecId,
|
|
24
|
-
owners,
|
|
25
|
-
descriptorId: descriptor.id,
|
|
26
|
-
entityLabel: "control hooks",
|
|
27
|
-
entityOwnershipLabel: "owner"
|
|
28
|
-
});
|
|
29
|
-
hooks.set(codecId, hook);
|
|
30
|
-
owners.set(codecId, descriptor.id);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return hooks;
|
|
34
|
-
}
|
|
35
|
-
//#endregion
|
|
36
|
-
//#region src/core/migrations/contract-to-schema-ir.ts
|
|
37
|
-
function convertColumn(name, column, storageTypes, expandNativeType, renderDefault) {
|
|
38
|
-
const resolved = resolveColumnTypeMetadata(column, storageTypes);
|
|
39
|
-
const baseNativeType = expandNativeType ? expandNativeType({
|
|
40
|
-
nativeType: resolved.nativeType,
|
|
41
|
-
codecId: resolved.codecId,
|
|
42
|
-
...ifDefined("typeParams", resolved.typeParams)
|
|
43
|
-
}) : resolved.nativeType;
|
|
44
|
-
return {
|
|
45
|
-
name,
|
|
46
|
-
nativeType: column.many ? `${baseNativeType}[]` : baseNativeType,
|
|
47
|
-
nullable: column.nullable,
|
|
48
|
-
...ifDefined("default", column.default != null && renderDefault ? renderDefault(column.default, column) : void 0)
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function resolveColumnTypeMetadata(column, storageTypes) {
|
|
52
|
-
if (!column.typeRef) return column;
|
|
53
|
-
const referenced = storageTypes[column.typeRef];
|
|
54
|
-
if (!referenced) throw new Error(`Column references storage type "${column.typeRef}" but it is not defined in storage.types.`);
|
|
55
|
-
if (isStorageTypeInstance(referenced)) return {
|
|
56
|
-
codecId: referenced.codecId,
|
|
57
|
-
nativeType: referenced.nativeType,
|
|
58
|
-
typeParams: referenced.typeParams
|
|
59
|
-
};
|
|
60
|
-
throw new Error(`Storage type "${column.typeRef}" has an unknown polymorphic kind; expected a codec-typed StorageTypeInstance.`);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Resolves a `ValueSetRef` to its permitted values from the contract storage.
|
|
64
|
-
*
|
|
65
|
-
* Throws when the referenced namespace or value-set is absent — this indicates
|
|
66
|
-
* the contract was built incorrectly (the check and the value-set must be
|
|
67
|
-
* co-emitted by the lowering step). Used by `convertCheck` (schema-IR
|
|
68
|
-
* projection), `verifyCheckConstraints` (verification), and
|
|
69
|
-
* `checkConstraintPlanCallStrategy` (migration planning) so all three agree on
|
|
70
|
-
* the resolved values and the error behavior on a missing reference.
|
|
71
|
-
*/
|
|
72
|
-
function allStrings(values) {
|
|
73
|
-
return values.every((value) => typeof value === "string");
|
|
74
|
-
}
|
|
75
|
-
function resolveValueSetValues(ref, storage, contextLabel) {
|
|
76
|
-
const ns = storage.namespaces[ref.namespaceId];
|
|
77
|
-
if (!ns) throw new Error(`resolveValueSetValues: namespace "${ref.namespaceId}" not found in storage (${contextLabel})`);
|
|
78
|
-
const valueSet = ns.entries.valueSet?.[ref.entityName];
|
|
79
|
-
if (!valueSet) throw new Error(`resolveValueSetValues: value-set "${ref.entityName}" not found in namespace "${ref.namespaceId}" (${contextLabel})`);
|
|
80
|
-
const values = valueSet.values;
|
|
81
|
-
if (!allStrings(values)) throw new Error(`resolveValueSetValues: value-set "${ref.entityName}" in namespace "${ref.namespaceId}" has a non-string value; numeric-enum CHECK constraints are not yet supported (${contextLabel})`);
|
|
82
|
-
return values;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Projects a `CheckConstraint` IR into an `SqlCheckConstraintIRInput` by
|
|
86
|
-
* resolving the permitted values from the storage value-set it references.
|
|
87
|
-
*
|
|
88
|
-
* The `CheckConstraint.valueSet` ref points to
|
|
89
|
-
* `storage.namespaces[namespaceId].entries.valueSet[name]`. The resolved
|
|
90
|
-
* values are lifted directly from `StorageValueSet.values` so verification
|
|
91
|
-
* compares value sets, not SQL predicate strings.
|
|
92
|
-
*
|
|
93
|
-
* Throws if the referenced namespace or value-set is absent — this
|
|
94
|
-
* indicates the contract was built incorrectly (the check and the
|
|
95
|
-
* value-set must be co-emitted by the lowering step).
|
|
96
|
-
*/
|
|
97
|
-
function convertCheck(check, storage) {
|
|
98
|
-
const permittedValues = resolveValueSetValues(check.valueSet, storage, `check "${check.name}"`);
|
|
99
|
-
return {
|
|
100
|
-
name: check.name,
|
|
101
|
-
column: check.column,
|
|
102
|
-
permittedValues
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function convertUnique(unique) {
|
|
106
|
-
return {
|
|
107
|
-
columns: unique.columns,
|
|
108
|
-
...ifDefined("name", unique.name)
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function convertIndex(index) {
|
|
112
|
-
return {
|
|
113
|
-
columns: index.columns,
|
|
114
|
-
unique: false,
|
|
115
|
-
...ifDefined("name", index.name)
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function convertForeignKey(fk) {
|
|
119
|
-
return {
|
|
120
|
-
columns: fk.source.columns,
|
|
121
|
-
referencedTable: fk.target.tableName,
|
|
122
|
-
referencedSchema: fk.target.namespaceId,
|
|
123
|
-
referencedColumns: fk.target.columns,
|
|
124
|
-
...ifDefined("name", fk.name),
|
|
125
|
-
...ifDefined("onDelete", fk.onDelete),
|
|
126
|
-
...ifDefined("onUpdate", fk.onUpdate)
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function convertTable(name, table, storageTypes, expandNativeType, renderDefault, storage) {
|
|
130
|
-
const columns = {};
|
|
131
|
-
for (const [colName, colDef] of Object.entries(table.columns)) columns[colName] = convertColumn(colName, colDef, storageTypes, expandNativeType, renderDefault);
|
|
132
|
-
const satisfiedIndexColumns = new Set([
|
|
133
|
-
...table.indexes.map((idx) => idx.columns.join(",")),
|
|
134
|
-
...table.uniques.map((unique) => unique.columns.join(",")),
|
|
135
|
-
...table.primaryKey ? [table.primaryKey.columns.join(",")] : []
|
|
136
|
-
]);
|
|
137
|
-
const fkBackingIndexes = [];
|
|
138
|
-
for (const fk of table.foreignKeys) {
|
|
139
|
-
if (fk.index === false) continue;
|
|
140
|
-
const key = fk.source.columns.join(",");
|
|
141
|
-
if (satisfiedIndexColumns.has(key)) continue;
|
|
142
|
-
fkBackingIndexes.push({
|
|
143
|
-
columns: fk.source.columns,
|
|
144
|
-
unique: false,
|
|
145
|
-
name: defaultIndexName(name, fk.source.columns)
|
|
146
|
-
});
|
|
147
|
-
satisfiedIndexColumns.add(key);
|
|
148
|
-
}
|
|
149
|
-
const checks = table.checks && table.checks.length > 0 ? table.checks.map((c) => convertCheck(c, storage)) : void 0;
|
|
150
|
-
return {
|
|
151
|
-
name,
|
|
152
|
-
columns,
|
|
153
|
-
...ifDefined("primaryKey", table.primaryKey),
|
|
154
|
-
foreignKeys: table.foreignKeys.filter((fk) => fk.constraint !== false).map(convertForeignKey),
|
|
155
|
-
uniques: table.uniques.map(convertUnique),
|
|
156
|
-
indexes: [...table.indexes.map(convertIndex), ...fkBackingIndexes],
|
|
157
|
-
...ifDefined("checks", checks)
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Detects destructive changes between two contract storages.
|
|
162
|
-
*
|
|
163
|
-
* The additive-only planner silently ignores removals (tables, columns).
|
|
164
|
-
* This function detects those removals so callers can report them as conflicts
|
|
165
|
-
* rather than silently producing an empty plan.
|
|
166
|
-
*
|
|
167
|
-
* Returns an empty array if no destructive changes are found.
|
|
168
|
-
*/
|
|
169
|
-
function detectDestructiveChanges(from, to) {
|
|
170
|
-
if (!from) return [];
|
|
171
|
-
const hasOwn = (value, key) => Object.hasOwn(value, key);
|
|
172
|
-
const conflicts = [];
|
|
173
|
-
const namespaceIds = [...new Set([...Object.keys(from.namespaces), ...Object.keys(to.namespaces)])].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
174
|
-
for (const namespaceId of namespaceIds) {
|
|
175
|
-
const fromNs = from.namespaces[namespaceId];
|
|
176
|
-
const toNs = to.namespaces[namespaceId];
|
|
177
|
-
const fromTables = fromNs?.entries.table;
|
|
178
|
-
if (!fromTables) continue;
|
|
179
|
-
for (const tableName of Object.keys(fromTables)) {
|
|
180
|
-
const toTableRaw = toNs?.entries.table?.[tableName];
|
|
181
|
-
if (!isStorageTable(toTableRaw)) {
|
|
182
|
-
conflicts.push({
|
|
183
|
-
kind: "tableRemoved",
|
|
184
|
-
summary: `Table "${tableName}" was removed`
|
|
185
|
-
});
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
const toTable = toTableRaw;
|
|
189
|
-
const fromTableRaw = fromTables[tableName];
|
|
190
|
-
if (!isStorageTable(fromTableRaw)) continue;
|
|
191
|
-
const fromTable = fromTableRaw;
|
|
192
|
-
for (const columnName of Object.keys(fromTable.columns)) if (!hasOwn(toTable.columns, columnName)) conflicts.push({
|
|
193
|
-
kind: "columnRemoved",
|
|
194
|
-
summary: `Column "${tableName}"."${columnName}" was removed`
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return conflicts;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Converts a `Contract` to `SqlSchemaIR`.
|
|
202
|
-
*
|
|
203
|
-
* Reads `contract.storage` for tables and `contract.storage.types` for type
|
|
204
|
-
* annotations. Storage-type annotations are written under
|
|
205
|
-
* `options.annotationNamespace`.
|
|
206
|
-
*
|
|
207
|
-
* Drops codec metadata (`codecId`, `typeRef`) since the schema IR only represents
|
|
208
|
-
* structural information. When `expandNativeType` is provided, parameterized types
|
|
209
|
-
* are expanded (e.g. `character` + `{ length: 36 }` → `character(36)`) so the
|
|
210
|
-
* resulting IR compares correctly against the "to" contract during planning.
|
|
211
|
-
*
|
|
212
|
-
* Returns an empty schema IR when `contract` is `null` (new project).
|
|
213
|
-
*/
|
|
214
|
-
function contractToSchemaIR(contract, options) {
|
|
215
|
-
if (options.annotationNamespace.length === 0) throw new Error("annotationNamespace must be a non-empty string");
|
|
216
|
-
if (!contract) return { tables: {} };
|
|
217
|
-
const storage = contract.storage;
|
|
218
|
-
const storageTypes = { ...storage.types ?? {} };
|
|
219
|
-
const tables = {};
|
|
220
|
-
for (const ns of Object.values(storage.namespaces)) for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table ?? {})) {
|
|
221
|
-
if (!isStorageTable(tableDefRaw)) throw new Error(`contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.entries.table.${tableName}`);
|
|
222
|
-
const tableDef = tableDefRaw;
|
|
223
|
-
if (tables[tableName] !== void 0) throw new Error(`contractToSchemaIR: duplicate SQL table name "${tableName}" across namespaces (ambiguous for flat SqlSchemaIR.tables).`);
|
|
224
|
-
tables[tableName] = convertTable(tableName, tableDef, storageTypes, options.expandNativeType, options.renderDefault, storage);
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
tables,
|
|
228
|
-
...ifDefined("annotations", deriveAnnotations(storage, options.annotationNamespace, options.resolveEnumNamespaceSchema))
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
function deriveAnnotations(storage, annotationNamespace, _resolveEnumNamespaceSchema) {
|
|
232
|
-
const storageTypes = {};
|
|
233
|
-
for (const typeInstance of Object.values(storage.types ?? {})) if (isStorageTypeInstance(typeInstance)) storageTypes[typeInstance.nativeType] = typeInstance;
|
|
234
|
-
const envelope = { ...Object.keys(storageTypes).length > 0 ? { storageTypes } : {} };
|
|
235
|
-
if (Object.keys(envelope).length === 0) return void 0;
|
|
236
|
-
return { [annotationNamespace]: envelope };
|
|
237
|
-
}
|
|
238
|
-
//#endregion
|
|
239
|
-
//#region src/core/schema-verify/verifier-disposition.ts
|
|
9
|
+
//#region src/core/diff/verifier-disposition.ts
|
|
240
10
|
/**
|
|
241
11
|
* Classifies the relational verifier issue kinds the SQL family emits (tables,
|
|
242
12
|
* columns, constraints, indexes, defaults, enum types) into the target-neutral
|
|
@@ -276,7 +46,7 @@ function verifierDisposition(controlPolicy, issueKind) {
|
|
|
276
46
|
return dispositionForCategory(controlPolicy, classifySqlVerifierIssueKind(issueKind));
|
|
277
47
|
}
|
|
278
48
|
//#endregion
|
|
279
|
-
//#region src/core/
|
|
49
|
+
//#region src/core/diff/control-verify-emit.ts
|
|
280
50
|
/**
|
|
281
51
|
* Grades `issue` under `controlPolicy` and, unless suppressed, pushes both the
|
|
282
52
|
* issue and a status-stamped verification node. Returns the resolved outcome so
|
|
@@ -304,7 +74,7 @@ function emitIssueUnderControlPolicy(controlPolicy, issue, issues) {
|
|
|
304
74
|
return disposition;
|
|
305
75
|
}
|
|
306
76
|
//#endregion
|
|
307
|
-
//#region src/core/
|
|
77
|
+
//#region src/core/diff/verify-helpers.ts
|
|
308
78
|
function indexOptionsLooselyEqual(a, b) {
|
|
309
79
|
const aKeys = a ? Object.keys(a).sort() : [];
|
|
310
80
|
const bKeys = b ? Object.keys(b).sort() : [];
|
|
@@ -369,6 +139,7 @@ function verifyPrimaryKey(contractPK, schemaPK, tableName, namespaceId, tableCon
|
|
|
369
139
|
if (!schemaPK) {
|
|
370
140
|
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, {
|
|
371
141
|
kind: "primary_key_mismatch",
|
|
142
|
+
reason: "not-equal",
|
|
372
143
|
table: tableName,
|
|
373
144
|
namespaceId,
|
|
374
145
|
expected: contractPK.columns.join(", "),
|
|
@@ -379,6 +150,7 @@ function verifyPrimaryKey(contractPK, schemaPK, tableName, namespaceId, tableCon
|
|
|
379
150
|
if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
|
|
380
151
|
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, {
|
|
381
152
|
kind: "primary_key_mismatch",
|
|
153
|
+
reason: "not-equal",
|
|
382
154
|
table: tableName,
|
|
383
155
|
namespaceId,
|
|
384
156
|
expected: contractPK.columns.join(", "),
|
|
@@ -406,6 +178,7 @@ function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, table
|
|
|
406
178
|
});
|
|
407
179
|
if (!matchingFK) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
408
180
|
kind: "foreign_key_mismatch",
|
|
181
|
+
reason: "not-equal",
|
|
409
182
|
table: tableName,
|
|
410
183
|
namespaceId,
|
|
411
184
|
expected: `${contractFK.source.columns.join(", ")} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(", ")})`,
|
|
@@ -429,6 +202,7 @@ function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, table
|
|
|
429
202
|
const combinedActual = actionMismatches.map((m) => m.actual).join(", ");
|
|
430
203
|
emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
431
204
|
kind: "foreign_key_mismatch",
|
|
205
|
+
reason: "not-equal",
|
|
432
206
|
table: tableName,
|
|
433
207
|
namespaceId,
|
|
434
208
|
indexOrConstraint: matchingFK.name ?? `fk(${contractFK.source.columns.join(",")})`,
|
|
@@ -465,6 +239,7 @@ function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, table
|
|
|
465
239
|
return arraysEqual(fk.source.columns, schemaFK.columns) && tablesMatch && arraysEqual(fk.target.columns, schemaFK.referencedColumns);
|
|
466
240
|
})) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
467
241
|
kind: "extra_foreign_key",
|
|
242
|
+
reason: "not-expected",
|
|
468
243
|
table: tableName,
|
|
469
244
|
namespaceId,
|
|
470
245
|
indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(",")})`,
|
|
@@ -502,6 +277,7 @@ function verifyUniqueConstraints(contractUniques, schemaUniques, schemaIndexes,
|
|
|
502
277
|
const matchingUniqueIndex = !matchingUnique && schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
|
|
503
278
|
if (!matchingUnique && !matchingUniqueIndex) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
504
279
|
kind: "unique_constraint_mismatch",
|
|
280
|
+
reason: "not-equal",
|
|
505
281
|
table: tableName,
|
|
506
282
|
namespaceId,
|
|
507
283
|
expected: contractUnique.columns.join(", "),
|
|
@@ -532,6 +308,7 @@ function verifyUniqueConstraints(contractUniques, schemaUniques, schemaIndexes,
|
|
|
532
308
|
if (strict) {
|
|
533
309
|
for (const schemaUnique of schemaUniques) if (!contractUniques.find((u) => arraysEqual(u.columns, schemaUnique.columns))) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
534
310
|
kind: "extra_unique_constraint",
|
|
311
|
+
reason: "not-expected",
|
|
535
312
|
table: tableName,
|
|
536
313
|
namespaceId,
|
|
537
314
|
indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(",")})`,
|
|
@@ -569,6 +346,7 @@ function verifyIndexes(contractIndexes, schemaIndexes, schemaUniques, tableName,
|
|
|
569
346
|
const matchingUniqueConstraint = !matchingIndex && contractIndex.type === void 0 && contractIndex.options === void 0 && schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
|
|
570
347
|
if (!matchingIndex && !matchingUniqueConstraint) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
571
348
|
kind: "index_mismatch",
|
|
349
|
+
reason: "not-equal",
|
|
572
350
|
table: tableName,
|
|
573
351
|
namespaceId,
|
|
574
352
|
expected: contractIndex.columns.join(", "),
|
|
@@ -600,6 +378,7 @@ function verifyIndexes(contractIndexes, schemaIndexes, schemaUniques, tableName,
|
|
|
600
378
|
if (schemaIndex.unique) continue;
|
|
601
379
|
if (!contractIndexes.find((idx) => arraysEqual(idx.columns, schemaIndex.columns) && indexExtrasMatch(idx, schemaIndex))) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
602
380
|
kind: "extra_index",
|
|
381
|
+
reason: "not-expected",
|
|
603
382
|
table: tableName,
|
|
604
383
|
namespaceId,
|
|
605
384
|
indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(",")})`,
|
|
@@ -708,6 +487,7 @@ function verifyCheckConstraints(contractChecks, schemaChecks, tableName, namespa
|
|
|
708
487
|
const liveCheck = schemaChecks.find((c) => c.name === contractCheck.name);
|
|
709
488
|
if (!liveCheck) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
710
489
|
kind: "check_missing",
|
|
490
|
+
reason: "not-found",
|
|
711
491
|
table: tableName,
|
|
712
492
|
namespaceId,
|
|
713
493
|
indexOrConstraint: contractCheck.name,
|
|
@@ -726,6 +506,7 @@ function verifyCheckConstraints(contractChecks, schemaChecks, tableName, namespa
|
|
|
726
506
|
}, issues, nodes);
|
|
727
507
|
else if (!valueSetsEqual(contractCheck.permittedValues, liveCheck.permittedValues)) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
728
508
|
kind: "check_mismatch",
|
|
509
|
+
reason: "not-equal",
|
|
729
510
|
table: tableName,
|
|
730
511
|
namespaceId,
|
|
731
512
|
indexOrConstraint: contractCheck.name,
|
|
@@ -758,6 +539,7 @@ function verifyCheckConstraints(contractChecks, schemaChecks, tableName, namespa
|
|
|
758
539
|
if (strict) {
|
|
759
540
|
for (const liveCheck of schemaChecks) if (!contractChecks.find((c) => c.name === liveCheck.name)) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
760
541
|
kind: "check_removed",
|
|
542
|
+
reason: "not-equal",
|
|
761
543
|
table: tableName,
|
|
762
544
|
namespaceId,
|
|
763
545
|
indexOrConstraint: liveCheck.name,
|
|
@@ -778,7 +560,20 @@ function verifyCheckConstraints(contractChecks, schemaChecks, tableName, namespa
|
|
|
778
560
|
return nodes;
|
|
779
561
|
}
|
|
780
562
|
//#endregion
|
|
781
|
-
//#region src/core/
|
|
563
|
+
//#region src/core/diff/sql-schema-diff.ts
|
|
564
|
+
/**
|
|
565
|
+
* Returns the per-schema namespace nodes of an introspected schema node, for
|
|
566
|
+
* the relational verify to consume one at a time. Structure-agnostic — imports
|
|
567
|
+
* no target node class. A root exposing a `namespaces` record (Postgres) yields
|
|
568
|
+
* its namespace nodes (never merged, so same-named tables in different schemas
|
|
569
|
+
* cannot collide); a flat schema (SQLite) is its own single namespace and
|
|
570
|
+
* yields itself. Handles spread-flattened input (own-enumerable fields survive).
|
|
571
|
+
*/
|
|
572
|
+
function namespaceSchemaNodes(schema) {
|
|
573
|
+
const obj = blindCast(schema);
|
|
574
|
+
if (obj.namespaces !== void 0) return Object.values(obj.namespaces);
|
|
575
|
+
return [blindCast(schema)];
|
|
576
|
+
}
|
|
782
577
|
/**
|
|
783
578
|
* Verifies that a SqlSchemaIR matches a Contract.
|
|
784
579
|
*
|
|
@@ -802,7 +597,8 @@ function verifySqlSchema(options) {
|
|
|
802
597
|
codecHooks,
|
|
803
598
|
storageTypes: contract.storage.types ?? {},
|
|
804
599
|
...ifDefined("normalizeDefault", normalizeDefault),
|
|
805
|
-
...ifDefined("normalizeNativeType", normalizeNativeType)
|
|
600
|
+
...ifDefined("normalizeNativeType", normalizeNativeType),
|
|
601
|
+
...ifDefined("restrictToNamespaceIds", options.restrictToNamespaceIds)
|
|
806
602
|
});
|
|
807
603
|
validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
|
|
808
604
|
const typeNodes = [];
|
|
@@ -893,17 +689,18 @@ function extractContractMetadata(contract) {
|
|
|
893
689
|
};
|
|
894
690
|
}
|
|
895
691
|
function verifySchemaTables(options) {
|
|
896
|
-
const { contract, schema, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType } = options;
|
|
692
|
+
const { contract, schema, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType, restrictToNamespaceIds } = options;
|
|
897
693
|
const contractDefaultControl = contract.defaultControlPolicy;
|
|
898
694
|
const issues = [];
|
|
899
695
|
const rootChildren = [];
|
|
900
696
|
const schemaTables = schema.tables;
|
|
901
697
|
const namespaceIds = Object.keys(contract.storage.namespaces).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
902
698
|
for (const namespaceId of namespaceIds) {
|
|
699
|
+
if (restrictToNamespaceIds !== void 0 && !restrictToNamespaceIds.has(namespaceId)) continue;
|
|
903
700
|
const ns = contract.storage.namespaces[namespaceId];
|
|
904
701
|
if (!ns) continue;
|
|
905
702
|
for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table ?? {})) {
|
|
906
|
-
|
|
703
|
+
StorageTable.assert(contractTableRaw, `storage.namespaces.${namespaceId}.entries.table.${tableName}`);
|
|
907
704
|
const contractTable = contractTableRaw;
|
|
908
705
|
const tableControlPolicy = effectiveControlPolicy(contractTable.control, contractDefaultControl);
|
|
909
706
|
const schemaTable = schemaTables[tableName];
|
|
@@ -911,6 +708,7 @@ function verifySchemaTables(options) {
|
|
|
911
708
|
if (!schemaTable) {
|
|
912
709
|
emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
913
710
|
kind: "missing_table",
|
|
711
|
+
reason: "not-found",
|
|
914
712
|
table: tableName,
|
|
915
713
|
namespaceId,
|
|
916
714
|
message: `Table "${tableName}" is missing from database`
|
|
@@ -949,6 +747,7 @@ function verifySchemaTables(options) {
|
|
|
949
747
|
if (strict) {
|
|
950
748
|
for (const tableName of Object.keys(schemaTables)) if (!namespaceIds.some((namespaceId) => contract.storage.namespaces[namespaceId]?.entries.table?.[tableName] !== void 0)) emitIssueAndNodeUnderControlPolicy(effectiveControlPolicy(void 0, contractDefaultControl), {
|
|
951
749
|
kind: "extra_table",
|
|
750
|
+
reason: "not-expected",
|
|
952
751
|
table: tableName,
|
|
953
752
|
message: `Extra table "${tableName}" found in database (not in contract)`
|
|
954
753
|
}, {
|
|
@@ -960,7 +759,8 @@ function verifySchemaTables(options) {
|
|
|
960
759
|
message: `Extra table "${tableName}" found`,
|
|
961
760
|
expected: void 0,
|
|
962
761
|
actual: void 0,
|
|
963
|
-
children: []
|
|
762
|
+
children: [],
|
|
763
|
+
reason: "not-expected"
|
|
964
764
|
}, issues, rootChildren);
|
|
965
765
|
}
|
|
966
766
|
return {
|
|
@@ -1034,6 +834,7 @@ function verifyTableChildren(options) {
|
|
|
1034
834
|
});
|
|
1035
835
|
} else if (schemaTable.primaryKey && strict) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
1036
836
|
kind: "extra_primary_key",
|
|
837
|
+
reason: "not-expected",
|
|
1037
838
|
table: tableName,
|
|
1038
839
|
namespaceId,
|
|
1039
840
|
message: "Extra primary key found in database (not in contract)"
|
|
@@ -1078,6 +879,7 @@ function collectContractColumnNodes(options) {
|
|
|
1078
879
|
if (!schemaColumn) {
|
|
1079
880
|
emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
1080
881
|
kind: "missing_column",
|
|
882
|
+
reason: "not-found",
|
|
1081
883
|
table: tableName,
|
|
1082
884
|
namespaceId,
|
|
1083
885
|
column: columnName,
|
|
@@ -1118,6 +920,7 @@ function appendExtraColumnNodes(options) {
|
|
|
1118
920
|
const { contractTable, schemaTable, tableName, namespaceId, tablePath, tableControlPolicy, issues, columnNodes } = options;
|
|
1119
921
|
for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) if (!contractTable.columns[columnName]) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
1120
922
|
kind: "extra_column",
|
|
923
|
+
reason: "not-expected",
|
|
1121
924
|
table: tableName,
|
|
1122
925
|
namespaceId,
|
|
1123
926
|
column: columnName,
|
|
@@ -1151,6 +954,7 @@ function verifyColumn(options) {
|
|
|
1151
954
|
if (!(contractNativeType === schemaNativeType)) {
|
|
1152
955
|
const issue = {
|
|
1153
956
|
kind: "type_mismatch",
|
|
957
|
+
reason: "not-equal",
|
|
1154
958
|
table: tableName,
|
|
1155
959
|
namespaceId,
|
|
1156
960
|
column: columnName,
|
|
@@ -1203,6 +1007,7 @@ function verifyColumn(options) {
|
|
|
1203
1007
|
if (contractColumn.nullable !== schemaColumn.nullable) {
|
|
1204
1008
|
const issue = {
|
|
1205
1009
|
kind: "nullability_mismatch",
|
|
1010
|
+
reason: "not-equal",
|
|
1206
1011
|
table: tableName,
|
|
1207
1012
|
namespaceId,
|
|
1208
1013
|
column: columnName,
|
|
@@ -1232,6 +1037,7 @@ function verifyColumn(options) {
|
|
|
1232
1037
|
const defaultDescription = describeColumnDefault(contractColumn.default);
|
|
1233
1038
|
const issue = {
|
|
1234
1039
|
kind: "default_missing",
|
|
1040
|
+
reason: "not-found",
|
|
1235
1041
|
table: tableName,
|
|
1236
1042
|
namespaceId,
|
|
1237
1043
|
column: columnName,
|
|
@@ -1259,6 +1065,7 @@ function verifyColumn(options) {
|
|
|
1259
1065
|
const actualDescription = schemaColumn.default;
|
|
1260
1066
|
const issue = {
|
|
1261
1067
|
kind: "default_mismatch",
|
|
1068
|
+
reason: "not-equal",
|
|
1262
1069
|
table: tableName,
|
|
1263
1070
|
namespaceId,
|
|
1264
1071
|
column: columnName,
|
|
@@ -1286,6 +1093,7 @@ function verifyColumn(options) {
|
|
|
1286
1093
|
} else if (strict && schemaColumn.default) {
|
|
1287
1094
|
const issue = {
|
|
1288
1095
|
kind: "extra_default",
|
|
1096
|
+
reason: "not-expected",
|
|
1289
1097
|
table: tableName,
|
|
1290
1098
|
namespaceId,
|
|
1291
1099
|
column: columnName,
|
|
@@ -1501,7 +1309,109 @@ function formatLiteralValue(value) {
|
|
|
1501
1309
|
if (typeof value === "string") return value;
|
|
1502
1310
|
return JSON.stringify(value);
|
|
1503
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Reads a namespace node's DDL schema name. Namespaced target nodes (Postgres
|
|
1314
|
+
* `PostgresNamespaceSchemaNode`) carry `schemaName`; a flat schema (SQLite) has
|
|
1315
|
+
* none and pairs by position as the sole namespace.
|
|
1316
|
+
*/
|
|
1317
|
+
function namespaceSchemaName(node) {
|
|
1318
|
+
return blindCast(node).schemaName;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Returns a shallow copy of `contract` exposing only the one named namespace —
|
|
1322
|
+
* used solely to resolve that namespace's live DDL schema via the target's
|
|
1323
|
+
* expected-tree projection (never for verification, so global value-sets it may
|
|
1324
|
+
* reference are not consulted).
|
|
1325
|
+
*/
|
|
1326
|
+
function scopeContractToNamespace(contract, namespaceId) {
|
|
1327
|
+
const namespace = contract.storage.namespaces[namespaceId];
|
|
1328
|
+
const scopedNamespaces = namespace === void 0 ? {} : { [namespaceId]: namespace };
|
|
1329
|
+
return blindCast({
|
|
1330
|
+
...contract,
|
|
1331
|
+
storage: blindCast({
|
|
1332
|
+
...contract.storage,
|
|
1333
|
+
namespaces: scopedNamespaces
|
|
1334
|
+
})
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Combines two `VerifyDatabaseSchemaResult`s by concatenating issues and summing
|
|
1339
|
+
* counts — used to fold the per-namespace pairings of a multi-schema database
|
|
1340
|
+
* into one result. The verification-tree `root` of the first pairing is
|
|
1341
|
+
* retained (multi-schema verify-tree shaping is future work).
|
|
1342
|
+
*/
|
|
1343
|
+
function mergeVerifyResults(a, b) {
|
|
1344
|
+
return {
|
|
1345
|
+
...a,
|
|
1346
|
+
ok: a.ok && b.ok,
|
|
1347
|
+
...ifDefined("code", a.code ?? b.code),
|
|
1348
|
+
schema: {
|
|
1349
|
+
...a.schema,
|
|
1350
|
+
issues: [...a.schema.issues, ...b.schema.issues],
|
|
1351
|
+
schemaDiffIssues: [...a.schema.schemaDiffIssues, ...b.schema.schemaDiffIssues],
|
|
1352
|
+
counts: {
|
|
1353
|
+
pass: a.schema.counts.pass + b.schema.counts.pass,
|
|
1354
|
+
warn: a.schema.counts.warn + b.schema.counts.warn,
|
|
1355
|
+
fail: a.schema.counts.fail + b.schema.counts.fail,
|
|
1356
|
+
totalNodes: a.schema.counts.totalNodes + b.schema.counts.totalNodes
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* The single per-namespace-paired relational verify shared by the migration
|
|
1363
|
+
* planner and the family schema verify — there is exactly one such operation.
|
|
1364
|
+
*
|
|
1365
|
+
* Each contract namespace is paired to the introspected namespace node holding
|
|
1366
|
+
* the same DDL schema, then `verifySqlSchema` checks that namespace's tables
|
|
1367
|
+
* against the matching actual node (a contract table under `auth` is only ever
|
|
1368
|
+
* looked up in the `auth` actual node, so a multi-schema database no longer
|
|
1369
|
+
* reports tables in other schemas as missing). The full contract is passed every
|
|
1370
|
+
* time — `restrictToNamespaceIds` scopes only which tables are checked, so
|
|
1371
|
+
* cross-namespace value-set / control-policy resolution is unaffected.
|
|
1372
|
+
*
|
|
1373
|
+
* The DDL schema of each contract namespace is read from a single-namespace
|
|
1374
|
+
* expected projection (`buildExpectedSchema`), which both callers already build
|
|
1375
|
+
* the same way. Empty contract namespaces verify nothing and are skipped.
|
|
1376
|
+
* Single-schema (one namespace) and SQLite's flat schema are one pairing —
|
|
1377
|
+
* byte-identical to the prior per-node verify.
|
|
1378
|
+
*/
|
|
1379
|
+
function verifySqlSchemaTree(options) {
|
|
1380
|
+
const baseOptions = {
|
|
1381
|
+
contract: options.contract,
|
|
1382
|
+
strict: options.strict,
|
|
1383
|
+
typeMetadataRegistry: options.typeMetadataRegistry,
|
|
1384
|
+
frameworkComponents: options.frameworkComponents,
|
|
1385
|
+
...ifDefined("normalizeDefault", options.normalizeDefault),
|
|
1386
|
+
...ifDefined("normalizeNativeType", options.normalizeNativeType)
|
|
1387
|
+
};
|
|
1388
|
+
const actualNodes = namespaceSchemaNodes(options.actualSchema);
|
|
1389
|
+
const actualByName = /* @__PURE__ */ new Map();
|
|
1390
|
+
for (const node of actualNodes) {
|
|
1391
|
+
const name = namespaceSchemaName(node);
|
|
1392
|
+
if (name !== void 0) actualByName.set(name, node);
|
|
1393
|
+
}
|
|
1394
|
+
const soleFlatActual = actualByName.size === 0 ? actualNodes[0] : void 0;
|
|
1395
|
+
const emptyNamespace = { tables: {} };
|
|
1396
|
+
let combined;
|
|
1397
|
+
for (const namespaceId of Object.keys(options.contract.storage.namespaces)) {
|
|
1398
|
+
const namespace = options.contract.storage.namespaces[namespaceId];
|
|
1399
|
+
if (!namespace || Object.keys(namespace.entries.table ?? {}).length === 0) continue;
|
|
1400
|
+
const ddlSchema = namespaceSchemaNodes(options.buildExpectedSchema(scopeContractToNamespace(options.contract, namespaceId))).map(namespaceSchemaName).find((name) => name !== void 0);
|
|
1401
|
+
const actualNode = (ddlSchema !== void 0 ? actualByName.get(ddlSchema) : soleFlatActual) ?? emptyNamespace;
|
|
1402
|
+
const result = verifySqlSchema({
|
|
1403
|
+
...baseOptions,
|
|
1404
|
+
schema: actualNode,
|
|
1405
|
+
restrictToNamespaceIds: new Set([namespaceId])
|
|
1406
|
+
});
|
|
1407
|
+
combined = combined === void 0 ? result : mergeVerifyResults(combined, result);
|
|
1408
|
+
}
|
|
1409
|
+
return combined ?? verifySqlSchema({
|
|
1410
|
+
...baseOptions,
|
|
1411
|
+
schema: emptyNamespace
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1504
1414
|
//#endregion
|
|
1505
|
-
export {
|
|
1415
|
+
export { arraysEqual, isIndexSatisfied, isUniqueConstraintSatisfied, verifySqlSchema, verifySqlSchemaTree };
|
|
1506
1416
|
|
|
1507
|
-
//# sourceMappingURL=
|
|
1417
|
+
//# sourceMappingURL=diff.mjs.map
|