@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
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { assertUniqueCodecOwner } from "@prisma-next/framework-components/control";
|
|
1
|
+
import { assertUniqueCodecOwner, dispositionForCategory } from "@prisma-next/framework-components/control";
|
|
2
|
+
import { defaultIndexName } from "@prisma-next/sql-schema-ir/naming";
|
|
2
3
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
3
4
|
import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
|
|
4
|
-
import { StorageTable, isPostgresEnumStorageEntry, isStorageTypeInstance } from "@prisma-next/sql-contract/types";
|
|
5
|
+
import { StorageTable, isPostgresEnumStorageEntry, isStorageTypeInstance, toStorageTypeInstance } from "@prisma-next/sql-contract/types";
|
|
6
|
+
import { blindCast } from "@prisma-next/utils/casts";
|
|
7
|
+
import { effectiveControlPolicy } from "@prisma-next/contract/types";
|
|
5
8
|
import { canonicalStringify } from "@prisma-next/utils/canonical-stringify";
|
|
6
9
|
//#region src/core/assembly.ts
|
|
7
10
|
function hasCodecControlHooks(descriptor) {
|
|
@@ -31,6 +34,310 @@ function extractCodecControlHooks(descriptors) {
|
|
|
31
34
|
return hooks;
|
|
32
35
|
}
|
|
33
36
|
//#endregion
|
|
37
|
+
//#region src/core/migrations/contract-to-schema-ir.ts
|
|
38
|
+
function convertColumn(name, column, storageTypes, expandNativeType, renderDefault) {
|
|
39
|
+
const resolved = resolveColumnTypeMetadata(column, storageTypes);
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
nativeType: expandNativeType ? expandNativeType({
|
|
43
|
+
nativeType: resolved.nativeType,
|
|
44
|
+
codecId: resolved.codecId,
|
|
45
|
+
...ifDefined("typeParams", resolved.typeParams)
|
|
46
|
+
}) : resolved.nativeType,
|
|
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 (isPostgresEnumStorageEntry(referenced)) return {
|
|
56
|
+
codecId: referenced.codecId,
|
|
57
|
+
nativeType: referenced.nativeType,
|
|
58
|
+
typeParams: { values: referenced.values }
|
|
59
|
+
};
|
|
60
|
+
if (isStorageTypeInstance(referenced)) return {
|
|
61
|
+
codecId: referenced.codecId,
|
|
62
|
+
nativeType: referenced.nativeType,
|
|
63
|
+
typeParams: referenced.typeParams
|
|
64
|
+
};
|
|
65
|
+
throw new Error(`Storage type "${column.typeRef}" has an unknown polymorphic kind; expected codec-instance or postgres-enum.`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolves a `ValueSetRef` to its permitted values from the contract storage.
|
|
69
|
+
*
|
|
70
|
+
* Throws when the referenced namespace or value-set is absent — this indicates
|
|
71
|
+
* the contract was built incorrectly (the check and the value-set must be
|
|
72
|
+
* co-emitted by the lowering step). Used by `convertCheck` (schema-IR
|
|
73
|
+
* projection), `verifyCheckConstraints` (verification), and
|
|
74
|
+
* `checkConstraintPlanCallStrategy` (migration planning) so all three agree on
|
|
75
|
+
* the resolved values and the error behavior on a missing reference.
|
|
76
|
+
*/
|
|
77
|
+
function resolveValueSetValues(ref, storage, contextLabel) {
|
|
78
|
+
const ns = storage.namespaces[ref.namespaceId];
|
|
79
|
+
if (!ns) throw new Error(`resolveValueSetValues: namespace "${ref.namespaceId}" not found in storage (${contextLabel})`);
|
|
80
|
+
const valueSet = ns.entries.valueSet?.[ref.name];
|
|
81
|
+
if (!valueSet) throw new Error(`resolveValueSetValues: value-set "${ref.name}" not found in namespace "${ref.namespaceId}" (${contextLabel})`);
|
|
82
|
+
return valueSet.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 (!(toTableRaw instanceof StorageTable)) {
|
|
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 (!(fromTableRaw instanceof StorageTable)) 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 allTypes = { ...storage.types ?? {} };
|
|
219
|
+
for (const ns of Object.values(storage.namespaces)) {
|
|
220
|
+
const nsEnums = ns.entries["type"];
|
|
221
|
+
if (nsEnums) for (const [k, v] of Object.entries(nsEnums)) allTypes[k] = blindCast(v);
|
|
222
|
+
}
|
|
223
|
+
const storageTypes = allTypes;
|
|
224
|
+
const tables = {};
|
|
225
|
+
for (const ns of Object.values(storage.namespaces)) for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table)) {
|
|
226
|
+
if (!(tableDefRaw instanceof StorageTable)) throw new Error(`contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.entries.table.${tableName}`);
|
|
227
|
+
const tableDef = tableDefRaw;
|
|
228
|
+
if (tables[tableName] !== void 0) throw new Error(`contractToSchemaIR: duplicate SQL table name "${tableName}" across namespaces (ambiguous for flat SqlSchemaIR.tables).`);
|
|
229
|
+
tables[tableName] = convertTable(tableName, tableDef, storageTypes, options.expandNativeType, options.renderDefault, storage);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
tables,
|
|
233
|
+
...ifDefined("annotations", deriveAnnotations(storage, options.annotationNamespace, options.resolveEnumStorageKey))
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Normalises a native enum storage entry to the codec-typed annotation shape
|
|
238
|
+
* `{codecId, nativeType, typeParams}` the introspector writes and
|
|
239
|
+
* `readExistingEnumValues` reads (`existing.codecId` + `existing.typeParams.values`).
|
|
240
|
+
* Without this the projector would emit the raw `PostgresEnumStorageEntry`
|
|
241
|
+
* shape (top-level `values`, no `typeParams`) and the enum would read as new.
|
|
242
|
+
*/
|
|
243
|
+
function normalizeEnumAnnotation(entry) {
|
|
244
|
+
return toStorageTypeInstance({
|
|
245
|
+
codecId: entry.codecId,
|
|
246
|
+
nativeType: entry.nativeType,
|
|
247
|
+
typeParams: { values: entry.values }
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function deriveAnnotations(storage, annotationNamespace, resolveEnumStorageKey) {
|
|
251
|
+
const storageTypes = {};
|
|
252
|
+
for (const typeInstance of Object.values(storage.types ?? {})) {
|
|
253
|
+
if (isPostgresEnumStorageEntry(typeInstance)) {
|
|
254
|
+
const key = resolveEnumStorageKey ? resolveEnumStorageKey(storage, UNBOUND_NAMESPACE_ID, typeInstance.nativeType) : typeInstance.nativeType;
|
|
255
|
+
storageTypes[key] = normalizeEnumAnnotation(typeInstance);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (isStorageTypeInstance(typeInstance)) storageTypes[typeInstance.nativeType] = typeInstance;
|
|
259
|
+
}
|
|
260
|
+
for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {
|
|
261
|
+
const nsEnums = ns.entries["type"];
|
|
262
|
+
if (!nsEnums) continue;
|
|
263
|
+
for (const entry of Object.values(nsEnums)) {
|
|
264
|
+
if (!isPostgresEnumStorageEntry(entry)) continue;
|
|
265
|
+
const key = resolveEnumStorageKey ? resolveEnumStorageKey(storage, namespaceId, entry.nativeType) : entry.nativeType;
|
|
266
|
+
storageTypes[key] = normalizeEnumAnnotation(entry);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (Object.keys(storageTypes).length === 0) return void 0;
|
|
270
|
+
return { [annotationNamespace]: { storageTypes } };
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/core/schema-verify/verifier-disposition.ts
|
|
274
|
+
/**
|
|
275
|
+
* Classifies the relational verifier issue kinds the SQL family emits (tables,
|
|
276
|
+
* columns, constraints, indexes, defaults, enum types) into the target-neutral
|
|
277
|
+
* categories the framework grades. The relational vocabulary lives here, in the
|
|
278
|
+
* SQL domain — the framework never switches over `extra_foreign_key` and friends.
|
|
279
|
+
*/
|
|
280
|
+
function classifySqlVerifierIssueKind(kind) {
|
|
281
|
+
switch (kind) {
|
|
282
|
+
case "extra_column": return "extraNestedElement";
|
|
283
|
+
case "extra_primary_key":
|
|
284
|
+
case "extra_foreign_key":
|
|
285
|
+
case "extra_unique_constraint":
|
|
286
|
+
case "extra_index":
|
|
287
|
+
case "extra_validator":
|
|
288
|
+
case "extra_default": return "extraAuxiliary";
|
|
289
|
+
case "extra_table": return "extraTopLevelObject";
|
|
290
|
+
case "missing_schema":
|
|
291
|
+
case "missing_table":
|
|
292
|
+
case "missing_column":
|
|
293
|
+
case "type_missing":
|
|
294
|
+
case "default_missing": return "declaredMissing";
|
|
295
|
+
case "type_values_mismatch":
|
|
296
|
+
case "enum_values_changed":
|
|
297
|
+
case "check_mismatch": return "valueDrift";
|
|
298
|
+
case "type_mismatch":
|
|
299
|
+
case "nullability_mismatch":
|
|
300
|
+
case "primary_key_mismatch":
|
|
301
|
+
case "foreign_key_mismatch":
|
|
302
|
+
case "unique_constraint_mismatch":
|
|
303
|
+
case "index_mismatch":
|
|
304
|
+
case "default_mismatch": return "declaredIncompatible";
|
|
305
|
+
case "check_missing": return "declaredMissing";
|
|
306
|
+
case "check_removed": return "extraAuxiliary";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function verifierDisposition(controlPolicy, issueKind) {
|
|
310
|
+
return dispositionForCategory(controlPolicy, classifySqlVerifierIssueKind(issueKind));
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/core/schema-verify/control-verify-emit.ts
|
|
314
|
+
/**
|
|
315
|
+
* Grades `issue` under `controlPolicy` and, unless suppressed, pushes both the
|
|
316
|
+
* issue and a status-stamped verification node. Returns the resolved outcome so
|
|
317
|
+
* the caller never re-grades the same issue.
|
|
318
|
+
*/
|
|
319
|
+
function emitIssueAndNodeUnderControlPolicy(controlPolicy, issue, node, issues, nodes) {
|
|
320
|
+
const disposition = verifierDisposition(controlPolicy, issue.kind);
|
|
321
|
+
if (disposition === "suppress") return disposition;
|
|
322
|
+
issues.push(issue);
|
|
323
|
+
nodes.push({
|
|
324
|
+
...node,
|
|
325
|
+
status: disposition
|
|
326
|
+
});
|
|
327
|
+
return disposition;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Grades `issue` under `controlPolicy` and, unless suppressed, pushes the issue
|
|
331
|
+
* (no verification node). Returns the resolved outcome so the caller maps it to
|
|
332
|
+
* a node status itself without re-grading.
|
|
333
|
+
*/
|
|
334
|
+
function emitIssueUnderControlPolicy(controlPolicy, issue, issues) {
|
|
335
|
+
const disposition = verifierDisposition(controlPolicy, issue.kind);
|
|
336
|
+
if (disposition === "suppress") return disposition;
|
|
337
|
+
issues.push(issue);
|
|
338
|
+
return disposition;
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
34
341
|
//#region src/core/schema-verify/verify-helpers.ts
|
|
35
342
|
function indexOptionsLooselyEqual(a, b) {
|
|
36
343
|
const aKeys = a ? Object.keys(a).sort() : [];
|
|
@@ -92,27 +399,27 @@ function isIndexSatisfied(indexes, uniques, columns) {
|
|
|
92
399
|
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
93
400
|
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
94
401
|
*/
|
|
95
|
-
function verifyPrimaryKey(contractPK, schemaPK, tableName, namespaceId, issues) {
|
|
402
|
+
function verifyPrimaryKey(contractPK, schemaPK, tableName, namespaceId, tableControlPolicy, issues) {
|
|
96
403
|
if (!schemaPK) {
|
|
97
|
-
|
|
404
|
+
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, {
|
|
98
405
|
kind: "primary_key_mismatch",
|
|
99
406
|
table: tableName,
|
|
100
407
|
namespaceId,
|
|
101
408
|
expected: contractPK.columns.join(", "),
|
|
102
409
|
message: `Table "${tableName}" is missing primary key`
|
|
103
|
-
});
|
|
104
|
-
return "
|
|
410
|
+
}, issues);
|
|
411
|
+
return outcome === "suppress" ? "pass" : outcome;
|
|
105
412
|
}
|
|
106
413
|
if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
|
|
107
|
-
|
|
414
|
+
const outcome = emitIssueUnderControlPolicy(tableControlPolicy, {
|
|
108
415
|
kind: "primary_key_mismatch",
|
|
109
416
|
table: tableName,
|
|
110
417
|
namespaceId,
|
|
111
418
|
expected: contractPK.columns.join(", "),
|
|
112
419
|
actual: schemaPK.columns.join(", "),
|
|
113
420
|
message: `Table "${tableName}" has primary key mismatch: expected columns [${contractPK.columns.join(", ")}], got [${schemaPK.columns.join(", ")}]`
|
|
114
|
-
});
|
|
115
|
-
return "
|
|
421
|
+
}, issues);
|
|
422
|
+
return outcome === "suppress" ? "pass" : outcome;
|
|
116
423
|
}
|
|
117
424
|
return "pass";
|
|
118
425
|
}
|
|
@@ -123,7 +430,7 @@ function verifyPrimaryKey(contractPK, schemaPK, tableName, namespaceId, issues)
|
|
|
123
430
|
* Uses semantic satisfaction: identity is based on (table + columns + referenced table + referenced columns).
|
|
124
431
|
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
125
432
|
*/
|
|
126
|
-
function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, tablePath, issues, strict) {
|
|
433
|
+
function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict) {
|
|
127
434
|
const nodes = [];
|
|
128
435
|
for (const contractFK of contractFKs) {
|
|
129
436
|
const fkPath = `${tablePath}.foreignKeys[${contractFK.source.columns.join(",")}]`;
|
|
@@ -131,32 +438,30 @@ function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, table
|
|
|
131
438
|
const tablesMatch = fk.referencedSchema !== void 0 && contractFK.target.namespaceId !== UNBOUND_NAMESPACE_ID ? fk.referencedSchema === contractFK.target.namespaceId && fk.referencedTable === contractFK.target.tableName : fk.referencedTable === contractFK.target.tableName;
|
|
132
439
|
return arraysEqual(fk.columns, contractFK.source.columns) && tablesMatch && arraysEqual(fk.referencedColumns, contractFK.target.columns);
|
|
133
440
|
});
|
|
134
|
-
if (!matchingFK) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
} else {
|
|
441
|
+
if (!matchingFK) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
442
|
+
kind: "foreign_key_mismatch",
|
|
443
|
+
table: tableName,
|
|
444
|
+
namespaceId,
|
|
445
|
+
expected: `${contractFK.source.columns.join(", ")} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(", ")})`,
|
|
446
|
+
message: `Table "${tableName}" is missing foreign key: ${contractFK.source.columns.join(", ")} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(", ")})`
|
|
447
|
+
}, {
|
|
448
|
+
status: "fail",
|
|
449
|
+
kind: "foreignKey",
|
|
450
|
+
name: `foreignKey(${contractFK.source.columns.join(", ")})`,
|
|
451
|
+
contractPath: fkPath,
|
|
452
|
+
code: "foreign_key_mismatch",
|
|
453
|
+
message: "Foreign key missing",
|
|
454
|
+
expected: contractFK,
|
|
455
|
+
actual: void 0,
|
|
456
|
+
children: []
|
|
457
|
+
}, issues, nodes);
|
|
458
|
+
else {
|
|
154
459
|
const actionMismatches = getReferentialActionMismatches(contractFK, matchingFK);
|
|
155
460
|
if (actionMismatches.length > 0) {
|
|
156
461
|
const combinedMessage = actionMismatches.map((m) => m.message).join("; ");
|
|
157
462
|
const combinedExpected = actionMismatches.map((m) => m.expected).join(", ");
|
|
158
463
|
const combinedActual = actionMismatches.map((m) => m.actual).join(", ");
|
|
159
|
-
|
|
464
|
+
emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
160
465
|
kind: "foreign_key_mismatch",
|
|
161
466
|
table: tableName,
|
|
162
467
|
namespaceId,
|
|
@@ -164,8 +469,7 @@ function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, table
|
|
|
164
469
|
expected: combinedExpected,
|
|
165
470
|
actual: combinedActual,
|
|
166
471
|
message: `Table "${tableName}" foreign key ${contractFK.source.columns.join(", ")} -> ${contractFK.target.tableName}: ${combinedMessage}`
|
|
167
|
-
}
|
|
168
|
-
nodes.push({
|
|
472
|
+
}, {
|
|
169
473
|
status: "fail",
|
|
170
474
|
kind: "foreignKey",
|
|
171
475
|
name: `foreignKey(${contractFK.source.columns.join(", ")})`,
|
|
@@ -175,7 +479,7 @@ function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, table
|
|
|
175
479
|
expected: contractFK,
|
|
176
480
|
actual: matchingFK,
|
|
177
481
|
children: []
|
|
178
|
-
});
|
|
482
|
+
}, issues, nodes);
|
|
179
483
|
} else nodes.push({
|
|
180
484
|
status: "pass",
|
|
181
485
|
kind: "foreignKey",
|
|
@@ -193,26 +497,23 @@ function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, table
|
|
|
193
497
|
for (const schemaFK of schemaFKs) if (!contractFKs.find((fk) => {
|
|
194
498
|
const tablesMatch = schemaFK.referencedSchema !== void 0 && fk.target.namespaceId !== UNBOUND_NAMESPACE_ID ? schemaFK.referencedSchema === fk.target.namespaceId && schemaFK.referencedTable === fk.target.tableName : schemaFK.referencedTable === fk.target.tableName;
|
|
195
499
|
return arraysEqual(fk.source.columns, schemaFK.columns) && tablesMatch && arraysEqual(fk.target.columns, schemaFK.referencedColumns);
|
|
196
|
-
})) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
children: []
|
|
214
|
-
});
|
|
215
|
-
}
|
|
500
|
+
})) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
501
|
+
kind: "extra_foreign_key",
|
|
502
|
+
table: tableName,
|
|
503
|
+
namespaceId,
|
|
504
|
+
indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(",")})`,
|
|
505
|
+
message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(", ")} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(", ")})`
|
|
506
|
+
}, {
|
|
507
|
+
status: "fail",
|
|
508
|
+
kind: "foreignKey",
|
|
509
|
+
name: `foreignKey(${schemaFK.columns.join(", ")})`,
|
|
510
|
+
contractPath: `${tablePath}.foreignKeys[${schemaFK.columns.join(",")}]`,
|
|
511
|
+
code: "extra_foreign_key",
|
|
512
|
+
message: "Extra foreign key found",
|
|
513
|
+
expected: void 0,
|
|
514
|
+
actual: schemaFK,
|
|
515
|
+
children: []
|
|
516
|
+
}, issues, nodes);
|
|
216
517
|
}
|
|
217
518
|
return nodes;
|
|
218
519
|
}
|
|
@@ -227,32 +528,30 @@ function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, table
|
|
|
227
528
|
*
|
|
228
529
|
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
229
530
|
*/
|
|
230
|
-
function verifyUniqueConstraints(contractUniques, schemaUniques, schemaIndexes, tableName, namespaceId, tablePath, issues, strict) {
|
|
531
|
+
function verifyUniqueConstraints(contractUniques, schemaUniques, schemaIndexes, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict) {
|
|
231
532
|
const nodes = [];
|
|
232
533
|
for (const contractUnique of contractUniques) {
|
|
233
534
|
const uniquePath = `${tablePath}.uniques[${contractUnique.columns.join(",")}]`;
|
|
234
535
|
const matchingUnique = schemaUniques.find((u) => arraysEqual(u.columns, contractUnique.columns));
|
|
235
536
|
const matchingUniqueIndex = !matchingUnique && schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
|
|
236
|
-
if (!matchingUnique && !matchingUniqueIndex) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
});
|
|
255
|
-
} else nodes.push({
|
|
537
|
+
if (!matchingUnique && !matchingUniqueIndex) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
538
|
+
kind: "unique_constraint_mismatch",
|
|
539
|
+
table: tableName,
|
|
540
|
+
namespaceId,
|
|
541
|
+
expected: contractUnique.columns.join(", "),
|
|
542
|
+
message: `Table "${tableName}" is missing unique constraint: ${contractUnique.columns.join(", ")}`
|
|
543
|
+
}, {
|
|
544
|
+
status: "fail",
|
|
545
|
+
kind: "unique",
|
|
546
|
+
name: `unique(${contractUnique.columns.join(", ")})`,
|
|
547
|
+
contractPath: uniquePath,
|
|
548
|
+
code: "unique_constraint_mismatch",
|
|
549
|
+
message: "Unique constraint missing",
|
|
550
|
+
expected: contractUnique,
|
|
551
|
+
actual: void 0,
|
|
552
|
+
children: []
|
|
553
|
+
}, issues, nodes);
|
|
554
|
+
else nodes.push({
|
|
256
555
|
status: "pass",
|
|
257
556
|
kind: "unique",
|
|
258
557
|
name: `unique(${contractUnique.columns.join(", ")})`,
|
|
@@ -265,26 +564,23 @@ function verifyUniqueConstraints(contractUniques, schemaUniques, schemaIndexes,
|
|
|
265
564
|
});
|
|
266
565
|
}
|
|
267
566
|
if (strict) {
|
|
268
|
-
for (const schemaUnique of schemaUniques) if (!contractUniques.find((u) => arraysEqual(u.columns, schemaUnique.columns))) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
children: []
|
|
286
|
-
});
|
|
287
|
-
}
|
|
567
|
+
for (const schemaUnique of schemaUniques) if (!contractUniques.find((u) => arraysEqual(u.columns, schemaUnique.columns))) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
568
|
+
kind: "extra_unique_constraint",
|
|
569
|
+
table: tableName,
|
|
570
|
+
namespaceId,
|
|
571
|
+
indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(",")})`,
|
|
572
|
+
message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(", ")}`
|
|
573
|
+
}, {
|
|
574
|
+
status: "fail",
|
|
575
|
+
kind: "unique",
|
|
576
|
+
name: `unique(${schemaUnique.columns.join(", ")})`,
|
|
577
|
+
contractPath: `${tablePath}.uniques[${schemaUnique.columns.join(",")}]`,
|
|
578
|
+
code: "extra_unique_constraint",
|
|
579
|
+
message: "Extra unique constraint found",
|
|
580
|
+
expected: void 0,
|
|
581
|
+
actual: schemaUnique,
|
|
582
|
+
children: []
|
|
583
|
+
}, issues, nodes);
|
|
288
584
|
}
|
|
289
585
|
return nodes;
|
|
290
586
|
}
|
|
@@ -299,32 +595,30 @@ function verifyUniqueConstraints(contractUniques, schemaUniques, schemaIndexes,
|
|
|
299
595
|
*
|
|
300
596
|
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
301
597
|
*/
|
|
302
|
-
function verifyIndexes(contractIndexes, schemaIndexes, schemaUniques, tableName, namespaceId, tablePath, issues, strict) {
|
|
598
|
+
function verifyIndexes(contractIndexes, schemaIndexes, schemaUniques, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict) {
|
|
303
599
|
const nodes = [];
|
|
304
600
|
for (const contractIndex of contractIndexes) {
|
|
305
601
|
const indexPath = `${tablePath}.indexes[${contractIndex.columns.join(",")}]`;
|
|
306
602
|
const matchingIndex = schemaIndexes.find((idx) => arraysEqual(idx.columns, contractIndex.columns) && indexExtrasMatch(contractIndex, idx));
|
|
307
603
|
const matchingUniqueConstraint = !matchingIndex && contractIndex.type === void 0 && contractIndex.options === void 0 && schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
|
|
308
|
-
if (!matchingIndex && !matchingUniqueConstraint) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
});
|
|
327
|
-
} else nodes.push({
|
|
604
|
+
if (!matchingIndex && !matchingUniqueConstraint) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
605
|
+
kind: "index_mismatch",
|
|
606
|
+
table: tableName,
|
|
607
|
+
namespaceId,
|
|
608
|
+
expected: contractIndex.columns.join(", "),
|
|
609
|
+
message: `Table "${tableName}" is missing index: ${contractIndex.columns.join(", ")}`
|
|
610
|
+
}, {
|
|
611
|
+
status: "fail",
|
|
612
|
+
kind: "index",
|
|
613
|
+
name: `index(${contractIndex.columns.join(", ")})`,
|
|
614
|
+
contractPath: indexPath,
|
|
615
|
+
code: "index_mismatch",
|
|
616
|
+
message: "Index missing",
|
|
617
|
+
expected: contractIndex,
|
|
618
|
+
actual: void 0,
|
|
619
|
+
children: []
|
|
620
|
+
}, issues, nodes);
|
|
621
|
+
else nodes.push({
|
|
328
622
|
status: "pass",
|
|
329
623
|
kind: "index",
|
|
330
624
|
name: `index(${contractIndex.columns.join(", ")})`,
|
|
@@ -338,26 +632,23 @@ function verifyIndexes(contractIndexes, schemaIndexes, schemaUniques, tableName,
|
|
|
338
632
|
}
|
|
339
633
|
if (strict) for (const schemaIndex of schemaIndexes) {
|
|
340
634
|
if (schemaIndex.unique) continue;
|
|
341
|
-
if (!contractIndexes.find((idx) => arraysEqual(idx.columns, schemaIndex.columns) && indexExtrasMatch(idx, schemaIndex))) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
children: []
|
|
359
|
-
});
|
|
360
|
-
}
|
|
635
|
+
if (!contractIndexes.find((idx) => arraysEqual(idx.columns, schemaIndex.columns) && indexExtrasMatch(idx, schemaIndex))) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
636
|
+
kind: "extra_index",
|
|
637
|
+
table: tableName,
|
|
638
|
+
namespaceId,
|
|
639
|
+
indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(",")})`,
|
|
640
|
+
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(", ")}`
|
|
641
|
+
}, {
|
|
642
|
+
status: "fail",
|
|
643
|
+
kind: "index",
|
|
644
|
+
name: `index(${schemaIndex.columns.join(", ")})`,
|
|
645
|
+
contractPath: `${tablePath}.indexes[${schemaIndex.columns.join(",")}]`,
|
|
646
|
+
code: "extra_index",
|
|
647
|
+
message: "Extra index found",
|
|
648
|
+
expected: void 0,
|
|
649
|
+
actual: schemaIndex,
|
|
650
|
+
children: []
|
|
651
|
+
}, issues, nodes);
|
|
361
652
|
}
|
|
362
653
|
return nodes;
|
|
363
654
|
}
|
|
@@ -416,6 +707,110 @@ function getReferentialActionMismatches(contractFK, schemaFK) {
|
|
|
416
707
|
function normalizeReferentialAction(action) {
|
|
417
708
|
return action === "noAction" ? void 0 : action;
|
|
418
709
|
}
|
|
710
|
+
/**
|
|
711
|
+
* Compares two value arrays as unordered sets.
|
|
712
|
+
* Returns true when both sides contain exactly the same values.
|
|
713
|
+
*/
|
|
714
|
+
function valueSetsEqual(a, b) {
|
|
715
|
+
const aSet = new Set(a);
|
|
716
|
+
const bSet = new Set(b);
|
|
717
|
+
if (aSet.size !== bSet.size) return false;
|
|
718
|
+
return [...aSet].every((v) => bSet.has(v));
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Verifies check constraints match between contract-projected checks and
|
|
722
|
+
* introspected live checks.
|
|
723
|
+
*
|
|
724
|
+
* Comparison is value-set-based, not SQL-string-based. Postgres rewrites
|
|
725
|
+
* `col IN ('a','b')` as `col = ANY (ARRAY['a','b'])` in
|
|
726
|
+
* `pg_get_constraintdef`, so comparing the extracted value sets (after
|
|
727
|
+
* the introspection adapter parses the predicate) avoids false mismatches
|
|
728
|
+
* from the `IN`-vs-`= ANY (ARRAY…)` rendering difference.
|
|
729
|
+
*
|
|
730
|
+
* Issues emitted:
|
|
731
|
+
* - `check_missing` — check expected by contract but absent from live DB
|
|
732
|
+
* - `check_removed` — check present in live DB but not in contract
|
|
733
|
+
* - `check_mismatch` — check present on both sides but permitted values differ
|
|
734
|
+
*
|
|
735
|
+
* `check_removed` is emitted only when `strict` is true so non-strict
|
|
736
|
+
* verification (the normal path) does not complain about extra constraints.
|
|
737
|
+
*/
|
|
738
|
+
function verifyCheckConstraints(contractChecks, schemaChecks, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict) {
|
|
739
|
+
const nodes = [];
|
|
740
|
+
for (const contractCheck of contractChecks) {
|
|
741
|
+
const checkPath = `${tablePath}.checks[${contractCheck.name}]`;
|
|
742
|
+
const liveCheck = schemaChecks.find((c) => c.name === contractCheck.name);
|
|
743
|
+
if (!liveCheck) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
744
|
+
kind: "check_missing",
|
|
745
|
+
table: tableName,
|
|
746
|
+
namespaceId,
|
|
747
|
+
indexOrConstraint: contractCheck.name,
|
|
748
|
+
expected: contractCheck.permittedValues.join(", "),
|
|
749
|
+
message: `Table "${tableName}" is missing check constraint "${contractCheck.name}" (column "${contractCheck.column}" IN (${contractCheck.permittedValues.join(", ")}))`
|
|
750
|
+
}, {
|
|
751
|
+
status: "fail",
|
|
752
|
+
kind: "checkConstraint",
|
|
753
|
+
name: `check(${contractCheck.name})`,
|
|
754
|
+
contractPath: checkPath,
|
|
755
|
+
code: "check_missing",
|
|
756
|
+
message: `Check constraint "${contractCheck.name}" missing`,
|
|
757
|
+
expected: contractCheck,
|
|
758
|
+
actual: void 0,
|
|
759
|
+
children: []
|
|
760
|
+
}, issues, nodes);
|
|
761
|
+
else if (!valueSetsEqual(contractCheck.permittedValues, liveCheck.permittedValues)) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
762
|
+
kind: "check_mismatch",
|
|
763
|
+
table: tableName,
|
|
764
|
+
namespaceId,
|
|
765
|
+
indexOrConstraint: contractCheck.name,
|
|
766
|
+
expected: contractCheck.permittedValues.join(", "),
|
|
767
|
+
actual: liveCheck.permittedValues.join(", "),
|
|
768
|
+
message: `Table "${tableName}" check constraint "${contractCheck.name}" has different permitted values: expected [${contractCheck.permittedValues.join(", ")}], got [${liveCheck.permittedValues.join(", ")}]`
|
|
769
|
+
}, {
|
|
770
|
+
status: "fail",
|
|
771
|
+
kind: "checkConstraint",
|
|
772
|
+
name: `check(${contractCheck.name})`,
|
|
773
|
+
contractPath: checkPath,
|
|
774
|
+
code: "check_mismatch",
|
|
775
|
+
message: `Check constraint "${contractCheck.name}" values mismatch`,
|
|
776
|
+
expected: contractCheck,
|
|
777
|
+
actual: liveCheck,
|
|
778
|
+
children: []
|
|
779
|
+
}, issues, nodes);
|
|
780
|
+
else nodes.push({
|
|
781
|
+
status: "pass",
|
|
782
|
+
kind: "checkConstraint",
|
|
783
|
+
name: `check(${contractCheck.name})`,
|
|
784
|
+
contractPath: checkPath,
|
|
785
|
+
code: "",
|
|
786
|
+
message: "",
|
|
787
|
+
expected: void 0,
|
|
788
|
+
actual: void 0,
|
|
789
|
+
children: []
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
if (strict) {
|
|
793
|
+
for (const liveCheck of schemaChecks) if (!contractChecks.find((c) => c.name === liveCheck.name)) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
794
|
+
kind: "check_removed",
|
|
795
|
+
table: tableName,
|
|
796
|
+
namespaceId,
|
|
797
|
+
indexOrConstraint: liveCheck.name,
|
|
798
|
+
actual: liveCheck.permittedValues.join(", "),
|
|
799
|
+
message: `Table "${tableName}" has extra check constraint "${liveCheck.name}" in database (not in contract)`
|
|
800
|
+
}, {
|
|
801
|
+
status: "fail",
|
|
802
|
+
kind: "checkConstraint",
|
|
803
|
+
name: `check(${liveCheck.name})`,
|
|
804
|
+
contractPath: `${tablePath}.checks[${liveCheck.name}]`,
|
|
805
|
+
code: "check_removed",
|
|
806
|
+
message: `Extra check constraint "${liveCheck.name}" found`,
|
|
807
|
+
expected: void 0,
|
|
808
|
+
actual: liveCheck,
|
|
809
|
+
children: []
|
|
810
|
+
}, issues, nodes);
|
|
811
|
+
}
|
|
812
|
+
return nodes;
|
|
813
|
+
}
|
|
419
814
|
//#endregion
|
|
420
815
|
//#region src/core/schema-verify/verify-sql-schema.ts
|
|
421
816
|
/**
|
|
@@ -435,7 +830,7 @@ function verifySqlSchema(options) {
|
|
|
435
830
|
const { contractStorageHash, contractProfileHash, contractTarget } = extractContractMetadata(contract);
|
|
436
831
|
const allStorageTypesMap = { ...contract.storage.types ?? {} };
|
|
437
832
|
for (const ns of Object.values(contract.storage.namespaces)) {
|
|
438
|
-
const nsEnums = ns.
|
|
833
|
+
const nsEnums = blindCast(ns.entries).type;
|
|
439
834
|
if (nsEnums) for (const [k, v] of Object.entries(nsEnums)) allStorageTypesMap[k] = v;
|
|
440
835
|
}
|
|
441
836
|
const { issues, rootChildren } = verifySchemaTables({
|
|
@@ -450,53 +845,57 @@ function verifySqlSchema(options) {
|
|
|
450
845
|
});
|
|
451
846
|
validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
|
|
452
847
|
const typeNodes = [];
|
|
453
|
-
const pushTypeNode = (typeName, contractPath, typeIssues) => {
|
|
454
|
-
|
|
848
|
+
const pushTypeNode = (typeName, contractPath, typeIssues, controlPolicy) => {
|
|
849
|
+
let status = "pass";
|
|
850
|
+
let code = "";
|
|
851
|
+
let emitted = 0;
|
|
852
|
+
for (const issue of typeIssues) {
|
|
853
|
+
const disposition = verifierDisposition(controlPolicy, issue.kind);
|
|
854
|
+
if (disposition === "suppress") continue;
|
|
855
|
+
issues.push(issue);
|
|
856
|
+
emitted += 1;
|
|
857
|
+
if (code === "") code = issue.kind;
|
|
858
|
+
if (disposition === "fail") status = "fail";
|
|
859
|
+
else if (disposition === "warn" && status !== "fail") status = "warn";
|
|
860
|
+
}
|
|
455
861
|
typeNodes.push({
|
|
456
|
-
status
|
|
862
|
+
status,
|
|
457
863
|
kind: "storageType",
|
|
458
864
|
name: `type ${typeName}`,
|
|
459
865
|
contractPath,
|
|
460
|
-
code:
|
|
461
|
-
message:
|
|
866
|
+
code: status === "pass" ? "" : code,
|
|
867
|
+
message: emitted > 0 ? `${emitted} issue${emitted === 1 ? "" : "s"}` : "",
|
|
462
868
|
expected: void 0,
|
|
463
869
|
actual: void 0,
|
|
464
870
|
children: []
|
|
465
871
|
});
|
|
466
872
|
};
|
|
467
|
-
for (const [typeName, typeInstance] of Object.entries(contract.storage.types ?? {})) if (
|
|
468
|
-
typeName,
|
|
469
|
-
typeInstance,
|
|
470
|
-
schema,
|
|
471
|
-
resolveExistingEnumValues,
|
|
472
|
-
namespaceId: UNBOUND_NAMESPACE_ID
|
|
473
|
-
}));
|
|
474
|
-
else if (isStorageTypeInstance(typeInstance)) {
|
|
873
|
+
for (const [typeName, typeInstance] of Object.entries(contract.storage.types ?? {})) if (isStorageTypeInstance(typeInstance)) {
|
|
475
874
|
const hook = codecHooks.get(typeInstance.codecId);
|
|
476
875
|
pushTypeNode(typeName, `storage.types.${typeName}`, hook?.verifyType ? hook.verifyType({
|
|
477
876
|
typeName,
|
|
478
877
|
typeInstance,
|
|
479
878
|
schema
|
|
480
|
-
}) : []);
|
|
879
|
+
}) : [], effectiveControlPolicy(void 0, contract.defaultControlPolicy));
|
|
481
880
|
}
|
|
482
881
|
for (const nsId of Object.keys(contract.storage.namespaces)) {
|
|
483
882
|
const ns = contract.storage.namespaces[nsId];
|
|
484
883
|
if (!ns) continue;
|
|
485
|
-
const nsEnums = ns.
|
|
884
|
+
const nsEnums = ns.entries["type"];
|
|
486
885
|
if (!nsEnums) continue;
|
|
487
886
|
for (const [typeName, entry] of Object.entries(nsEnums)) {
|
|
488
887
|
if (!isPostgresEnumStorageEntry(entry)) continue;
|
|
489
|
-
pushTypeNode(typeName, `storage.namespaces.${nsId}.
|
|
888
|
+
pushTypeNode(typeName, `storage.namespaces.${nsId}.entries.type.${typeName}`, verifyEnumType({
|
|
490
889
|
typeName,
|
|
491
890
|
typeInstance: entry,
|
|
492
891
|
schema,
|
|
493
892
|
resolveExistingEnumValues,
|
|
494
893
|
namespaceId: nsId
|
|
495
|
-
}));
|
|
894
|
+
}), effectiveControlPolicy(entry.control, contract.defaultControlPolicy));
|
|
496
895
|
}
|
|
497
896
|
}
|
|
498
897
|
if (typeNodes.length > 0) {
|
|
499
|
-
const typesStatus = typeNodes.some((n) => n.status === "fail") ? "fail" : "pass";
|
|
898
|
+
const typesStatus = typeNodes.some((n) => n.status === "fail") ? "fail" : typeNodes.some((n) => n.status === "warn") ? "warn" : "pass";
|
|
500
899
|
rootChildren.push({
|
|
501
900
|
status: typesStatus,
|
|
502
901
|
kind: "storageTypes",
|
|
@@ -581,6 +980,7 @@ function extractContractMetadata(contract) {
|
|
|
581
980
|
}
|
|
582
981
|
function verifySchemaTables(options) {
|
|
583
982
|
const { contract, schema, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType } = options;
|
|
983
|
+
const contractDefaultControl = contract.defaultControlPolicy;
|
|
584
984
|
const issues = [];
|
|
585
985
|
const rootChildren = [];
|
|
586
986
|
const schemaTables = schema.tables;
|
|
@@ -588,19 +988,19 @@ function verifySchemaTables(options) {
|
|
|
588
988
|
for (const namespaceId of namespaceIds) {
|
|
589
989
|
const ns = contract.storage.namespaces[namespaceId];
|
|
590
990
|
if (!ns) continue;
|
|
591
|
-
for (const [tableName, contractTableRaw] of Object.entries(ns.
|
|
592
|
-
if (!(contractTableRaw instanceof StorageTable)) throw new Error(`verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.
|
|
991
|
+
for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table)) {
|
|
992
|
+
if (!(contractTableRaw instanceof StorageTable)) throw new Error(`verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.entries.table.${tableName}`);
|
|
593
993
|
const contractTable = contractTableRaw;
|
|
994
|
+
const tableControlPolicy = effectiveControlPolicy(contractTable.control, contractDefaultControl);
|
|
594
995
|
const schemaTable = schemaTables[tableName];
|
|
595
|
-
const tablePath = `storage.namespaces.${namespaceId}.
|
|
996
|
+
const tablePath = `storage.namespaces.${namespaceId}.entries.table.${tableName}`;
|
|
596
997
|
if (!schemaTable) {
|
|
597
|
-
|
|
998
|
+
emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
598
999
|
kind: "missing_table",
|
|
599
1000
|
table: tableName,
|
|
600
1001
|
namespaceId,
|
|
601
1002
|
message: `Table "${tableName}" is missing from database`
|
|
602
|
-
}
|
|
603
|
-
rootChildren.push({
|
|
1003
|
+
}, {
|
|
604
1004
|
status: "fail",
|
|
605
1005
|
kind: "table",
|
|
606
1006
|
name: `table ${tableName}`,
|
|
@@ -610,7 +1010,7 @@ function verifySchemaTables(options) {
|
|
|
610
1010
|
expected: void 0,
|
|
611
1011
|
actual: void 0,
|
|
612
1012
|
children: []
|
|
613
|
-
});
|
|
1013
|
+
}, issues, rootChildren);
|
|
614
1014
|
continue;
|
|
615
1015
|
}
|
|
616
1016
|
const tableChildren = verifyTableChildren({
|
|
@@ -619,11 +1019,13 @@ function verifySchemaTables(options) {
|
|
|
619
1019
|
tableName,
|
|
620
1020
|
namespaceId,
|
|
621
1021
|
tablePath,
|
|
1022
|
+
tableControlPolicy,
|
|
622
1023
|
issues,
|
|
623
1024
|
strict,
|
|
624
1025
|
typeMetadataRegistry,
|
|
625
1026
|
codecHooks,
|
|
626
1027
|
storageTypes,
|
|
1028
|
+
contractStorage: contract.storage,
|
|
627
1029
|
...ifDefined("normalizeDefault", normalizeDefault),
|
|
628
1030
|
...ifDefined("normalizeNativeType", normalizeNativeType)
|
|
629
1031
|
});
|
|
@@ -631,24 +1033,21 @@ function verifySchemaTables(options) {
|
|
|
631
1033
|
}
|
|
632
1034
|
}
|
|
633
1035
|
if (strict) {
|
|
634
|
-
for (const tableName of Object.keys(schemaTables)) if (!namespaceIds.some((namespaceId) => contract.storage.namespaces[namespaceId]?.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
children: []
|
|
650
|
-
});
|
|
651
|
-
}
|
|
1036
|
+
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), {
|
|
1037
|
+
kind: "extra_table",
|
|
1038
|
+
table: tableName,
|
|
1039
|
+
message: `Extra table "${tableName}" found in database (not in contract)`
|
|
1040
|
+
}, {
|
|
1041
|
+
status: "fail",
|
|
1042
|
+
kind: "table",
|
|
1043
|
+
name: `table ${tableName}`,
|
|
1044
|
+
contractPath: `storage.namespaces.*.entries.table.${tableName}`,
|
|
1045
|
+
code: "extra_table",
|
|
1046
|
+
message: `Extra table "${tableName}" found`,
|
|
1047
|
+
expected: void 0,
|
|
1048
|
+
actual: void 0,
|
|
1049
|
+
children: []
|
|
1050
|
+
}, issues, rootChildren);
|
|
652
1051
|
}
|
|
653
1052
|
return {
|
|
654
1053
|
issues,
|
|
@@ -656,7 +1055,7 @@ function verifySchemaTables(options) {
|
|
|
656
1055
|
};
|
|
657
1056
|
}
|
|
658
1057
|
function verifyTableChildren(options) {
|
|
659
|
-
const { contractTable, schemaTable, tableName, namespaceId, tablePath, issues, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType } = options;
|
|
1058
|
+
const { contractTable, schemaTable, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType, contractStorage } = options;
|
|
660
1059
|
const tableChildren = [];
|
|
661
1060
|
const columnNodes = collectContractColumnNodes({
|
|
662
1061
|
contractTable,
|
|
@@ -664,6 +1063,7 @@ function verifyTableChildren(options) {
|
|
|
664
1063
|
tableName,
|
|
665
1064
|
namespaceId,
|
|
666
1065
|
tablePath,
|
|
1066
|
+
tableControlPolicy,
|
|
667
1067
|
issues,
|
|
668
1068
|
strict,
|
|
669
1069
|
typeMetadataRegistry,
|
|
@@ -679,77 +1079,96 @@ function verifyTableChildren(options) {
|
|
|
679
1079
|
tableName,
|
|
680
1080
|
namespaceId,
|
|
681
1081
|
tablePath,
|
|
1082
|
+
tableControlPolicy,
|
|
682
1083
|
issues,
|
|
683
1084
|
columnNodes
|
|
684
1085
|
});
|
|
685
|
-
if (contractTable.primaryKey)
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
|
|
689
|
-
contractPath: `${tablePath}.primaryKey`,
|
|
690
|
-
code: "primary_key_mismatch",
|
|
691
|
-
message: "Primary key mismatch",
|
|
692
|
-
expected: contractTable.primaryKey,
|
|
693
|
-
actual: schemaTable.primaryKey,
|
|
694
|
-
children: []
|
|
695
|
-
});
|
|
696
|
-
else tableChildren.push({
|
|
697
|
-
status: "pass",
|
|
698
|
-
kind: "primaryKey",
|
|
699
|
-
name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
|
|
700
|
-
contractPath: `${tablePath}.primaryKey`,
|
|
701
|
-
code: "",
|
|
702
|
-
message: "",
|
|
703
|
-
expected: void 0,
|
|
704
|
-
actual: void 0,
|
|
705
|
-
children: []
|
|
706
|
-
});
|
|
707
|
-
else if (schemaTable.primaryKey && strict) {
|
|
708
|
-
issues.push({
|
|
709
|
-
kind: "extra_primary_key",
|
|
710
|
-
table: tableName,
|
|
711
|
-
namespaceId,
|
|
712
|
-
message: "Extra primary key found in database (not in contract)"
|
|
713
|
-
});
|
|
714
|
-
tableChildren.push({
|
|
1086
|
+
if (contractTable.primaryKey) {
|
|
1087
|
+
const pkStatus = verifyPrimaryKey(contractTable.primaryKey, schemaTable.primaryKey, tableName, namespaceId, tableControlPolicy, issues);
|
|
1088
|
+
if (pkStatus === "fail") tableChildren.push({
|
|
715
1089
|
status: "fail",
|
|
716
1090
|
kind: "primaryKey",
|
|
717
|
-
name: `primary key: ${
|
|
1091
|
+
name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
|
|
718
1092
|
contractPath: `${tablePath}.primaryKey`,
|
|
719
|
-
code: "
|
|
720
|
-
message: "
|
|
721
|
-
expected:
|
|
1093
|
+
code: "primary_key_mismatch",
|
|
1094
|
+
message: "Primary key mismatch",
|
|
1095
|
+
expected: contractTable.primaryKey,
|
|
722
1096
|
actual: schemaTable.primaryKey,
|
|
723
1097
|
children: []
|
|
724
1098
|
});
|
|
725
|
-
|
|
1099
|
+
else if (pkStatus === "warn") tableChildren.push({
|
|
1100
|
+
status: "warn",
|
|
1101
|
+
kind: "primaryKey",
|
|
1102
|
+
name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
|
|
1103
|
+
contractPath: `${tablePath}.primaryKey`,
|
|
1104
|
+
code: "primary_key_mismatch",
|
|
1105
|
+
message: "Primary key mismatch",
|
|
1106
|
+
expected: contractTable.primaryKey,
|
|
1107
|
+
actual: schemaTable.primaryKey,
|
|
1108
|
+
children: []
|
|
1109
|
+
});
|
|
1110
|
+
else tableChildren.push({
|
|
1111
|
+
status: "pass",
|
|
1112
|
+
kind: "primaryKey",
|
|
1113
|
+
name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
|
|
1114
|
+
contractPath: `${tablePath}.primaryKey`,
|
|
1115
|
+
code: "",
|
|
1116
|
+
message: "",
|
|
1117
|
+
expected: void 0,
|
|
1118
|
+
actual: void 0,
|
|
1119
|
+
children: []
|
|
1120
|
+
});
|
|
1121
|
+
} else if (schemaTable.primaryKey && strict) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
1122
|
+
kind: "extra_primary_key",
|
|
1123
|
+
table: tableName,
|
|
1124
|
+
namespaceId,
|
|
1125
|
+
message: "Extra primary key found in database (not in contract)"
|
|
1126
|
+
}, {
|
|
1127
|
+
status: "fail",
|
|
1128
|
+
kind: "primaryKey",
|
|
1129
|
+
name: `primary key: ${schemaTable.primaryKey.columns.join(", ")}`,
|
|
1130
|
+
contractPath: `${tablePath}.primaryKey`,
|
|
1131
|
+
code: "extra_primary_key",
|
|
1132
|
+
message: "Extra primary key found",
|
|
1133
|
+
expected: void 0,
|
|
1134
|
+
actual: schemaTable.primaryKey,
|
|
1135
|
+
children: []
|
|
1136
|
+
}, issues, tableChildren);
|
|
726
1137
|
const constraintFks = contractTable.foreignKeys.filter((fk) => fk.constraint === true);
|
|
727
1138
|
if (constraintFks.length > 0 || strict) {
|
|
728
|
-
const fkStatuses = verifyForeignKeys(constraintFks, schemaTable.foreignKeys, tableName, namespaceId, tablePath, issues, strict);
|
|
1139
|
+
const fkStatuses = verifyForeignKeys(constraintFks, schemaTable.foreignKeys, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict);
|
|
729
1140
|
tableChildren.push(...fkStatuses);
|
|
730
1141
|
}
|
|
731
|
-
const uniqueStatuses = verifyUniqueConstraints(contractTable.uniques, schemaTable.uniques, schemaTable.indexes, tableName, namespaceId, tablePath, issues, strict);
|
|
1142
|
+
const uniqueStatuses = verifyUniqueConstraints(contractTable.uniques, schemaTable.uniques, schemaTable.indexes, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict);
|
|
732
1143
|
tableChildren.push(...uniqueStatuses);
|
|
733
1144
|
const fkBackingIndexes = contractTable.foreignKeys.filter((fk) => fk.index === true && !contractTable.indexes.some((idx) => arraysEqual(idx.columns, fk.source.columns))).map((fk) => ({ columns: fk.source.columns }));
|
|
734
|
-
const indexStatuses = verifyIndexes([...contractTable.indexes, ...fkBackingIndexes], schemaTable.indexes, schemaTable.uniques, tableName, namespaceId, tablePath, issues, strict);
|
|
1145
|
+
const indexStatuses = verifyIndexes([...contractTable.indexes, ...fkBackingIndexes], schemaTable.indexes, schemaTable.uniques, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict);
|
|
735
1146
|
tableChildren.push(...indexStatuses);
|
|
1147
|
+
const contractCheckIRs = (contractTable.checks ?? []).map((c) => ({
|
|
1148
|
+
name: c.name,
|
|
1149
|
+
column: c.column,
|
|
1150
|
+
permittedValues: resolveValueSetValues(c.valueSet, contractStorage, `check "${c.name}"`)
|
|
1151
|
+
}));
|
|
1152
|
+
if (strict || contractCheckIRs.length > 0) {
|
|
1153
|
+
const checkStatuses = verifyCheckConstraints(contractCheckIRs, schemaTable.checks ?? [], tableName, namespaceId, tablePath, tableControlPolicy, issues, strict);
|
|
1154
|
+
tableChildren.push(...checkStatuses);
|
|
1155
|
+
}
|
|
736
1156
|
return tableChildren;
|
|
737
1157
|
}
|
|
738
1158
|
function collectContractColumnNodes(options) {
|
|
739
|
-
const { contractTable, schemaTable, tableName, namespaceId, tablePath, issues, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType } = options;
|
|
1159
|
+
const { contractTable, schemaTable, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType } = options;
|
|
740
1160
|
const columnNodes = [];
|
|
741
1161
|
for (const [columnName, contractColumn] of Object.entries(contractTable.columns)) {
|
|
742
1162
|
const schemaColumn = schemaTable.columns[columnName];
|
|
743
1163
|
const columnPath = `${tablePath}.columns.${columnName}`;
|
|
744
1164
|
if (!schemaColumn) {
|
|
745
|
-
|
|
1165
|
+
emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
746
1166
|
kind: "missing_column",
|
|
747
1167
|
table: tableName,
|
|
748
1168
|
namespaceId,
|
|
749
1169
|
column: columnName,
|
|
750
1170
|
message: `Column "${tableName}"."${columnName}" is missing from database`
|
|
751
|
-
}
|
|
752
|
-
columnNodes.push({
|
|
1171
|
+
}, {
|
|
753
1172
|
status: "fail",
|
|
754
1173
|
kind: "column",
|
|
755
1174
|
name: `${columnName}: missing`,
|
|
@@ -759,7 +1178,7 @@ function collectContractColumnNodes(options) {
|
|
|
759
1178
|
expected: void 0,
|
|
760
1179
|
actual: void 0,
|
|
761
1180
|
children: []
|
|
762
|
-
});
|
|
1181
|
+
}, issues, columnNodes);
|
|
763
1182
|
continue;
|
|
764
1183
|
}
|
|
765
1184
|
columnNodes.push(verifyColumn({
|
|
@@ -769,6 +1188,7 @@ function collectContractColumnNodes(options) {
|
|
|
769
1188
|
contractColumn,
|
|
770
1189
|
schemaColumn,
|
|
771
1190
|
columnPath,
|
|
1191
|
+
tableControlPolicy,
|
|
772
1192
|
issues,
|
|
773
1193
|
strict,
|
|
774
1194
|
typeMetadataRegistry,
|
|
@@ -781,30 +1201,27 @@ function collectContractColumnNodes(options) {
|
|
|
781
1201
|
return columnNodes;
|
|
782
1202
|
}
|
|
783
1203
|
function appendExtraColumnNodes(options) {
|
|
784
|
-
const { contractTable, schemaTable, tableName, namespaceId, tablePath, issues, columnNodes } = options;
|
|
785
|
-
for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) if (!contractTable.columns[columnName]) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
children: []
|
|
803
|
-
});
|
|
804
|
-
}
|
|
1204
|
+
const { contractTable, schemaTable, tableName, namespaceId, tablePath, tableControlPolicy, issues, columnNodes } = options;
|
|
1205
|
+
for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) if (!contractTable.columns[columnName]) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
|
|
1206
|
+
kind: "extra_column",
|
|
1207
|
+
table: tableName,
|
|
1208
|
+
namespaceId,
|
|
1209
|
+
column: columnName,
|
|
1210
|
+
message: `Extra column "${tableName}"."${columnName}" found in database (not in contract)`
|
|
1211
|
+
}, {
|
|
1212
|
+
status: "fail",
|
|
1213
|
+
kind: "column",
|
|
1214
|
+
name: `${columnName}: extra`,
|
|
1215
|
+
contractPath: `${tablePath}.columns.${columnName}`,
|
|
1216
|
+
code: "extra_column",
|
|
1217
|
+
message: `Extra column "${columnName}" found`,
|
|
1218
|
+
expected: void 0,
|
|
1219
|
+
actual: nativeType,
|
|
1220
|
+
children: []
|
|
1221
|
+
}, issues, columnNodes);
|
|
805
1222
|
}
|
|
806
1223
|
function verifyColumn(options) {
|
|
807
|
-
const { tableName, namespaceId, columnName, contractColumn, schemaColumn, columnPath, issues, strict, codecHooks, storageTypes, normalizeDefault, normalizeNativeType } = options;
|
|
1224
|
+
const { tableName, namespaceId, columnName, contractColumn, schemaColumn, columnPath, tableControlPolicy, issues, strict, codecHooks, storageTypes, normalizeDefault, normalizeNativeType } = options;
|
|
808
1225
|
const columnChildren = [];
|
|
809
1226
|
let columnStatus = "pass";
|
|
810
1227
|
const resolvedContractColumn = resolveContractColumnTypeMetadata(contractColumn, storageTypes, {
|
|
@@ -816,8 +1233,8 @@ function verifyColumn(options) {
|
|
|
816
1233
|
columnName
|
|
817
1234
|
});
|
|
818
1235
|
const schemaNativeType = normalizeNativeType?.(schemaColumn.nativeType) ?? schemaColumn.nativeType;
|
|
819
|
-
if (contractNativeType
|
|
820
|
-
|
|
1236
|
+
if (!(contractNativeType === schemaNativeType)) {
|
|
1237
|
+
const issue = {
|
|
821
1238
|
kind: "type_mismatch",
|
|
822
1239
|
table: tableName,
|
|
823
1240
|
namespaceId,
|
|
@@ -825,19 +1242,23 @@ function verifyColumn(options) {
|
|
|
825
1242
|
expected: contractNativeType,
|
|
826
1243
|
actual: schemaNativeType,
|
|
827
1244
|
message: `Column "${tableName}"."${columnName}" has type mismatch: expected "${contractNativeType}", got "${schemaNativeType}"`
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1245
|
+
};
|
|
1246
|
+
const disposition = verifierDisposition(tableControlPolicy, issue.kind);
|
|
1247
|
+
if (disposition !== "suppress") {
|
|
1248
|
+
issues.push(issue);
|
|
1249
|
+
columnChildren.push({
|
|
1250
|
+
status: disposition,
|
|
1251
|
+
kind: "type",
|
|
1252
|
+
name: "type",
|
|
1253
|
+
contractPath: `${columnPath}.nativeType`,
|
|
1254
|
+
code: "type_mismatch",
|
|
1255
|
+
message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
|
|
1256
|
+
expected: contractNativeType,
|
|
1257
|
+
actual: schemaNativeType,
|
|
1258
|
+
children: []
|
|
1259
|
+
});
|
|
1260
|
+
columnStatus = disposition;
|
|
1261
|
+
}
|
|
841
1262
|
}
|
|
842
1263
|
if (resolvedContractColumn.codecId) {
|
|
843
1264
|
const typeMetadata = options.typeMetadataRegistry.get(resolvedContractColumn.codecId);
|
|
@@ -865,7 +1286,7 @@ function verifyColumn(options) {
|
|
|
865
1286
|
});
|
|
866
1287
|
}
|
|
867
1288
|
if (contractColumn.nullable !== schemaColumn.nullable) {
|
|
868
|
-
|
|
1289
|
+
const issue = {
|
|
869
1290
|
kind: "nullability_mismatch",
|
|
870
1291
|
table: tableName,
|
|
871
1292
|
namespaceId,
|
|
@@ -873,47 +1294,55 @@ function verifyColumn(options) {
|
|
|
873
1294
|
expected: String(contractColumn.nullable),
|
|
874
1295
|
actual: String(schemaColumn.nullable),
|
|
875
1296
|
message: `Column "${tableName}"."${columnName}" has nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1297
|
+
};
|
|
1298
|
+
const disposition = verifierDisposition(tableControlPolicy, issue.kind);
|
|
1299
|
+
if (disposition !== "suppress") {
|
|
1300
|
+
issues.push(issue);
|
|
1301
|
+
columnChildren.push({
|
|
1302
|
+
status: disposition,
|
|
1303
|
+
kind: "nullability",
|
|
1304
|
+
name: "nullability",
|
|
1305
|
+
contractPath: `${columnPath}.nullable`,
|
|
1306
|
+
code: "nullability_mismatch",
|
|
1307
|
+
message: `Nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`,
|
|
1308
|
+
expected: contractColumn.nullable,
|
|
1309
|
+
actual: schemaColumn.nullable,
|
|
1310
|
+
children: []
|
|
1311
|
+
});
|
|
1312
|
+
columnStatus = disposition;
|
|
1313
|
+
}
|
|
889
1314
|
}
|
|
890
1315
|
if (contractColumn.default) {
|
|
891
1316
|
if (!schemaColumn.default) {
|
|
892
1317
|
const defaultDescription = describeColumnDefault(contractColumn.default);
|
|
893
|
-
|
|
1318
|
+
const issue = {
|
|
894
1319
|
kind: "default_missing",
|
|
895
1320
|
table: tableName,
|
|
896
1321
|
namespaceId,
|
|
897
1322
|
column: columnName,
|
|
898
1323
|
expected: defaultDescription,
|
|
899
1324
|
message: `Column "${tableName}"."${columnName}" should have default ${defaultDescription} but database has no default`
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1325
|
+
};
|
|
1326
|
+
const disposition = verifierDisposition(tableControlPolicy, issue.kind);
|
|
1327
|
+
if (disposition !== "suppress") {
|
|
1328
|
+
issues.push(issue);
|
|
1329
|
+
columnChildren.push({
|
|
1330
|
+
status: disposition,
|
|
1331
|
+
kind: "default",
|
|
1332
|
+
name: "default",
|
|
1333
|
+
contractPath: `${columnPath}.default`,
|
|
1334
|
+
code: "default_missing",
|
|
1335
|
+
message: `Default missing: expected ${defaultDescription}`,
|
|
1336
|
+
expected: defaultDescription,
|
|
1337
|
+
actual: void 0,
|
|
1338
|
+
children: []
|
|
1339
|
+
});
|
|
1340
|
+
columnStatus = disposition;
|
|
1341
|
+
}
|
|
913
1342
|
} else if (!columnDefaultsEqual(contractColumn.default, schemaColumn.default, normalizeDefault, schemaNativeType)) {
|
|
914
1343
|
const expectedDescription = describeColumnDefault(contractColumn.default);
|
|
915
1344
|
const actualDescription = schemaColumn.default;
|
|
916
|
-
|
|
1345
|
+
const issue = {
|
|
917
1346
|
kind: "default_mismatch",
|
|
918
1347
|
table: tableName,
|
|
919
1348
|
namespaceId,
|
|
@@ -921,41 +1350,49 @@ function verifyColumn(options) {
|
|
|
921
1350
|
expected: expectedDescription,
|
|
922
1351
|
actual: actualDescription,
|
|
923
1352
|
message: `Column "${tableName}"."${columnName}" has default mismatch: expected ${expectedDescription}, got ${actualDescription}`
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1353
|
+
};
|
|
1354
|
+
const disposition = verifierDisposition(tableControlPolicy, issue.kind);
|
|
1355
|
+
if (disposition !== "suppress") {
|
|
1356
|
+
issues.push(issue);
|
|
1357
|
+
columnChildren.push({
|
|
1358
|
+
status: disposition,
|
|
1359
|
+
kind: "default",
|
|
1360
|
+
name: "default",
|
|
1361
|
+
contractPath: `${columnPath}.default`,
|
|
1362
|
+
code: "default_mismatch",
|
|
1363
|
+
message: `Default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
|
|
1364
|
+
expected: expectedDescription,
|
|
1365
|
+
actual: actualDescription,
|
|
1366
|
+
children: []
|
|
1367
|
+
});
|
|
1368
|
+
columnStatus = disposition;
|
|
1369
|
+
}
|
|
937
1370
|
}
|
|
938
1371
|
} else if (strict && schemaColumn.default) {
|
|
939
|
-
|
|
1372
|
+
const issue = {
|
|
940
1373
|
kind: "extra_default",
|
|
941
1374
|
table: tableName,
|
|
942
1375
|
namespaceId,
|
|
943
1376
|
column: columnName,
|
|
944
1377
|
actual: schemaColumn.default,
|
|
945
1378
|
message: `Column "${tableName}"."${columnName}" has default ${schemaColumn.default} in database but contract specifies no default`
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1379
|
+
};
|
|
1380
|
+
const disposition = verifierDisposition(tableControlPolicy, issue.kind);
|
|
1381
|
+
if (disposition !== "suppress") {
|
|
1382
|
+
issues.push(issue);
|
|
1383
|
+
columnChildren.push({
|
|
1384
|
+
status: disposition,
|
|
1385
|
+
kind: "default",
|
|
1386
|
+
name: "default",
|
|
1387
|
+
contractPath: `${columnPath}.default`,
|
|
1388
|
+
code: "extra_default",
|
|
1389
|
+
message: `Extra default: ${schemaColumn.default}`,
|
|
1390
|
+
expected: void 0,
|
|
1391
|
+
actual: schemaColumn.default,
|
|
1392
|
+
children: []
|
|
1393
|
+
});
|
|
1394
|
+
columnStatus = disposition;
|
|
1395
|
+
}
|
|
959
1396
|
}
|
|
960
1397
|
const aggregated = aggregateChildState(columnChildren, columnStatus);
|
|
961
1398
|
const nullableText = contractColumn.nullable ? "nullable" : "not nullable";
|
|
@@ -1152,6 +1589,6 @@ function formatLiteralValue(value) {
|
|
|
1152
1589
|
return JSON.stringify(value);
|
|
1153
1590
|
}
|
|
1154
1591
|
//#endregion
|
|
1155
|
-
export {
|
|
1592
|
+
export { contractToSchemaIR as a, extractCodecControlHooks as c, isUniqueConstraintSatisfied as i, arraysEqual as n, detectDestructiveChanges as o, isIndexSatisfied as r, resolveValueSetValues as s, verifySqlSchema as t };
|
|
1156
1593
|
|
|
1157
|
-
//# sourceMappingURL=verify-sql-schema-
|
|
1594
|
+
//# sourceMappingURL=verify-sql-schema-DlAgBiT_.mjs.map
|