@prisma-next/family-sql 0.3.0-pr.93.4 → 0.3.0-pr.94.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/control-adapter.d.mts +47 -0
- package/dist/control-adapter.d.mts.map +1 -0
- package/dist/control-adapter.mjs +1 -0
- package/dist/control.d.mts +65 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +136 -0
- package/dist/control.mjs.map +1 -0
- package/dist/instance-B_PdDN4y.mjs +525 -0
- package/dist/instance-B_PdDN4y.mjs.map +1 -0
- package/dist/instance-wm4TrZN0.d.mts +130 -0
- package/dist/instance-wm4TrZN0.d.mts.map +1 -0
- package/dist/runtime.d.mts +70 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +74 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/schema-verify.d.mts +85 -0
- package/dist/schema-verify.d.mts.map +1 -0
- package/dist/schema-verify.mjs +3 -0
- package/dist/test-utils.d.mts +31 -0
- package/dist/test-utils.d.mts.map +1 -0
- package/dist/test-utils.mjs +5 -0
- package/dist/types-D5CPT9N4.d.mts +270 -0
- package/dist/types-D5CPT9N4.d.mts.map +1 -0
- package/dist/verify-DhFytkFC.mjs +108 -0
- package/dist/verify-DhFytkFC.mjs.map +1 -0
- package/dist/verify-sql-schema-BnLVoeWI.mjs +686 -0
- package/dist/verify-sql-schema-BnLVoeWI.mjs.map +1 -0
- package/dist/verify.d.mts +31 -0
- package/dist/verify.d.mts.map +1 -0
- package/dist/verify.mjs +3 -0
- package/package.json +30 -43
- package/dist/chunk-F27CR6XZ.js +0 -589
- package/dist/chunk-F27CR6XZ.js.map +0 -1
- package/dist/chunk-SU7LN2UH.js +0 -96
- package/dist/chunk-SU7LN2UH.js.map +0 -1
- package/dist/chunk-XH2Y5NTD.js +0 -715
- package/dist/chunk-XH2Y5NTD.js.map +0 -1
- package/dist/core/assembly.d.ts +0 -25
- package/dist/core/assembly.d.ts.map +0 -1
- package/dist/core/control-adapter.d.ts +0 -42
- package/dist/core/control-adapter.d.ts.map +0 -1
- package/dist/core/descriptor.d.ts +0 -24
- package/dist/core/descriptor.d.ts.map +0 -1
- package/dist/core/instance.d.ts +0 -140
- package/dist/core/instance.d.ts.map +0 -1
- package/dist/core/migrations/plan-helpers.d.ts +0 -20
- package/dist/core/migrations/plan-helpers.d.ts.map +0 -1
- package/dist/core/migrations/policies.d.ts +0 -6
- package/dist/core/migrations/policies.d.ts.map +0 -1
- package/dist/core/migrations/types.d.ts +0 -280
- package/dist/core/migrations/types.d.ts.map +0 -1
- package/dist/core/runtime-descriptor.d.ts +0 -19
- package/dist/core/runtime-descriptor.d.ts.map +0 -1
- package/dist/core/runtime-instance.d.ts +0 -54
- package/dist/core/runtime-instance.d.ts.map +0 -1
- package/dist/core/schema-verify/verify-helpers.d.ts +0 -96
- package/dist/core/schema-verify/verify-helpers.d.ts.map +0 -1
- package/dist/core/schema-verify/verify-sql-schema.d.ts +0 -45
- package/dist/core/schema-verify/verify-sql-schema.d.ts.map +0 -1
- package/dist/core/verify.d.ts +0 -39
- package/dist/core/verify.d.ts.map +0 -1
- package/dist/exports/control-adapter.d.ts +0 -2
- package/dist/exports/control-adapter.d.ts.map +0 -1
- package/dist/exports/control-adapter.js +0 -1
- package/dist/exports/control-adapter.js.map +0 -1
- package/dist/exports/control.d.ts +0 -13
- package/dist/exports/control.d.ts.map +0 -1
- package/dist/exports/control.js +0 -149
- package/dist/exports/control.js.map +0 -1
- package/dist/exports/runtime.d.ts +0 -8
- package/dist/exports/runtime.d.ts.map +0 -1
- package/dist/exports/runtime.js +0 -64
- package/dist/exports/runtime.js.map +0 -1
- package/dist/exports/schema-verify.d.ts +0 -11
- package/dist/exports/schema-verify.d.ts.map +0 -1
- package/dist/exports/schema-verify.js +0 -15
- package/dist/exports/schema-verify.js.map +0 -1
- package/dist/exports/test-utils.d.ts +0 -7
- package/dist/exports/test-utils.d.ts.map +0 -1
- package/dist/exports/test-utils.js +0 -17
- package/dist/exports/test-utils.js.map +0 -1
- package/dist/exports/verify.d.ts +0 -2
- package/dist/exports/verify.d.ts.map +0 -1
- package/dist/exports/verify.js +0 -11
- package/dist/exports/verify.js.map +0 -1
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
2
|
+
|
|
3
|
+
//#region src/core/schema-verify/verify-helpers.ts
|
|
4
|
+
/**
|
|
5
|
+
* Compares two arrays of strings for equality (order-sensitive).
|
|
6
|
+
*/
|
|
7
|
+
function arraysEqual(a, b) {
|
|
8
|
+
if (a.length !== b.length) return false;
|
|
9
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a unique constraint requirement is satisfied by the given columns.
|
|
14
|
+
*
|
|
15
|
+
* Semantic satisfaction: a unique constraint requirement can be satisfied by:
|
|
16
|
+
* - A unique constraint with the same columns, OR
|
|
17
|
+
* - A unique index with the same columns
|
|
18
|
+
*
|
|
19
|
+
* @param uniques - The unique constraints in the schema table
|
|
20
|
+
* @param indexes - The indexes in the schema table
|
|
21
|
+
* @param columns - The columns required by the unique constraint
|
|
22
|
+
* @returns true if the requirement is satisfied
|
|
23
|
+
*/
|
|
24
|
+
function isUniqueConstraintSatisfied(uniques, indexes, columns) {
|
|
25
|
+
if (uniques.some((unique) => arraysEqual(unique.columns, columns))) return true;
|
|
26
|
+
return indexes.some((index) => index.unique && arraysEqual(index.columns, columns));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Checks if an index requirement is satisfied by the given columns.
|
|
30
|
+
*
|
|
31
|
+
* Semantic satisfaction: a non-unique index requirement can be satisfied by:
|
|
32
|
+
* - Any index (unique or non-unique) with the same columns, OR
|
|
33
|
+
* - A unique constraint with the same columns (stronger satisfies weaker)
|
|
34
|
+
*
|
|
35
|
+
* @param indexes - The indexes in the schema table
|
|
36
|
+
* @param uniques - The unique constraints in the schema table
|
|
37
|
+
* @param columns - The columns required by the index
|
|
38
|
+
* @returns true if the requirement is satisfied
|
|
39
|
+
*/
|
|
40
|
+
function isIndexSatisfied(indexes, uniques, columns) {
|
|
41
|
+
if (indexes.some((index) => arraysEqual(index.columns, columns))) return true;
|
|
42
|
+
return uniques.some((unique) => arraysEqual(unique.columns, columns));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Verifies primary key matches between contract and schema.
|
|
46
|
+
* Returns 'pass' or 'fail'.
|
|
47
|
+
*
|
|
48
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
49
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
50
|
+
*/
|
|
51
|
+
function verifyPrimaryKey(contractPK, schemaPK, tableName, issues) {
|
|
52
|
+
if (!schemaPK) {
|
|
53
|
+
issues.push({
|
|
54
|
+
kind: "primary_key_mismatch",
|
|
55
|
+
table: tableName,
|
|
56
|
+
expected: contractPK.columns.join(", "),
|
|
57
|
+
message: `Table "${tableName}" is missing primary key`
|
|
58
|
+
});
|
|
59
|
+
return "fail";
|
|
60
|
+
}
|
|
61
|
+
if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
|
|
62
|
+
issues.push({
|
|
63
|
+
kind: "primary_key_mismatch",
|
|
64
|
+
table: tableName,
|
|
65
|
+
expected: contractPK.columns.join(", "),
|
|
66
|
+
actual: schemaPK.columns.join(", "),
|
|
67
|
+
message: `Table "${tableName}" has primary key mismatch: expected columns [${contractPK.columns.join(", ")}], got [${schemaPK.columns.join(", ")}]`
|
|
68
|
+
});
|
|
69
|
+
return "fail";
|
|
70
|
+
}
|
|
71
|
+
return "pass";
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Verifies foreign keys match between contract and schema.
|
|
75
|
+
* Returns verification nodes for the tree.
|
|
76
|
+
*
|
|
77
|
+
* Uses semantic satisfaction: identity is based on (table + columns + referenced table + referenced columns).
|
|
78
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
79
|
+
*/
|
|
80
|
+
function verifyForeignKeys(contractFKs, schemaFKs, tableName, tablePath, issues, strict) {
|
|
81
|
+
const nodes = [];
|
|
82
|
+
for (const contractFK of contractFKs) {
|
|
83
|
+
const fkPath = `${tablePath}.foreignKeys[${contractFK.columns.join(",")}]`;
|
|
84
|
+
if (!schemaFKs.find((fk) => {
|
|
85
|
+
return arraysEqual(fk.columns, contractFK.columns) && fk.referencedTable === contractFK.references.table && arraysEqual(fk.referencedColumns, contractFK.references.columns);
|
|
86
|
+
})) {
|
|
87
|
+
issues.push({
|
|
88
|
+
kind: "foreign_key_mismatch",
|
|
89
|
+
table: tableName,
|
|
90
|
+
expected: `${contractFK.columns.join(", ")} -> ${contractFK.references.table}(${contractFK.references.columns.join(", ")})`,
|
|
91
|
+
message: `Table "${tableName}" is missing foreign key: ${contractFK.columns.join(", ")} -> ${contractFK.references.table}(${contractFK.references.columns.join(", ")})`
|
|
92
|
+
});
|
|
93
|
+
nodes.push({
|
|
94
|
+
status: "fail",
|
|
95
|
+
kind: "foreignKey",
|
|
96
|
+
name: `foreignKey(${contractFK.columns.join(", ")})`,
|
|
97
|
+
contractPath: fkPath,
|
|
98
|
+
code: "foreign_key_mismatch",
|
|
99
|
+
message: "Foreign key missing",
|
|
100
|
+
expected: contractFK,
|
|
101
|
+
actual: void 0,
|
|
102
|
+
children: []
|
|
103
|
+
});
|
|
104
|
+
} else nodes.push({
|
|
105
|
+
status: "pass",
|
|
106
|
+
kind: "foreignKey",
|
|
107
|
+
name: `foreignKey(${contractFK.columns.join(", ")})`,
|
|
108
|
+
contractPath: fkPath,
|
|
109
|
+
code: "",
|
|
110
|
+
message: "",
|
|
111
|
+
expected: void 0,
|
|
112
|
+
actual: void 0,
|
|
113
|
+
children: []
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (strict) {
|
|
117
|
+
for (const schemaFK of schemaFKs) if (!contractFKs.find((fk) => {
|
|
118
|
+
return arraysEqual(fk.columns, schemaFK.columns) && fk.references.table === schemaFK.referencedTable && arraysEqual(fk.references.columns, schemaFK.referencedColumns);
|
|
119
|
+
})) {
|
|
120
|
+
issues.push({
|
|
121
|
+
kind: "extra_foreign_key",
|
|
122
|
+
table: tableName,
|
|
123
|
+
message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(", ")} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(", ")})`
|
|
124
|
+
});
|
|
125
|
+
nodes.push({
|
|
126
|
+
status: "fail",
|
|
127
|
+
kind: "foreignKey",
|
|
128
|
+
name: `foreignKey(${schemaFK.columns.join(", ")})`,
|
|
129
|
+
contractPath: `${tablePath}.foreignKeys[${schemaFK.columns.join(",")}]`,
|
|
130
|
+
code: "extra_foreign_key",
|
|
131
|
+
message: "Extra foreign key found",
|
|
132
|
+
expected: void 0,
|
|
133
|
+
actual: schemaFK,
|
|
134
|
+
children: []
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return nodes;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Verifies unique constraints match between contract and schema.
|
|
142
|
+
* Returns verification nodes for the tree.
|
|
143
|
+
*
|
|
144
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
145
|
+
* A unique constraint requirement can be satisfied by either:
|
|
146
|
+
* - A unique constraint with the same columns, or
|
|
147
|
+
* - A unique index with the same columns
|
|
148
|
+
*
|
|
149
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
150
|
+
*/
|
|
151
|
+
function verifyUniqueConstraints(contractUniques, schemaUniques, schemaIndexes, tableName, tablePath, issues, strict) {
|
|
152
|
+
const nodes = [];
|
|
153
|
+
for (const contractUnique of contractUniques) {
|
|
154
|
+
const uniquePath = `${tablePath}.uniques[${contractUnique.columns.join(",")}]`;
|
|
155
|
+
const matchingUnique = schemaUniques.find((u) => arraysEqual(u.columns, contractUnique.columns));
|
|
156
|
+
const matchingUniqueIndex = !matchingUnique && schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
|
|
157
|
+
if (!matchingUnique && !matchingUniqueIndex) {
|
|
158
|
+
issues.push({
|
|
159
|
+
kind: "unique_constraint_mismatch",
|
|
160
|
+
table: tableName,
|
|
161
|
+
expected: contractUnique.columns.join(", "),
|
|
162
|
+
message: `Table "${tableName}" is missing unique constraint: ${contractUnique.columns.join(", ")}`
|
|
163
|
+
});
|
|
164
|
+
nodes.push({
|
|
165
|
+
status: "fail",
|
|
166
|
+
kind: "unique",
|
|
167
|
+
name: `unique(${contractUnique.columns.join(", ")})`,
|
|
168
|
+
contractPath: uniquePath,
|
|
169
|
+
code: "unique_constraint_mismatch",
|
|
170
|
+
message: "Unique constraint missing",
|
|
171
|
+
expected: contractUnique,
|
|
172
|
+
actual: void 0,
|
|
173
|
+
children: []
|
|
174
|
+
});
|
|
175
|
+
} else nodes.push({
|
|
176
|
+
status: "pass",
|
|
177
|
+
kind: "unique",
|
|
178
|
+
name: `unique(${contractUnique.columns.join(", ")})`,
|
|
179
|
+
contractPath: uniquePath,
|
|
180
|
+
code: "",
|
|
181
|
+
message: "",
|
|
182
|
+
expected: void 0,
|
|
183
|
+
actual: void 0,
|
|
184
|
+
children: []
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (strict) {
|
|
188
|
+
for (const schemaUnique of schemaUniques) if (!contractUniques.find((u) => arraysEqual(u.columns, schemaUnique.columns))) {
|
|
189
|
+
issues.push({
|
|
190
|
+
kind: "extra_unique_constraint",
|
|
191
|
+
table: tableName,
|
|
192
|
+
message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(", ")}`
|
|
193
|
+
});
|
|
194
|
+
nodes.push({
|
|
195
|
+
status: "fail",
|
|
196
|
+
kind: "unique",
|
|
197
|
+
name: `unique(${schemaUnique.columns.join(", ")})`,
|
|
198
|
+
contractPath: `${tablePath}.uniques[${schemaUnique.columns.join(",")}]`,
|
|
199
|
+
code: "extra_unique_constraint",
|
|
200
|
+
message: "Extra unique constraint found",
|
|
201
|
+
expected: void 0,
|
|
202
|
+
actual: schemaUnique,
|
|
203
|
+
children: []
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return nodes;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Verifies indexes match between contract and schema.
|
|
211
|
+
* Returns verification nodes for the tree.
|
|
212
|
+
*
|
|
213
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
214
|
+
* A non-unique index requirement can be satisfied by either:
|
|
215
|
+
* - A non-unique index with the same columns, or
|
|
216
|
+
* - A unique index with the same columns (stronger satisfies weaker)
|
|
217
|
+
*
|
|
218
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
219
|
+
*/
|
|
220
|
+
function verifyIndexes(contractIndexes, schemaIndexes, schemaUniques, tableName, tablePath, issues, strict) {
|
|
221
|
+
const nodes = [];
|
|
222
|
+
for (const contractIndex of contractIndexes) {
|
|
223
|
+
const indexPath = `${tablePath}.indexes[${contractIndex.columns.join(",")}]`;
|
|
224
|
+
const matchingIndex = schemaIndexes.find((idx) => arraysEqual(idx.columns, contractIndex.columns));
|
|
225
|
+
const matchingUniqueConstraint = !matchingIndex && schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
|
|
226
|
+
if (!matchingIndex && !matchingUniqueConstraint) {
|
|
227
|
+
issues.push({
|
|
228
|
+
kind: "index_mismatch",
|
|
229
|
+
table: tableName,
|
|
230
|
+
expected: contractIndex.columns.join(", "),
|
|
231
|
+
message: `Table "${tableName}" is missing index: ${contractIndex.columns.join(", ")}`
|
|
232
|
+
});
|
|
233
|
+
nodes.push({
|
|
234
|
+
status: "fail",
|
|
235
|
+
kind: "index",
|
|
236
|
+
name: `index(${contractIndex.columns.join(", ")})`,
|
|
237
|
+
contractPath: indexPath,
|
|
238
|
+
code: "index_mismatch",
|
|
239
|
+
message: "Index missing",
|
|
240
|
+
expected: contractIndex,
|
|
241
|
+
actual: void 0,
|
|
242
|
+
children: []
|
|
243
|
+
});
|
|
244
|
+
} else nodes.push({
|
|
245
|
+
status: "pass",
|
|
246
|
+
kind: "index",
|
|
247
|
+
name: `index(${contractIndex.columns.join(", ")})`,
|
|
248
|
+
contractPath: indexPath,
|
|
249
|
+
code: "",
|
|
250
|
+
message: "",
|
|
251
|
+
expected: void 0,
|
|
252
|
+
actual: void 0,
|
|
253
|
+
children: []
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (strict) for (const schemaIndex of schemaIndexes) {
|
|
257
|
+
if (schemaIndex.unique) continue;
|
|
258
|
+
if (!contractIndexes.find((idx) => arraysEqual(idx.columns, schemaIndex.columns))) {
|
|
259
|
+
issues.push({
|
|
260
|
+
kind: "extra_index",
|
|
261
|
+
table: tableName,
|
|
262
|
+
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(", ")}`
|
|
263
|
+
});
|
|
264
|
+
nodes.push({
|
|
265
|
+
status: "fail",
|
|
266
|
+
kind: "index",
|
|
267
|
+
name: `index(${schemaIndex.columns.join(", ")})`,
|
|
268
|
+
contractPath: `${tablePath}.indexes[${schemaIndex.columns.join(",")}]`,
|
|
269
|
+
code: "extra_index",
|
|
270
|
+
message: "Extra index found",
|
|
271
|
+
expected: void 0,
|
|
272
|
+
actual: schemaIndex,
|
|
273
|
+
children: []
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return nodes;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Verifies database dependencies are installed using component-owned verification hooks.
|
|
281
|
+
* Each dependency provides a pure verifyDatabaseDependencyInstalled function that checks
|
|
282
|
+
* whether the dependency is satisfied based on the in-memory schema IR (no DB I/O).
|
|
283
|
+
*
|
|
284
|
+
* Returns verification nodes for the tree.
|
|
285
|
+
*/
|
|
286
|
+
function verifyDatabaseDependencies(dependencies, schema, issues) {
|
|
287
|
+
const nodes = [];
|
|
288
|
+
for (const dependency of dependencies) {
|
|
289
|
+
const depIssues = dependency.verifyDatabaseDependencyInstalled(schema);
|
|
290
|
+
const depPath = `dependencies.${dependency.id}`;
|
|
291
|
+
if (depIssues.length > 0) {
|
|
292
|
+
issues.push(...depIssues);
|
|
293
|
+
const issuesMessage = depIssues.map((i) => i.message).join("; ");
|
|
294
|
+
const nodeMessage = issuesMessage ? `${dependency.id}: ${issuesMessage}` : dependency.id;
|
|
295
|
+
nodes.push({
|
|
296
|
+
status: "fail",
|
|
297
|
+
kind: "databaseDependency",
|
|
298
|
+
name: dependency.label,
|
|
299
|
+
contractPath: depPath,
|
|
300
|
+
code: "dependency_missing",
|
|
301
|
+
message: nodeMessage,
|
|
302
|
+
expected: void 0,
|
|
303
|
+
actual: void 0,
|
|
304
|
+
children: []
|
|
305
|
+
});
|
|
306
|
+
} else nodes.push({
|
|
307
|
+
status: "pass",
|
|
308
|
+
kind: "databaseDependency",
|
|
309
|
+
name: dependency.label,
|
|
310
|
+
contractPath: depPath,
|
|
311
|
+
code: "",
|
|
312
|
+
message: "",
|
|
313
|
+
expected: void 0,
|
|
314
|
+
actual: void 0,
|
|
315
|
+
children: []
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return nodes;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Computes counts of pass/warn/fail nodes by traversing the tree.
|
|
322
|
+
*/
|
|
323
|
+
function computeCounts(node) {
|
|
324
|
+
let pass = 0;
|
|
325
|
+
let warn = 0;
|
|
326
|
+
let fail = 0;
|
|
327
|
+
function traverse(n) {
|
|
328
|
+
if (n.status === "pass") pass++;
|
|
329
|
+
else if (n.status === "warn") warn++;
|
|
330
|
+
else if (n.status === "fail") fail++;
|
|
331
|
+
if (n.children) for (const child of n.children) traverse(child);
|
|
332
|
+
}
|
|
333
|
+
traverse(node);
|
|
334
|
+
return {
|
|
335
|
+
pass,
|
|
336
|
+
warn,
|
|
337
|
+
fail,
|
|
338
|
+
totalNodes: pass + warn + fail
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/core/schema-verify/verify-sql-schema.ts
|
|
344
|
+
/**
|
|
345
|
+
* Verifies that a SqlSchemaIR matches a SqlContract.
|
|
346
|
+
*
|
|
347
|
+
* This is a pure function that does NOT perform any database I/O.
|
|
348
|
+
* It takes an already-introspected schema IR and compares it against
|
|
349
|
+
* the contract requirements.
|
|
350
|
+
*
|
|
351
|
+
* @param options - Verification options
|
|
352
|
+
* @returns VerifyDatabaseSchemaResult with verification tree and issues
|
|
353
|
+
*/
|
|
354
|
+
function verifySqlSchema(options) {
|
|
355
|
+
const { contract, schema, strict, context, typeMetadataRegistry } = options;
|
|
356
|
+
const startTime = Date.now();
|
|
357
|
+
const contractCoreHash = contract.coreHash;
|
|
358
|
+
const contractProfileHash = "profileHash" in contract && typeof contract.profileHash === "string" ? contract.profileHash : void 0;
|
|
359
|
+
const contractTarget = contract.target;
|
|
360
|
+
const issues = [];
|
|
361
|
+
const rootChildren = [];
|
|
362
|
+
const contractTables = contract.storage.tables;
|
|
363
|
+
const schemaTables = schema.tables;
|
|
364
|
+
for (const [tableName, contractTable] of Object.entries(contractTables)) {
|
|
365
|
+
const schemaTable = schemaTables[tableName];
|
|
366
|
+
const tablePath = `storage.tables.${tableName}`;
|
|
367
|
+
if (!schemaTable) {
|
|
368
|
+
issues.push({
|
|
369
|
+
kind: "missing_table",
|
|
370
|
+
table: tableName,
|
|
371
|
+
message: `Table "${tableName}" is missing from database`
|
|
372
|
+
});
|
|
373
|
+
rootChildren.push({
|
|
374
|
+
status: "fail",
|
|
375
|
+
kind: "table",
|
|
376
|
+
name: `table ${tableName}`,
|
|
377
|
+
contractPath: tablePath,
|
|
378
|
+
code: "missing_table",
|
|
379
|
+
message: `Table "${tableName}" is missing`,
|
|
380
|
+
expected: void 0,
|
|
381
|
+
actual: void 0,
|
|
382
|
+
children: []
|
|
383
|
+
});
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const tableChildren = [];
|
|
387
|
+
const columnNodes = [];
|
|
388
|
+
for (const [columnName, contractColumn] of Object.entries(contractTable.columns)) {
|
|
389
|
+
const schemaColumn = schemaTable.columns[columnName];
|
|
390
|
+
const columnPath = `${tablePath}.columns.${columnName}`;
|
|
391
|
+
if (!schemaColumn) {
|
|
392
|
+
issues.push({
|
|
393
|
+
kind: "missing_column",
|
|
394
|
+
table: tableName,
|
|
395
|
+
column: columnName,
|
|
396
|
+
message: `Column "${tableName}"."${columnName}" is missing from database`
|
|
397
|
+
});
|
|
398
|
+
columnNodes.push({
|
|
399
|
+
status: "fail",
|
|
400
|
+
kind: "column",
|
|
401
|
+
name: `${columnName}: missing`,
|
|
402
|
+
contractPath: columnPath,
|
|
403
|
+
code: "missing_column",
|
|
404
|
+
message: `Column "${columnName}" is missing`,
|
|
405
|
+
expected: void 0,
|
|
406
|
+
actual: void 0,
|
|
407
|
+
children: []
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const columnChildren = [];
|
|
412
|
+
let columnStatus = "pass";
|
|
413
|
+
const contractNativeType = contractColumn.nativeType;
|
|
414
|
+
const schemaNativeType = schemaColumn.nativeType;
|
|
415
|
+
if (contractNativeType !== schemaNativeType) {
|
|
416
|
+
issues.push({
|
|
417
|
+
kind: "type_mismatch",
|
|
418
|
+
table: tableName,
|
|
419
|
+
column: columnName,
|
|
420
|
+
expected: contractNativeType,
|
|
421
|
+
actual: schemaNativeType,
|
|
422
|
+
message: `Column "${tableName}"."${columnName}" has type mismatch: expected "${contractNativeType}", got "${schemaNativeType}"`
|
|
423
|
+
});
|
|
424
|
+
columnChildren.push({
|
|
425
|
+
status: "fail",
|
|
426
|
+
kind: "type",
|
|
427
|
+
name: "type",
|
|
428
|
+
contractPath: `${columnPath}.nativeType`,
|
|
429
|
+
code: "type_mismatch",
|
|
430
|
+
message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
|
|
431
|
+
expected: contractNativeType,
|
|
432
|
+
actual: schemaNativeType,
|
|
433
|
+
children: []
|
|
434
|
+
});
|
|
435
|
+
columnStatus = "fail";
|
|
436
|
+
}
|
|
437
|
+
if (contractColumn.codecId) {
|
|
438
|
+
const typeMetadata = typeMetadataRegistry.get(contractColumn.codecId);
|
|
439
|
+
if (!typeMetadata) columnChildren.push({
|
|
440
|
+
status: "warn",
|
|
441
|
+
kind: "type",
|
|
442
|
+
name: "type_metadata_missing",
|
|
443
|
+
contractPath: `${columnPath}.codecId`,
|
|
444
|
+
code: "type_metadata_missing",
|
|
445
|
+
message: `codecId "${contractColumn.codecId}" not found in type metadata registry`,
|
|
446
|
+
expected: contractColumn.codecId,
|
|
447
|
+
actual: void 0,
|
|
448
|
+
children: []
|
|
449
|
+
});
|
|
450
|
+
else if (typeMetadata.nativeType && typeMetadata.nativeType !== contractNativeType) columnChildren.push({
|
|
451
|
+
status: "warn",
|
|
452
|
+
kind: "type",
|
|
453
|
+
name: "type_consistency",
|
|
454
|
+
contractPath: `${columnPath}.codecId`,
|
|
455
|
+
code: "type_consistency_warning",
|
|
456
|
+
message: `codecId "${contractColumn.codecId}" maps to nativeType "${typeMetadata.nativeType}" in registry, but contract has "${contractNativeType}"`,
|
|
457
|
+
expected: typeMetadata.nativeType,
|
|
458
|
+
actual: contractNativeType,
|
|
459
|
+
children: []
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
if (contractColumn.nullable !== schemaColumn.nullable) {
|
|
463
|
+
issues.push({
|
|
464
|
+
kind: "nullability_mismatch",
|
|
465
|
+
table: tableName,
|
|
466
|
+
column: columnName,
|
|
467
|
+
expected: String(contractColumn.nullable),
|
|
468
|
+
actual: String(schemaColumn.nullable),
|
|
469
|
+
message: `Column "${tableName}"."${columnName}" has nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`
|
|
470
|
+
});
|
|
471
|
+
columnChildren.push({
|
|
472
|
+
status: "fail",
|
|
473
|
+
kind: "nullability",
|
|
474
|
+
name: "nullability",
|
|
475
|
+
contractPath: `${columnPath}.nullable`,
|
|
476
|
+
code: "nullability_mismatch",
|
|
477
|
+
message: `Nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`,
|
|
478
|
+
expected: contractColumn.nullable,
|
|
479
|
+
actual: schemaColumn.nullable,
|
|
480
|
+
children: []
|
|
481
|
+
});
|
|
482
|
+
columnStatus = "fail";
|
|
483
|
+
}
|
|
484
|
+
const computedColumnStatus = columnChildren.some((c) => c.status === "fail") ? "fail" : columnChildren.some((c) => c.status === "warn") ? "warn" : "pass";
|
|
485
|
+
const finalColumnStatus = columnChildren.length > 0 ? computedColumnStatus : columnStatus;
|
|
486
|
+
const nullableText = contractColumn.nullable ? "nullable" : "not nullable";
|
|
487
|
+
const columnTypeDisplay = contractColumn.codecId ? `${contractNativeType} (${contractColumn.codecId})` : contractNativeType;
|
|
488
|
+
const failureMessages = columnChildren.filter((child) => child.status === "fail" && child.message).map((child) => child.message).filter((msg) => typeof msg === "string" && msg.length > 0);
|
|
489
|
+
const columnMessage = finalColumnStatus === "fail" && failureMessages.length > 0 ? failureMessages.join("; ") : "";
|
|
490
|
+
const columnCode = (finalColumnStatus === "fail" || finalColumnStatus === "warn") && columnChildren[0] ? columnChildren[0].code : "";
|
|
491
|
+
columnNodes.push({
|
|
492
|
+
status: finalColumnStatus,
|
|
493
|
+
kind: "column",
|
|
494
|
+
name: `${columnName}: ${columnTypeDisplay} (${nullableText})`,
|
|
495
|
+
contractPath: columnPath,
|
|
496
|
+
code: columnCode,
|
|
497
|
+
message: columnMessage,
|
|
498
|
+
expected: void 0,
|
|
499
|
+
actual: void 0,
|
|
500
|
+
children: columnChildren
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (columnNodes.length > 0) {
|
|
504
|
+
const columnsStatus = columnNodes.some((c) => c.status === "fail") ? "fail" : columnNodes.some((c) => c.status === "warn") ? "warn" : "pass";
|
|
505
|
+
tableChildren.push({
|
|
506
|
+
status: columnsStatus,
|
|
507
|
+
kind: "columns",
|
|
508
|
+
name: "columns",
|
|
509
|
+
contractPath: `${tablePath}.columns`,
|
|
510
|
+
code: "",
|
|
511
|
+
message: "",
|
|
512
|
+
expected: void 0,
|
|
513
|
+
actual: void 0,
|
|
514
|
+
children: columnNodes
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (strict) {
|
|
518
|
+
for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) if (!contractTable.columns[columnName]) {
|
|
519
|
+
issues.push({
|
|
520
|
+
kind: "extra_column",
|
|
521
|
+
table: tableName,
|
|
522
|
+
column: columnName,
|
|
523
|
+
message: `Extra column "${tableName}"."${columnName}" found in database (not in contract)`
|
|
524
|
+
});
|
|
525
|
+
columnNodes.push({
|
|
526
|
+
status: "fail",
|
|
527
|
+
kind: "column",
|
|
528
|
+
name: `${columnName}: extra`,
|
|
529
|
+
contractPath: `${tablePath}.columns.${columnName}`,
|
|
530
|
+
code: "extra_column",
|
|
531
|
+
message: `Extra column "${columnName}" found`,
|
|
532
|
+
expected: void 0,
|
|
533
|
+
actual: nativeType,
|
|
534
|
+
children: []
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (contractTable.primaryKey) if (verifyPrimaryKey(contractTable.primaryKey, schemaTable.primaryKey, tableName, issues) === "fail") tableChildren.push({
|
|
539
|
+
status: "fail",
|
|
540
|
+
kind: "primaryKey",
|
|
541
|
+
name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
|
|
542
|
+
contractPath: `${tablePath}.primaryKey`,
|
|
543
|
+
code: "primary_key_mismatch",
|
|
544
|
+
message: "Primary key mismatch",
|
|
545
|
+
expected: contractTable.primaryKey,
|
|
546
|
+
actual: schemaTable.primaryKey,
|
|
547
|
+
children: []
|
|
548
|
+
});
|
|
549
|
+
else tableChildren.push({
|
|
550
|
+
status: "pass",
|
|
551
|
+
kind: "primaryKey",
|
|
552
|
+
name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
|
|
553
|
+
contractPath: `${tablePath}.primaryKey`,
|
|
554
|
+
code: "",
|
|
555
|
+
message: "",
|
|
556
|
+
expected: void 0,
|
|
557
|
+
actual: void 0,
|
|
558
|
+
children: []
|
|
559
|
+
});
|
|
560
|
+
else if (schemaTable.primaryKey && strict) {
|
|
561
|
+
issues.push({
|
|
562
|
+
kind: "extra_primary_key",
|
|
563
|
+
table: tableName,
|
|
564
|
+
message: "Extra primary key found in database (not in contract)"
|
|
565
|
+
});
|
|
566
|
+
tableChildren.push({
|
|
567
|
+
status: "fail",
|
|
568
|
+
kind: "primaryKey",
|
|
569
|
+
name: `primary key: ${schemaTable.primaryKey.columns.join(", ")}`,
|
|
570
|
+
contractPath: `${tablePath}.primaryKey`,
|
|
571
|
+
code: "extra_primary_key",
|
|
572
|
+
message: "Extra primary key found",
|
|
573
|
+
expected: void 0,
|
|
574
|
+
actual: schemaTable.primaryKey,
|
|
575
|
+
children: []
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
const fkStatuses = verifyForeignKeys(contractTable.foreignKeys, schemaTable.foreignKeys, tableName, tablePath, issues, strict);
|
|
579
|
+
tableChildren.push(...fkStatuses);
|
|
580
|
+
const uniqueStatuses = verifyUniqueConstraints(contractTable.uniques, schemaTable.uniques, schemaTable.indexes, tableName, tablePath, issues, strict);
|
|
581
|
+
tableChildren.push(...uniqueStatuses);
|
|
582
|
+
const indexStatuses = verifyIndexes(contractTable.indexes, schemaTable.indexes, schemaTable.uniques, tableName, tablePath, issues, strict);
|
|
583
|
+
tableChildren.push(...indexStatuses);
|
|
584
|
+
const tableStatus = tableChildren.some((c) => c.status === "fail") ? "fail" : tableChildren.some((c) => c.status === "warn") ? "warn" : "pass";
|
|
585
|
+
const tableFailureMessages = tableChildren.filter((child) => child.status === "fail" && child.message).map((child) => child.message).filter((msg) => typeof msg === "string" && msg.length > 0);
|
|
586
|
+
const tableMessage = tableStatus === "fail" && tableFailureMessages.length > 0 ? `${tableFailureMessages.length} issue${tableFailureMessages.length === 1 ? "" : "s"}` : "";
|
|
587
|
+
const tableCode = tableStatus === "fail" && tableChildren.length > 0 && tableChildren[0] ? tableChildren[0].code : "";
|
|
588
|
+
rootChildren.push({
|
|
589
|
+
status: tableStatus,
|
|
590
|
+
kind: "table",
|
|
591
|
+
name: `table ${tableName}`,
|
|
592
|
+
contractPath: tablePath,
|
|
593
|
+
code: tableCode,
|
|
594
|
+
message: tableMessage,
|
|
595
|
+
expected: void 0,
|
|
596
|
+
actual: void 0,
|
|
597
|
+
children: tableChildren
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (strict) {
|
|
601
|
+
for (const tableName of Object.keys(schemaTables)) if (!contractTables[tableName]) {
|
|
602
|
+
issues.push({
|
|
603
|
+
kind: "extra_table",
|
|
604
|
+
table: tableName,
|
|
605
|
+
message: `Extra table "${tableName}" found in database (not in contract)`
|
|
606
|
+
});
|
|
607
|
+
rootChildren.push({
|
|
608
|
+
status: "fail",
|
|
609
|
+
kind: "table",
|
|
610
|
+
name: `table ${tableName}`,
|
|
611
|
+
contractPath: `storage.tables.${tableName}`,
|
|
612
|
+
code: "extra_table",
|
|
613
|
+
message: `Extra table "${tableName}" found`,
|
|
614
|
+
expected: void 0,
|
|
615
|
+
actual: void 0,
|
|
616
|
+
children: []
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const contractExtensionPacks = contract.extensionPacks ?? {};
|
|
621
|
+
for (const extensionNamespace of Object.keys(contractExtensionPacks)) if (!options.frameworkComponents.some((component) => component.id === extensionNamespace && (component.kind === "extension" || component.kind === "adapter" || component.kind === "target"))) throw new Error(`Extension pack '${extensionNamespace}' is declared in the contract but not found in framework components. This indicates a configuration mismatch - the contract was emitted with this extension pack, but it is not provided in the current configuration.`);
|
|
622
|
+
const dependencyStatuses = verifyDatabaseDependencies(collectDependenciesFromFrameworkComponents(options.frameworkComponents), schema, issues);
|
|
623
|
+
rootChildren.push(...dependencyStatuses);
|
|
624
|
+
const root = {
|
|
625
|
+
status: rootChildren.some((c) => c.status === "fail") ? "fail" : rootChildren.some((c) => c.status === "warn") ? "warn" : "pass",
|
|
626
|
+
kind: "contract",
|
|
627
|
+
name: "contract",
|
|
628
|
+
contractPath: "",
|
|
629
|
+
code: "",
|
|
630
|
+
message: "",
|
|
631
|
+
expected: void 0,
|
|
632
|
+
actual: void 0,
|
|
633
|
+
children: rootChildren
|
|
634
|
+
};
|
|
635
|
+
const counts = computeCounts(root);
|
|
636
|
+
const ok = counts.fail === 0;
|
|
637
|
+
const code = ok ? void 0 : "PN-SCHEMA-0001";
|
|
638
|
+
const summary = ok ? "Database schema satisfies contract" : `Database schema does not satisfy contract (${counts.fail} failure${counts.fail === 1 ? "" : "s"})`;
|
|
639
|
+
const totalTime = Date.now() - startTime;
|
|
640
|
+
return {
|
|
641
|
+
ok,
|
|
642
|
+
...ifDefined("code", code),
|
|
643
|
+
summary,
|
|
644
|
+
contract: {
|
|
645
|
+
coreHash: contractCoreHash,
|
|
646
|
+
...ifDefined("profileHash", contractProfileHash)
|
|
647
|
+
},
|
|
648
|
+
target: {
|
|
649
|
+
expected: contractTarget,
|
|
650
|
+
actual: contractTarget
|
|
651
|
+
},
|
|
652
|
+
schema: {
|
|
653
|
+
issues,
|
|
654
|
+
root,
|
|
655
|
+
counts
|
|
656
|
+
},
|
|
657
|
+
meta: {
|
|
658
|
+
strict,
|
|
659
|
+
...ifDefined("contractPath", context?.contractPath),
|
|
660
|
+
...ifDefined("configPath", context?.configPath)
|
|
661
|
+
},
|
|
662
|
+
timings: { total: totalTime }
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Type predicate to check if a component has database dependencies with an init array.
|
|
667
|
+
* The familyId check is redundant since TargetBoundComponentDescriptor<'sql', T> already
|
|
668
|
+
* guarantees familyId is 'sql' at the type level, so we don't need runtime checks for it.
|
|
669
|
+
*/
|
|
670
|
+
function hasDatabaseDependenciesInit(component) {
|
|
671
|
+
if (!("databaseDependencies" in component)) return false;
|
|
672
|
+
const dbDeps = component["databaseDependencies"];
|
|
673
|
+
if (dbDeps === void 0 || dbDeps === null || typeof dbDeps !== "object") return false;
|
|
674
|
+
const init = dbDeps["init"];
|
|
675
|
+
if (init === void 0 || !Array.isArray(init)) return false;
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
function collectDependenciesFromFrameworkComponents(components) {
|
|
679
|
+
const dependencies = [];
|
|
680
|
+
for (const component of components) if (hasDatabaseDependenciesInit(component)) dependencies.push(...component.databaseDependencies.init);
|
|
681
|
+
return dependencies;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
//#endregion
|
|
685
|
+
export { verifyDatabaseDependencies as a, isUniqueConstraintSatisfied as i, arraysEqual as n, isIndexSatisfied as r, verifySqlSchema as t };
|
|
686
|
+
//# sourceMappingURL=verify-sql-schema-BnLVoeWI.mjs.map
|