@prisma-next/family-sql 0.3.0-dev.5 → 0.3.0-dev.52
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/README.md +12 -6
- package/dist/assembly-BVS641kd.mjs +106 -0
- package/dist/assembly-BVS641kd.mjs.map +1 -0
- package/dist/control-adapter.d.mts +60 -0
- package/dist/control-adapter.d.mts.map +1 -0
- package/dist/control-adapter.mjs +1 -0
- package/dist/control-instance-CWKSpACr.d.mts +292 -0
- package/dist/control-instance-CWKSpACr.d.mts.map +1 -0
- package/dist/control.d.mts +64 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +536 -0
- package/dist/control.mjs.map +1 -0
- package/dist/runtime.d.mts +27 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +38 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/schema-verify.d.mts +48 -0
- package/dist/schema-verify.d.mts.map +1 -0
- package/dist/schema-verify.mjs +4 -0
- package/dist/test-utils.d.mts +2 -0
- package/dist/test-utils.mjs +3 -0
- package/dist/verify-BfMETJcM.mjs +108 -0
- package/dist/verify-BfMETJcM.mjs.map +1 -0
- package/dist/verify-sql-schema-CpAVEi8A.mjs +1058 -0
- package/dist/verify-sql-schema-CpAVEi8A.mjs.map +1 -0
- package/dist/verify-sql-schema-DhHnkpPa.d.mts +67 -0
- package/dist/verify-sql-schema-DhHnkpPa.d.mts.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 +36 -47
- package/src/core/assembly.ts +158 -59
- package/src/core/control-adapter.ts +15 -0
- package/src/core/control-descriptor.ts +37 -0
- package/src/core/{instance.ts → control-instance.ts} +108 -241
- package/src/core/migrations/types.ts +62 -163
- package/src/core/runtime-descriptor.ts +19 -41
- package/src/core/runtime-instance.ts +11 -133
- package/src/core/schema-verify/verify-helpers.ts +187 -97
- package/src/core/schema-verify/verify-sql-schema.ts +910 -392
- package/src/core/verify.ts +4 -13
- package/src/exports/control.ts +9 -6
- package/src/exports/runtime.ts +2 -6
- package/src/exports/schema-verify.ts +10 -2
- package/src/exports/test-utils.ts +0 -1
- package/dist/chunk-6K3RPBDP.js +0 -580
- package/dist/chunk-6K3RPBDP.js.map +0 -1
- package/dist/chunk-BHEGVBY7.js +0 -772
- package/dist/chunk-BHEGVBY7.js.map +0 -1
- package/dist/chunk-SU7LN2UH.js +0 -96
- package/dist/chunk-SU7LN2UH.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 -31
- package/dist/core/descriptor.d.ts.map +0 -1
- package/dist/core/instance.d.ts +0 -142
- 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 -50
- 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 -11
- 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
- package/src/core/descriptor.ts +0 -37
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import { n as extractCodecControlHooks } from "./assembly-BVS641kd.mjs";
|
|
2
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
3
|
+
import { isTaggedBigInt } from "@prisma-next/contract/types";
|
|
4
|
+
|
|
5
|
+
//#region src/core/schema-verify/verify-helpers.ts
|
|
6
|
+
/**
|
|
7
|
+
* Compares two arrays of strings for equality (order-sensitive).
|
|
8
|
+
*/
|
|
9
|
+
function arraysEqual(a, b) {
|
|
10
|
+
if (a.length !== b.length) return false;
|
|
11
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a unique constraint requirement is satisfied by the given columns.
|
|
16
|
+
*
|
|
17
|
+
* Semantic satisfaction: a unique constraint requirement can be satisfied by:
|
|
18
|
+
* - A unique constraint with the same columns, OR
|
|
19
|
+
* - A unique index with the same columns
|
|
20
|
+
*
|
|
21
|
+
* @param uniques - The unique constraints in the schema table
|
|
22
|
+
* @param indexes - The indexes in the schema table
|
|
23
|
+
* @param columns - The columns required by the unique constraint
|
|
24
|
+
* @returns true if the requirement is satisfied
|
|
25
|
+
*/
|
|
26
|
+
function isUniqueConstraintSatisfied(uniques, indexes, columns) {
|
|
27
|
+
if (uniques.some((unique) => arraysEqual(unique.columns, columns))) return true;
|
|
28
|
+
return indexes.some((index) => index.unique && arraysEqual(index.columns, columns));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Checks if an index requirement is satisfied by the given columns.
|
|
32
|
+
*
|
|
33
|
+
* Semantic satisfaction: a non-unique index requirement can be satisfied by:
|
|
34
|
+
* - Any index (unique or non-unique) with the same columns, OR
|
|
35
|
+
* - A unique constraint with the same columns (stronger satisfies weaker)
|
|
36
|
+
*
|
|
37
|
+
* @param indexes - The indexes in the schema table
|
|
38
|
+
* @param uniques - The unique constraints in the schema table
|
|
39
|
+
* @param columns - The columns required by the index
|
|
40
|
+
* @returns true if the requirement is satisfied
|
|
41
|
+
*/
|
|
42
|
+
function isIndexSatisfied(indexes, uniques, columns) {
|
|
43
|
+
if (indexes.some((index) => arraysEqual(index.columns, columns))) return true;
|
|
44
|
+
return uniques.some((unique) => arraysEqual(unique.columns, columns));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verifies primary key matches between contract and schema.
|
|
48
|
+
* Returns 'pass' or 'fail'.
|
|
49
|
+
*
|
|
50
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
51
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
52
|
+
*/
|
|
53
|
+
function verifyPrimaryKey(contractPK, schemaPK, tableName, issues) {
|
|
54
|
+
if (!schemaPK) {
|
|
55
|
+
issues.push({
|
|
56
|
+
kind: "primary_key_mismatch",
|
|
57
|
+
table: tableName,
|
|
58
|
+
expected: contractPK.columns.join(", "),
|
|
59
|
+
message: `Table "${tableName}" is missing primary key`
|
|
60
|
+
});
|
|
61
|
+
return "fail";
|
|
62
|
+
}
|
|
63
|
+
if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
|
|
64
|
+
issues.push({
|
|
65
|
+
kind: "primary_key_mismatch",
|
|
66
|
+
table: tableName,
|
|
67
|
+
expected: contractPK.columns.join(", "),
|
|
68
|
+
actual: schemaPK.columns.join(", "),
|
|
69
|
+
message: `Table "${tableName}" has primary key mismatch: expected columns [${contractPK.columns.join(", ")}], got [${schemaPK.columns.join(", ")}]`
|
|
70
|
+
});
|
|
71
|
+
return "fail";
|
|
72
|
+
}
|
|
73
|
+
return "pass";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Verifies foreign keys match between contract and schema.
|
|
77
|
+
* Returns verification nodes for the tree.
|
|
78
|
+
*
|
|
79
|
+
* Uses semantic satisfaction: identity is based on (table + columns + referenced table + referenced columns).
|
|
80
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
81
|
+
*/
|
|
82
|
+
function verifyForeignKeys(contractFKs, schemaFKs, tableName, tablePath, issues, strict) {
|
|
83
|
+
const nodes = [];
|
|
84
|
+
for (const contractFK of contractFKs) {
|
|
85
|
+
const fkPath = `${tablePath}.foreignKeys[${contractFK.columns.join(",")}]`;
|
|
86
|
+
const matchingFK = schemaFKs.find((fk) => {
|
|
87
|
+
return arraysEqual(fk.columns, contractFK.columns) && fk.referencedTable === contractFK.references.table && arraysEqual(fk.referencedColumns, contractFK.references.columns);
|
|
88
|
+
});
|
|
89
|
+
if (!matchingFK) {
|
|
90
|
+
issues.push({
|
|
91
|
+
kind: "foreign_key_mismatch",
|
|
92
|
+
table: tableName,
|
|
93
|
+
expected: `${contractFK.columns.join(", ")} -> ${contractFK.references.table}(${contractFK.references.columns.join(", ")})`,
|
|
94
|
+
message: `Table "${tableName}" is missing foreign key: ${contractFK.columns.join(", ")} -> ${contractFK.references.table}(${contractFK.references.columns.join(", ")})`
|
|
95
|
+
});
|
|
96
|
+
nodes.push({
|
|
97
|
+
status: "fail",
|
|
98
|
+
kind: "foreignKey",
|
|
99
|
+
name: `foreignKey(${contractFK.columns.join(", ")})`,
|
|
100
|
+
contractPath: fkPath,
|
|
101
|
+
code: "foreign_key_mismatch",
|
|
102
|
+
message: "Foreign key missing",
|
|
103
|
+
expected: contractFK,
|
|
104
|
+
actual: void 0,
|
|
105
|
+
children: []
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
const actionMismatches = getReferentialActionMismatches(contractFK, matchingFK);
|
|
109
|
+
if (actionMismatches.length > 0) {
|
|
110
|
+
const combinedMessage = actionMismatches.map((m) => m.message).join("; ");
|
|
111
|
+
const combinedExpected = actionMismatches.map((m) => m.expected).join(", ");
|
|
112
|
+
const combinedActual = actionMismatches.map((m) => m.actual).join(", ");
|
|
113
|
+
issues.push({
|
|
114
|
+
kind: "foreign_key_mismatch",
|
|
115
|
+
table: tableName,
|
|
116
|
+
indexOrConstraint: matchingFK.name ?? `fk(${contractFK.columns.join(",")})`,
|
|
117
|
+
expected: combinedExpected,
|
|
118
|
+
actual: combinedActual,
|
|
119
|
+
message: `Table "${tableName}" foreign key ${contractFK.columns.join(", ")} -> ${contractFK.references.table}: ${combinedMessage}`
|
|
120
|
+
});
|
|
121
|
+
nodes.push({
|
|
122
|
+
status: "fail",
|
|
123
|
+
kind: "foreignKey",
|
|
124
|
+
name: `foreignKey(${contractFK.columns.join(", ")})`,
|
|
125
|
+
contractPath: fkPath,
|
|
126
|
+
code: "foreign_key_mismatch",
|
|
127
|
+
message: combinedMessage,
|
|
128
|
+
expected: contractFK,
|
|
129
|
+
actual: matchingFK,
|
|
130
|
+
children: []
|
|
131
|
+
});
|
|
132
|
+
} else nodes.push({
|
|
133
|
+
status: "pass",
|
|
134
|
+
kind: "foreignKey",
|
|
135
|
+
name: `foreignKey(${contractFK.columns.join(", ")})`,
|
|
136
|
+
contractPath: fkPath,
|
|
137
|
+
code: "",
|
|
138
|
+
message: "",
|
|
139
|
+
expected: void 0,
|
|
140
|
+
actual: void 0,
|
|
141
|
+
children: []
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (strict) {
|
|
146
|
+
for (const schemaFK of schemaFKs) if (!contractFKs.find((fk) => {
|
|
147
|
+
return arraysEqual(fk.columns, schemaFK.columns) && fk.references.table === schemaFK.referencedTable && arraysEqual(fk.references.columns, schemaFK.referencedColumns);
|
|
148
|
+
})) {
|
|
149
|
+
issues.push({
|
|
150
|
+
kind: "extra_foreign_key",
|
|
151
|
+
table: tableName,
|
|
152
|
+
message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(", ")} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(", ")})`
|
|
153
|
+
});
|
|
154
|
+
nodes.push({
|
|
155
|
+
status: "fail",
|
|
156
|
+
kind: "foreignKey",
|
|
157
|
+
name: `foreignKey(${schemaFK.columns.join(", ")})`,
|
|
158
|
+
contractPath: `${tablePath}.foreignKeys[${schemaFK.columns.join(",")}]`,
|
|
159
|
+
code: "extra_foreign_key",
|
|
160
|
+
message: "Extra foreign key found",
|
|
161
|
+
expected: void 0,
|
|
162
|
+
actual: schemaFK,
|
|
163
|
+
children: []
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return nodes;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Verifies unique constraints match between contract and schema.
|
|
171
|
+
* Returns verification nodes for the tree.
|
|
172
|
+
*
|
|
173
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
174
|
+
* A unique constraint requirement can be satisfied by either:
|
|
175
|
+
* - A unique constraint with the same columns, or
|
|
176
|
+
* - A unique index with the same columns
|
|
177
|
+
*
|
|
178
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
179
|
+
*/
|
|
180
|
+
function verifyUniqueConstraints(contractUniques, schemaUniques, schemaIndexes, tableName, tablePath, issues, strict) {
|
|
181
|
+
const nodes = [];
|
|
182
|
+
for (const contractUnique of contractUniques) {
|
|
183
|
+
const uniquePath = `${tablePath}.uniques[${contractUnique.columns.join(",")}]`;
|
|
184
|
+
const matchingUnique = schemaUniques.find((u) => arraysEqual(u.columns, contractUnique.columns));
|
|
185
|
+
const matchingUniqueIndex = !matchingUnique && schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
|
|
186
|
+
if (!matchingUnique && !matchingUniqueIndex) {
|
|
187
|
+
issues.push({
|
|
188
|
+
kind: "unique_constraint_mismatch",
|
|
189
|
+
table: tableName,
|
|
190
|
+
expected: contractUnique.columns.join(", "),
|
|
191
|
+
message: `Table "${tableName}" is missing unique constraint: ${contractUnique.columns.join(", ")}`
|
|
192
|
+
});
|
|
193
|
+
nodes.push({
|
|
194
|
+
status: "fail",
|
|
195
|
+
kind: "unique",
|
|
196
|
+
name: `unique(${contractUnique.columns.join(", ")})`,
|
|
197
|
+
contractPath: uniquePath,
|
|
198
|
+
code: "unique_constraint_mismatch",
|
|
199
|
+
message: "Unique constraint missing",
|
|
200
|
+
expected: contractUnique,
|
|
201
|
+
actual: void 0,
|
|
202
|
+
children: []
|
|
203
|
+
});
|
|
204
|
+
} else nodes.push({
|
|
205
|
+
status: "pass",
|
|
206
|
+
kind: "unique",
|
|
207
|
+
name: `unique(${contractUnique.columns.join(", ")})`,
|
|
208
|
+
contractPath: uniquePath,
|
|
209
|
+
code: "",
|
|
210
|
+
message: "",
|
|
211
|
+
expected: void 0,
|
|
212
|
+
actual: void 0,
|
|
213
|
+
children: []
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (strict) {
|
|
217
|
+
for (const schemaUnique of schemaUniques) if (!contractUniques.find((u) => arraysEqual(u.columns, schemaUnique.columns))) {
|
|
218
|
+
issues.push({
|
|
219
|
+
kind: "extra_unique_constraint",
|
|
220
|
+
table: tableName,
|
|
221
|
+
message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(", ")}`
|
|
222
|
+
});
|
|
223
|
+
nodes.push({
|
|
224
|
+
status: "fail",
|
|
225
|
+
kind: "unique",
|
|
226
|
+
name: `unique(${schemaUnique.columns.join(", ")})`,
|
|
227
|
+
contractPath: `${tablePath}.uniques[${schemaUnique.columns.join(",")}]`,
|
|
228
|
+
code: "extra_unique_constraint",
|
|
229
|
+
message: "Extra unique constraint found",
|
|
230
|
+
expected: void 0,
|
|
231
|
+
actual: schemaUnique,
|
|
232
|
+
children: []
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return nodes;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Verifies indexes match between contract and schema.
|
|
240
|
+
* Returns verification nodes for the tree.
|
|
241
|
+
*
|
|
242
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
243
|
+
* A non-unique index requirement can be satisfied by either:
|
|
244
|
+
* - A non-unique index with the same columns, or
|
|
245
|
+
* - A unique index with the same columns (stronger satisfies weaker)
|
|
246
|
+
*
|
|
247
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
248
|
+
*/
|
|
249
|
+
function verifyIndexes(contractIndexes, schemaIndexes, schemaUniques, tableName, tablePath, issues, strict) {
|
|
250
|
+
const nodes = [];
|
|
251
|
+
for (const contractIndex of contractIndexes) {
|
|
252
|
+
const indexPath = `${tablePath}.indexes[${contractIndex.columns.join(",")}]`;
|
|
253
|
+
const matchingIndex = schemaIndexes.find((idx) => arraysEqual(idx.columns, contractIndex.columns));
|
|
254
|
+
const matchingUniqueConstraint = !matchingIndex && schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
|
|
255
|
+
if (!matchingIndex && !matchingUniqueConstraint) {
|
|
256
|
+
issues.push({
|
|
257
|
+
kind: "index_mismatch",
|
|
258
|
+
table: tableName,
|
|
259
|
+
expected: contractIndex.columns.join(", "),
|
|
260
|
+
message: `Table "${tableName}" is missing index: ${contractIndex.columns.join(", ")}`
|
|
261
|
+
});
|
|
262
|
+
nodes.push({
|
|
263
|
+
status: "fail",
|
|
264
|
+
kind: "index",
|
|
265
|
+
name: `index(${contractIndex.columns.join(", ")})`,
|
|
266
|
+
contractPath: indexPath,
|
|
267
|
+
code: "index_mismatch",
|
|
268
|
+
message: "Index missing",
|
|
269
|
+
expected: contractIndex,
|
|
270
|
+
actual: void 0,
|
|
271
|
+
children: []
|
|
272
|
+
});
|
|
273
|
+
} else nodes.push({
|
|
274
|
+
status: "pass",
|
|
275
|
+
kind: "index",
|
|
276
|
+
name: `index(${contractIndex.columns.join(", ")})`,
|
|
277
|
+
contractPath: indexPath,
|
|
278
|
+
code: "",
|
|
279
|
+
message: "",
|
|
280
|
+
expected: void 0,
|
|
281
|
+
actual: void 0,
|
|
282
|
+
children: []
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (strict) for (const schemaIndex of schemaIndexes) {
|
|
286
|
+
if (schemaIndex.unique) continue;
|
|
287
|
+
if (!contractIndexes.find((idx) => arraysEqual(idx.columns, schemaIndex.columns))) {
|
|
288
|
+
issues.push({
|
|
289
|
+
kind: "extra_index",
|
|
290
|
+
table: tableName,
|
|
291
|
+
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(", ")}`
|
|
292
|
+
});
|
|
293
|
+
nodes.push({
|
|
294
|
+
status: "fail",
|
|
295
|
+
kind: "index",
|
|
296
|
+
name: `index(${schemaIndex.columns.join(", ")})`,
|
|
297
|
+
contractPath: `${tablePath}.indexes[${schemaIndex.columns.join(",")}]`,
|
|
298
|
+
code: "extra_index",
|
|
299
|
+
message: "Extra index found",
|
|
300
|
+
expected: void 0,
|
|
301
|
+
actual: schemaIndex,
|
|
302
|
+
children: []
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return nodes;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Verifies database dependencies are installed using component-owned verification hooks.
|
|
310
|
+
* Each dependency provides a pure verifyDatabaseDependencyInstalled function that checks
|
|
311
|
+
* whether the dependency is satisfied based on the in-memory schema IR (no DB I/O).
|
|
312
|
+
*
|
|
313
|
+
* Returns verification nodes for the tree.
|
|
314
|
+
*/
|
|
315
|
+
function verifyDatabaseDependencies(dependencies, schema, issues) {
|
|
316
|
+
const nodes = [];
|
|
317
|
+
for (const dependency of dependencies) {
|
|
318
|
+
const depIssues = dependency.verifyDatabaseDependencyInstalled(schema);
|
|
319
|
+
const depPath = `dependencies.${dependency.id}`;
|
|
320
|
+
if (depIssues.length > 0) {
|
|
321
|
+
issues.push(...depIssues);
|
|
322
|
+
const issuesMessage = depIssues.map((i) => i.message).join("; ");
|
|
323
|
+
const nodeMessage = issuesMessage ? `${dependency.id}: ${issuesMessage}` : dependency.id;
|
|
324
|
+
nodes.push({
|
|
325
|
+
status: "fail",
|
|
326
|
+
kind: "databaseDependency",
|
|
327
|
+
name: dependency.label,
|
|
328
|
+
contractPath: depPath,
|
|
329
|
+
code: "dependency_missing",
|
|
330
|
+
message: nodeMessage,
|
|
331
|
+
expected: void 0,
|
|
332
|
+
actual: void 0,
|
|
333
|
+
children: []
|
|
334
|
+
});
|
|
335
|
+
} else nodes.push({
|
|
336
|
+
status: "pass",
|
|
337
|
+
kind: "databaseDependency",
|
|
338
|
+
name: dependency.label,
|
|
339
|
+
contractPath: depPath,
|
|
340
|
+
code: "",
|
|
341
|
+
message: "",
|
|
342
|
+
expected: void 0,
|
|
343
|
+
actual: void 0,
|
|
344
|
+
children: []
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return nodes;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Computes counts of pass/warn/fail nodes by traversing the tree.
|
|
351
|
+
*/
|
|
352
|
+
function computeCounts(node) {
|
|
353
|
+
let pass = 0;
|
|
354
|
+
let warn = 0;
|
|
355
|
+
let fail = 0;
|
|
356
|
+
function traverse(n) {
|
|
357
|
+
if (n.status === "pass") pass++;
|
|
358
|
+
else if (n.status === "warn") warn++;
|
|
359
|
+
else if (n.status === "fail") fail++;
|
|
360
|
+
if (n.children) for (const child of n.children) traverse(child);
|
|
361
|
+
}
|
|
362
|
+
traverse(node);
|
|
363
|
+
return {
|
|
364
|
+
pass,
|
|
365
|
+
warn,
|
|
366
|
+
fail,
|
|
367
|
+
totalNodes: pass + warn + fail
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Compares referential actions between a contract FK and a schema FK.
|
|
372
|
+
* Only compares when the contract FK explicitly specifies onDelete or onUpdate.
|
|
373
|
+
* Returns all mismatches (both onDelete and onUpdate) so both are reported at once.
|
|
374
|
+
*
|
|
375
|
+
* Note: 'noAction' in the contract is semantically equivalent to undefined in the
|
|
376
|
+
* schema IR, because the introspection adapter omits 'NO ACTION' (the database default)
|
|
377
|
+
* to keep the IR sparse. We normalize both sides before comparing.
|
|
378
|
+
*/
|
|
379
|
+
function getReferentialActionMismatches(contractFK, schemaFK) {
|
|
380
|
+
const mismatches = [];
|
|
381
|
+
const contractOnDelete = normalizeReferentialAction(contractFK.onDelete);
|
|
382
|
+
const schemaOnDelete = normalizeReferentialAction(schemaFK.onDelete);
|
|
383
|
+
if (contractOnDelete !== void 0 && contractOnDelete !== schemaOnDelete) mismatches.push({
|
|
384
|
+
expected: `onDelete: ${contractFK.onDelete}`,
|
|
385
|
+
actual: `onDelete: ${schemaFK.onDelete ?? "noAction (default)"}`,
|
|
386
|
+
message: `onDelete mismatch: expected ${contractFK.onDelete}, got ${schemaFK.onDelete ?? "noAction (default)"}`
|
|
387
|
+
});
|
|
388
|
+
const contractOnUpdate = normalizeReferentialAction(contractFK.onUpdate);
|
|
389
|
+
const schemaOnUpdate = normalizeReferentialAction(schemaFK.onUpdate);
|
|
390
|
+
if (contractOnUpdate !== void 0 && contractOnUpdate !== schemaOnUpdate) mismatches.push({
|
|
391
|
+
expected: `onUpdate: ${contractFK.onUpdate}`,
|
|
392
|
+
actual: `onUpdate: ${schemaFK.onUpdate ?? "noAction (default)"}`,
|
|
393
|
+
message: `onUpdate mismatch: expected ${contractFK.onUpdate}, got ${schemaFK.onUpdate ?? "noAction (default)"}`
|
|
394
|
+
});
|
|
395
|
+
return mismatches;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Normalizes a referential action value for comparison.
|
|
399
|
+
* 'noAction' is the database default and equivalent to undefined (omitted) in the sparse IR.
|
|
400
|
+
*/
|
|
401
|
+
function normalizeReferentialAction(action) {
|
|
402
|
+
return action === "noAction" ? void 0 : action;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/core/schema-verify/verify-sql-schema.ts
|
|
407
|
+
/**
|
|
408
|
+
* Verifies that a SqlSchemaIR matches a SqlContract.
|
|
409
|
+
*
|
|
410
|
+
* This is a pure function that does NOT perform any database I/O.
|
|
411
|
+
* It takes an already-introspected schema IR and compares it against
|
|
412
|
+
* the contract requirements.
|
|
413
|
+
*
|
|
414
|
+
* @param options - Verification options
|
|
415
|
+
* @returns VerifyDatabaseSchemaResult with verification tree and issues
|
|
416
|
+
*/
|
|
417
|
+
function verifySqlSchema(options) {
|
|
418
|
+
const { contract, schema, strict, context, typeMetadataRegistry, normalizeDefault, normalizeNativeType } = options;
|
|
419
|
+
const startTime = Date.now();
|
|
420
|
+
const codecHooks = extractCodecControlHooks(options.frameworkComponents);
|
|
421
|
+
const { contractStorageHash, contractProfileHash, contractTarget } = extractContractMetadata(contract);
|
|
422
|
+
const { issues, rootChildren } = verifySchemaTables({
|
|
423
|
+
contract,
|
|
424
|
+
schema,
|
|
425
|
+
strict,
|
|
426
|
+
typeMetadataRegistry,
|
|
427
|
+
codecHooks,
|
|
428
|
+
...ifDefined("normalizeDefault", normalizeDefault),
|
|
429
|
+
...ifDefined("normalizeNativeType", normalizeNativeType)
|
|
430
|
+
});
|
|
431
|
+
validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
|
|
432
|
+
const storageTypes = contract.storage.types ?? {};
|
|
433
|
+
const storageTypeEntries = Object.entries(storageTypes);
|
|
434
|
+
if (storageTypeEntries.length > 0) {
|
|
435
|
+
const typeNodes = [];
|
|
436
|
+
for (const [typeName, typeInstance] of storageTypeEntries) {
|
|
437
|
+
const hook = codecHooks.get(typeInstance.codecId);
|
|
438
|
+
const typeIssues = hook?.verifyType ? hook.verifyType({
|
|
439
|
+
typeName,
|
|
440
|
+
typeInstance,
|
|
441
|
+
schema
|
|
442
|
+
}) : [];
|
|
443
|
+
if (typeIssues.length > 0) issues.push(...typeIssues);
|
|
444
|
+
const typeStatus = typeIssues.length > 0 ? "fail" : "pass";
|
|
445
|
+
const typeCode = typeIssues.length > 0 ? typeIssues[0]?.kind ?? "" : "";
|
|
446
|
+
typeNodes.push({
|
|
447
|
+
status: typeStatus,
|
|
448
|
+
kind: "storageType",
|
|
449
|
+
name: `type ${typeName}`,
|
|
450
|
+
contractPath: `storage.types.${typeName}`,
|
|
451
|
+
code: typeCode,
|
|
452
|
+
message: typeIssues.length > 0 ? `${typeIssues.length} issue${typeIssues.length === 1 ? "" : "s"}` : "",
|
|
453
|
+
expected: void 0,
|
|
454
|
+
actual: void 0,
|
|
455
|
+
children: []
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
const typesStatus = typeNodes.some((n) => n.status === "fail") ? "fail" : "pass";
|
|
459
|
+
rootChildren.push({
|
|
460
|
+
status: typesStatus,
|
|
461
|
+
kind: "storageTypes",
|
|
462
|
+
name: "types",
|
|
463
|
+
contractPath: "storage.types",
|
|
464
|
+
code: typesStatus === "fail" ? "type_mismatch" : "",
|
|
465
|
+
message: "",
|
|
466
|
+
expected: void 0,
|
|
467
|
+
actual: void 0,
|
|
468
|
+
children: typeNodes
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
const dependencyStatuses = verifyDatabaseDependencies(collectDependenciesFromFrameworkComponents(options.frameworkComponents), schema, issues);
|
|
472
|
+
rootChildren.push(...dependencyStatuses);
|
|
473
|
+
const root = buildRootNode(rootChildren);
|
|
474
|
+
const counts = computeCounts(root);
|
|
475
|
+
const ok = counts.fail === 0;
|
|
476
|
+
const code = ok ? void 0 : "PN-SCHEMA-0001";
|
|
477
|
+
const summary = ok ? "Database schema satisfies contract" : `Database schema does not satisfy contract (${counts.fail} failure${counts.fail === 1 ? "" : "s"})`;
|
|
478
|
+
const totalTime = Date.now() - startTime;
|
|
479
|
+
return {
|
|
480
|
+
ok,
|
|
481
|
+
...ifDefined("code", code),
|
|
482
|
+
summary,
|
|
483
|
+
contract: {
|
|
484
|
+
storageHash: contractStorageHash,
|
|
485
|
+
...ifDefined("profileHash", contractProfileHash)
|
|
486
|
+
},
|
|
487
|
+
target: {
|
|
488
|
+
expected: contractTarget,
|
|
489
|
+
actual: contractTarget
|
|
490
|
+
},
|
|
491
|
+
schema: {
|
|
492
|
+
issues,
|
|
493
|
+
root,
|
|
494
|
+
counts
|
|
495
|
+
},
|
|
496
|
+
meta: {
|
|
497
|
+
strict,
|
|
498
|
+
...ifDefined("contractPath", context?.contractPath),
|
|
499
|
+
...ifDefined("configPath", context?.configPath)
|
|
500
|
+
},
|
|
501
|
+
timings: { total: totalTime }
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function extractContractMetadata(contract) {
|
|
505
|
+
return {
|
|
506
|
+
contractStorageHash: contract.storageHash,
|
|
507
|
+
contractProfileHash: "profileHash" in contract && typeof contract.profileHash === "string" ? contract.profileHash : void 0,
|
|
508
|
+
contractTarget: contract.target
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function verifySchemaTables(options) {
|
|
512
|
+
const { contract, schema, strict, typeMetadataRegistry, codecHooks, normalizeDefault, normalizeNativeType } = options;
|
|
513
|
+
const issues = [];
|
|
514
|
+
const rootChildren = [];
|
|
515
|
+
const contractTables = contract.storage.tables;
|
|
516
|
+
const schemaTables = schema.tables;
|
|
517
|
+
for (const [tableName, contractTable] of Object.entries(contractTables)) {
|
|
518
|
+
const schemaTable = schemaTables[tableName];
|
|
519
|
+
const tablePath = `storage.tables.${tableName}`;
|
|
520
|
+
if (!schemaTable) {
|
|
521
|
+
issues.push({
|
|
522
|
+
kind: "missing_table",
|
|
523
|
+
table: tableName,
|
|
524
|
+
message: `Table "${tableName}" is missing from database`
|
|
525
|
+
});
|
|
526
|
+
rootChildren.push({
|
|
527
|
+
status: "fail",
|
|
528
|
+
kind: "table",
|
|
529
|
+
name: `table ${tableName}`,
|
|
530
|
+
contractPath: tablePath,
|
|
531
|
+
code: "missing_table",
|
|
532
|
+
message: `Table "${tableName}" is missing`,
|
|
533
|
+
expected: void 0,
|
|
534
|
+
actual: void 0,
|
|
535
|
+
children: []
|
|
536
|
+
});
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const tableChildren = verifyTableChildren({
|
|
540
|
+
contractTable,
|
|
541
|
+
schemaTable,
|
|
542
|
+
tableName,
|
|
543
|
+
tablePath,
|
|
544
|
+
issues,
|
|
545
|
+
strict,
|
|
546
|
+
typeMetadataRegistry,
|
|
547
|
+
codecHooks,
|
|
548
|
+
...ifDefined("normalizeDefault", normalizeDefault),
|
|
549
|
+
...ifDefined("normalizeNativeType", normalizeNativeType)
|
|
550
|
+
});
|
|
551
|
+
rootChildren.push(buildTableNode(tableName, tablePath, tableChildren));
|
|
552
|
+
}
|
|
553
|
+
if (strict) {
|
|
554
|
+
for (const tableName of Object.keys(schemaTables)) if (!contractTables[tableName]) {
|
|
555
|
+
issues.push({
|
|
556
|
+
kind: "extra_table",
|
|
557
|
+
table: tableName,
|
|
558
|
+
message: `Extra table "${tableName}" found in database (not in contract)`
|
|
559
|
+
});
|
|
560
|
+
rootChildren.push({
|
|
561
|
+
status: "fail",
|
|
562
|
+
kind: "table",
|
|
563
|
+
name: `table ${tableName}`,
|
|
564
|
+
contractPath: `storage.tables.${tableName}`,
|
|
565
|
+
code: "extra_table",
|
|
566
|
+
message: `Extra table "${tableName}" found`,
|
|
567
|
+
expected: void 0,
|
|
568
|
+
actual: void 0,
|
|
569
|
+
children: []
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
issues,
|
|
575
|
+
rootChildren
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function verifyTableChildren(options) {
|
|
579
|
+
const { contractTable, schemaTable, tableName, tablePath, issues, strict, typeMetadataRegistry, codecHooks, normalizeDefault, normalizeNativeType } = options;
|
|
580
|
+
const tableChildren = [];
|
|
581
|
+
const columnNodes = collectContractColumnNodes({
|
|
582
|
+
contractTable,
|
|
583
|
+
schemaTable,
|
|
584
|
+
tableName,
|
|
585
|
+
tablePath,
|
|
586
|
+
issues,
|
|
587
|
+
typeMetadataRegistry,
|
|
588
|
+
codecHooks,
|
|
589
|
+
...ifDefined("normalizeDefault", normalizeDefault),
|
|
590
|
+
...ifDefined("normalizeNativeType", normalizeNativeType)
|
|
591
|
+
});
|
|
592
|
+
if (columnNodes.length > 0) tableChildren.push(buildColumnsNode(tablePath, columnNodes));
|
|
593
|
+
if (strict) appendExtraColumnNodes({
|
|
594
|
+
contractTable,
|
|
595
|
+
schemaTable,
|
|
596
|
+
tableName,
|
|
597
|
+
tablePath,
|
|
598
|
+
issues,
|
|
599
|
+
columnNodes
|
|
600
|
+
});
|
|
601
|
+
if (contractTable.primaryKey) if (verifyPrimaryKey(contractTable.primaryKey, schemaTable.primaryKey, tableName, issues) === "fail") tableChildren.push({
|
|
602
|
+
status: "fail",
|
|
603
|
+
kind: "primaryKey",
|
|
604
|
+
name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
|
|
605
|
+
contractPath: `${tablePath}.primaryKey`,
|
|
606
|
+
code: "primary_key_mismatch",
|
|
607
|
+
message: "Primary key mismatch",
|
|
608
|
+
expected: contractTable.primaryKey,
|
|
609
|
+
actual: schemaTable.primaryKey,
|
|
610
|
+
children: []
|
|
611
|
+
});
|
|
612
|
+
else tableChildren.push({
|
|
613
|
+
status: "pass",
|
|
614
|
+
kind: "primaryKey",
|
|
615
|
+
name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
|
|
616
|
+
contractPath: `${tablePath}.primaryKey`,
|
|
617
|
+
code: "",
|
|
618
|
+
message: "",
|
|
619
|
+
expected: void 0,
|
|
620
|
+
actual: void 0,
|
|
621
|
+
children: []
|
|
622
|
+
});
|
|
623
|
+
else if (schemaTable.primaryKey && strict) {
|
|
624
|
+
issues.push({
|
|
625
|
+
kind: "extra_primary_key",
|
|
626
|
+
table: tableName,
|
|
627
|
+
message: "Extra primary key found in database (not in contract)"
|
|
628
|
+
});
|
|
629
|
+
tableChildren.push({
|
|
630
|
+
status: "fail",
|
|
631
|
+
kind: "primaryKey",
|
|
632
|
+
name: `primary key: ${schemaTable.primaryKey.columns.join(", ")}`,
|
|
633
|
+
contractPath: `${tablePath}.primaryKey`,
|
|
634
|
+
code: "extra_primary_key",
|
|
635
|
+
message: "Extra primary key found",
|
|
636
|
+
expected: void 0,
|
|
637
|
+
actual: schemaTable.primaryKey,
|
|
638
|
+
children: []
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
const constraintFks = contractTable.foreignKeys.filter((fk) => fk.constraint === true);
|
|
642
|
+
if (constraintFks.length > 0) {
|
|
643
|
+
const fkStatuses = verifyForeignKeys(constraintFks, schemaTable.foreignKeys, tableName, tablePath, issues, strict);
|
|
644
|
+
tableChildren.push(...fkStatuses);
|
|
645
|
+
}
|
|
646
|
+
const uniqueStatuses = verifyUniqueConstraints(contractTable.uniques, schemaTable.uniques, schemaTable.indexes, tableName, tablePath, issues, strict);
|
|
647
|
+
tableChildren.push(...uniqueStatuses);
|
|
648
|
+
const fkBackingIndexes = contractTable.foreignKeys.filter((fk) => fk.index === true && !contractTable.indexes.some((idx) => arraysEqual(idx.columns, fk.columns))).map((fk) => ({ columns: fk.columns }));
|
|
649
|
+
const indexStatuses = verifyIndexes([...contractTable.indexes, ...fkBackingIndexes], schemaTable.indexes, schemaTable.uniques, tableName, tablePath, issues, strict);
|
|
650
|
+
tableChildren.push(...indexStatuses);
|
|
651
|
+
return tableChildren;
|
|
652
|
+
}
|
|
653
|
+
function collectContractColumnNodes(options) {
|
|
654
|
+
const { contractTable, schemaTable, tableName, tablePath, issues, typeMetadataRegistry, codecHooks, normalizeDefault, normalizeNativeType } = options;
|
|
655
|
+
const columnNodes = [];
|
|
656
|
+
for (const [columnName, contractColumn] of Object.entries(contractTable.columns)) {
|
|
657
|
+
const schemaColumn = schemaTable.columns[columnName];
|
|
658
|
+
const columnPath = `${tablePath}.columns.${columnName}`;
|
|
659
|
+
if (!schemaColumn) {
|
|
660
|
+
issues.push({
|
|
661
|
+
kind: "missing_column",
|
|
662
|
+
table: tableName,
|
|
663
|
+
column: columnName,
|
|
664
|
+
message: `Column "${tableName}"."${columnName}" is missing from database`
|
|
665
|
+
});
|
|
666
|
+
columnNodes.push({
|
|
667
|
+
status: "fail",
|
|
668
|
+
kind: "column",
|
|
669
|
+
name: `${columnName}: missing`,
|
|
670
|
+
contractPath: columnPath,
|
|
671
|
+
code: "missing_column",
|
|
672
|
+
message: `Column "${columnName}" is missing`,
|
|
673
|
+
expected: void 0,
|
|
674
|
+
actual: void 0,
|
|
675
|
+
children: []
|
|
676
|
+
});
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
columnNodes.push(verifyColumn({
|
|
680
|
+
tableName,
|
|
681
|
+
columnName,
|
|
682
|
+
contractColumn,
|
|
683
|
+
schemaColumn,
|
|
684
|
+
columnPath,
|
|
685
|
+
issues,
|
|
686
|
+
typeMetadataRegistry,
|
|
687
|
+
codecHooks,
|
|
688
|
+
...ifDefined("normalizeDefault", normalizeDefault),
|
|
689
|
+
...ifDefined("normalizeNativeType", normalizeNativeType)
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
return columnNodes;
|
|
693
|
+
}
|
|
694
|
+
function appendExtraColumnNodes(options) {
|
|
695
|
+
const { contractTable, schemaTable, tableName, tablePath, issues, columnNodes } = options;
|
|
696
|
+
for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) if (!contractTable.columns[columnName]) {
|
|
697
|
+
issues.push({
|
|
698
|
+
kind: "extra_column",
|
|
699
|
+
table: tableName,
|
|
700
|
+
column: columnName,
|
|
701
|
+
message: `Extra column "${tableName}"."${columnName}" found in database (not in contract)`
|
|
702
|
+
});
|
|
703
|
+
columnNodes.push({
|
|
704
|
+
status: "fail",
|
|
705
|
+
kind: "column",
|
|
706
|
+
name: `${columnName}: extra`,
|
|
707
|
+
contractPath: `${tablePath}.columns.${columnName}`,
|
|
708
|
+
code: "extra_column",
|
|
709
|
+
message: `Extra column "${columnName}" found`,
|
|
710
|
+
expected: void 0,
|
|
711
|
+
actual: nativeType,
|
|
712
|
+
children: []
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function verifyColumn(options) {
|
|
717
|
+
const { tableName, columnName, contractColumn, schemaColumn, columnPath, issues, codecHooks, normalizeDefault, normalizeNativeType } = options;
|
|
718
|
+
const columnChildren = [];
|
|
719
|
+
let columnStatus = "pass";
|
|
720
|
+
const contractNativeType = renderExpectedNativeType(contractColumn, codecHooks);
|
|
721
|
+
const schemaNativeType = normalizeNativeType?.(schemaColumn.nativeType) ?? schemaColumn.nativeType;
|
|
722
|
+
if (contractNativeType !== schemaNativeType) {
|
|
723
|
+
issues.push({
|
|
724
|
+
kind: "type_mismatch",
|
|
725
|
+
table: tableName,
|
|
726
|
+
column: columnName,
|
|
727
|
+
expected: contractNativeType,
|
|
728
|
+
actual: schemaNativeType,
|
|
729
|
+
message: `Column "${tableName}"."${columnName}" has type mismatch: expected "${contractNativeType}", got "${schemaNativeType}"`
|
|
730
|
+
});
|
|
731
|
+
columnChildren.push({
|
|
732
|
+
status: "fail",
|
|
733
|
+
kind: "type",
|
|
734
|
+
name: "type",
|
|
735
|
+
contractPath: `${columnPath}.nativeType`,
|
|
736
|
+
code: "type_mismatch",
|
|
737
|
+
message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
|
|
738
|
+
expected: contractNativeType,
|
|
739
|
+
actual: schemaNativeType,
|
|
740
|
+
children: []
|
|
741
|
+
});
|
|
742
|
+
columnStatus = "fail";
|
|
743
|
+
}
|
|
744
|
+
if (contractColumn.codecId) {
|
|
745
|
+
const typeMetadata = options.typeMetadataRegistry.get(contractColumn.codecId);
|
|
746
|
+
if (!typeMetadata) columnChildren.push({
|
|
747
|
+
status: "warn",
|
|
748
|
+
kind: "type",
|
|
749
|
+
name: "type_metadata_missing",
|
|
750
|
+
contractPath: `${columnPath}.codecId`,
|
|
751
|
+
code: "type_metadata_missing",
|
|
752
|
+
message: `codecId "${contractColumn.codecId}" not found in type metadata registry`,
|
|
753
|
+
expected: contractColumn.codecId,
|
|
754
|
+
actual: void 0,
|
|
755
|
+
children: []
|
|
756
|
+
});
|
|
757
|
+
else if (typeMetadata.nativeType && typeMetadata.nativeType !== contractColumn.nativeType) columnChildren.push({
|
|
758
|
+
status: "warn",
|
|
759
|
+
kind: "type",
|
|
760
|
+
name: "type_consistency",
|
|
761
|
+
contractPath: `${columnPath}.codecId`,
|
|
762
|
+
code: "type_consistency_warning",
|
|
763
|
+
message: `codecId "${contractColumn.codecId}" maps to nativeType "${typeMetadata.nativeType}" in registry, but contract has "${contractColumn.nativeType}"`,
|
|
764
|
+
expected: typeMetadata.nativeType,
|
|
765
|
+
actual: contractColumn.nativeType,
|
|
766
|
+
children: []
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (contractColumn.nullable !== schemaColumn.nullable) {
|
|
770
|
+
issues.push({
|
|
771
|
+
kind: "nullability_mismatch",
|
|
772
|
+
table: tableName,
|
|
773
|
+
column: columnName,
|
|
774
|
+
expected: String(contractColumn.nullable),
|
|
775
|
+
actual: String(schemaColumn.nullable),
|
|
776
|
+
message: `Column "${tableName}"."${columnName}" has nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`
|
|
777
|
+
});
|
|
778
|
+
columnChildren.push({
|
|
779
|
+
status: "fail",
|
|
780
|
+
kind: "nullability",
|
|
781
|
+
name: "nullability",
|
|
782
|
+
contractPath: `${columnPath}.nullable`,
|
|
783
|
+
code: "nullability_mismatch",
|
|
784
|
+
message: `Nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`,
|
|
785
|
+
expected: contractColumn.nullable,
|
|
786
|
+
actual: schemaColumn.nullable,
|
|
787
|
+
children: []
|
|
788
|
+
});
|
|
789
|
+
columnStatus = "fail";
|
|
790
|
+
}
|
|
791
|
+
if (contractColumn.default) {
|
|
792
|
+
if (!schemaColumn.default) {
|
|
793
|
+
const defaultDescription = describeColumnDefault(contractColumn.default);
|
|
794
|
+
issues.push({
|
|
795
|
+
kind: "default_missing",
|
|
796
|
+
table: tableName,
|
|
797
|
+
column: columnName,
|
|
798
|
+
expected: defaultDescription,
|
|
799
|
+
message: `Column "${tableName}"."${columnName}" should have default ${defaultDescription} but database has no default`
|
|
800
|
+
});
|
|
801
|
+
columnChildren.push({
|
|
802
|
+
status: "fail",
|
|
803
|
+
kind: "default",
|
|
804
|
+
name: "default",
|
|
805
|
+
contractPath: `${columnPath}.default`,
|
|
806
|
+
code: "default_missing",
|
|
807
|
+
message: `Default missing: expected ${defaultDescription}`,
|
|
808
|
+
expected: defaultDescription,
|
|
809
|
+
actual: void 0,
|
|
810
|
+
children: []
|
|
811
|
+
});
|
|
812
|
+
columnStatus = "fail";
|
|
813
|
+
} else if (!columnDefaultsEqual(contractColumn.default, schemaColumn.default, normalizeDefault, schemaNativeType)) {
|
|
814
|
+
const expectedDescription = describeColumnDefault(contractColumn.default);
|
|
815
|
+
const actualDescription = schemaColumn.default;
|
|
816
|
+
issues.push({
|
|
817
|
+
kind: "default_mismatch",
|
|
818
|
+
table: tableName,
|
|
819
|
+
column: columnName,
|
|
820
|
+
expected: expectedDescription,
|
|
821
|
+
actual: actualDescription,
|
|
822
|
+
message: `Column "${tableName}"."${columnName}" has default mismatch: expected ${expectedDescription}, got ${actualDescription}`
|
|
823
|
+
});
|
|
824
|
+
columnChildren.push({
|
|
825
|
+
status: "fail",
|
|
826
|
+
kind: "default",
|
|
827
|
+
name: "default",
|
|
828
|
+
contractPath: `${columnPath}.default`,
|
|
829
|
+
code: "default_mismatch",
|
|
830
|
+
message: `Default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
|
|
831
|
+
expected: expectedDescription,
|
|
832
|
+
actual: actualDescription,
|
|
833
|
+
children: []
|
|
834
|
+
});
|
|
835
|
+
columnStatus = "fail";
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const aggregated = aggregateChildState(columnChildren, columnStatus);
|
|
839
|
+
const nullableText = contractColumn.nullable ? "nullable" : "not nullable";
|
|
840
|
+
const columnTypeDisplay = contractColumn.codecId ? `${contractNativeType} (${contractColumn.codecId})` : contractNativeType;
|
|
841
|
+
const columnMessage = aggregated.failureMessages.join("; ");
|
|
842
|
+
return {
|
|
843
|
+
status: aggregated.status,
|
|
844
|
+
kind: "column",
|
|
845
|
+
name: `${columnName}: ${columnTypeDisplay} (${nullableText})`,
|
|
846
|
+
contractPath: columnPath,
|
|
847
|
+
code: aggregated.firstCode,
|
|
848
|
+
message: columnMessage,
|
|
849
|
+
expected: void 0,
|
|
850
|
+
actual: void 0,
|
|
851
|
+
children: columnChildren
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function buildColumnsNode(tablePath, columnNodes) {
|
|
855
|
+
return {
|
|
856
|
+
status: aggregateChildState(columnNodes, "pass").status,
|
|
857
|
+
kind: "columns",
|
|
858
|
+
name: "columns",
|
|
859
|
+
contractPath: `${tablePath}.columns`,
|
|
860
|
+
code: "",
|
|
861
|
+
message: "",
|
|
862
|
+
expected: void 0,
|
|
863
|
+
actual: void 0,
|
|
864
|
+
children: columnNodes
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function buildTableNode(tableName, tablePath, tableChildren) {
|
|
868
|
+
const tableStatus = aggregateChildState(tableChildren, "pass").status;
|
|
869
|
+
const tableFailureMessages = tableChildren.filter((child) => child.status === "fail" && child.message).map((child) => child.message).filter((msg) => typeof msg === "string" && msg.length > 0);
|
|
870
|
+
const tableMessage = tableStatus === "fail" && tableFailureMessages.length > 0 ? `${tableFailureMessages.length} issue${tableFailureMessages.length === 1 ? "" : "s"}` : "";
|
|
871
|
+
const tableCode = tableStatus === "fail" && tableChildren.length > 0 && tableChildren[0] ? tableChildren[0].code : "";
|
|
872
|
+
return {
|
|
873
|
+
status: tableStatus,
|
|
874
|
+
kind: "table",
|
|
875
|
+
name: `table ${tableName}`,
|
|
876
|
+
contractPath: tablePath,
|
|
877
|
+
code: tableCode,
|
|
878
|
+
message: tableMessage,
|
|
879
|
+
expected: void 0,
|
|
880
|
+
actual: void 0,
|
|
881
|
+
children: tableChildren
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function buildRootNode(rootChildren) {
|
|
885
|
+
return {
|
|
886
|
+
status: aggregateChildState(rootChildren, "pass").status,
|
|
887
|
+
kind: "contract",
|
|
888
|
+
name: "contract",
|
|
889
|
+
contractPath: "",
|
|
890
|
+
code: "",
|
|
891
|
+
message: "",
|
|
892
|
+
expected: void 0,
|
|
893
|
+
actual: void 0,
|
|
894
|
+
children: rootChildren
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Aggregates status, failure messages, and code from children in a single pass.
|
|
899
|
+
* This is more efficient than calling separate functions that each iterate the array.
|
|
900
|
+
*/
|
|
901
|
+
function aggregateChildState(children, fallback) {
|
|
902
|
+
let status = fallback;
|
|
903
|
+
const failureMessages = [];
|
|
904
|
+
let firstCode = "";
|
|
905
|
+
for (const child of children) if (child.status === "fail") {
|
|
906
|
+
status = "fail";
|
|
907
|
+
if (!firstCode) firstCode = child.code;
|
|
908
|
+
if (child.message && typeof child.message === "string" && child.message.length > 0) failureMessages.push(child.message);
|
|
909
|
+
} else if (child.status === "warn" && status !== "fail") {
|
|
910
|
+
status = "warn";
|
|
911
|
+
if (!firstCode) firstCode = child.code;
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
status,
|
|
915
|
+
failureMessages,
|
|
916
|
+
firstCode
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function validateFrameworkComponentsForExtensions(contract, frameworkComponents) {
|
|
920
|
+
const contractExtensionPacks = contract.extensionPacks ?? {};
|
|
921
|
+
for (const extensionNamespace of Object.keys(contractExtensionPacks)) if (!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.`);
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Type predicate to check if a component has database dependencies with an init array.
|
|
925
|
+
* The familyId check is redundant since TargetBoundComponentDescriptor<'sql', T> already
|
|
926
|
+
* guarantees familyId is 'sql' at the type level, so we don't need runtime checks for it.
|
|
927
|
+
*/
|
|
928
|
+
function hasDatabaseDependenciesInit(component) {
|
|
929
|
+
if (!("databaseDependencies" in component)) return false;
|
|
930
|
+
const dbDeps = component["databaseDependencies"];
|
|
931
|
+
if (dbDeps === void 0 || dbDeps === null || typeof dbDeps !== "object") return false;
|
|
932
|
+
const init = dbDeps["init"];
|
|
933
|
+
if (init === void 0 || !Array.isArray(init)) return false;
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
function collectDependenciesFromFrameworkComponents(components) {
|
|
937
|
+
const dependencies = [];
|
|
938
|
+
for (const component of components) if (hasDatabaseDependenciesInit(component)) dependencies.push(...component.databaseDependencies.init);
|
|
939
|
+
return dependencies;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Renders the expected native type for a contract column, expanding parameterized types
|
|
943
|
+
* using codec control hooks when available.
|
|
944
|
+
*
|
|
945
|
+
* This function delegates to the `expandNativeType` hook if the codec provides one,
|
|
946
|
+
* ensuring that the SQL family layer remains dialect-agnostic while allowing
|
|
947
|
+
* target-specific adapters (like Postgres) to provide their own expansion logic.
|
|
948
|
+
*/
|
|
949
|
+
function renderExpectedNativeType(contractColumn, codecHooks) {
|
|
950
|
+
const { codecId, nativeType, typeParams } = contractColumn;
|
|
951
|
+
if (!typeParams || !codecId) return nativeType;
|
|
952
|
+
const hooks = codecHooks.get(codecId);
|
|
953
|
+
if (hooks?.expandNativeType) return hooks.expandNativeType({
|
|
954
|
+
nativeType,
|
|
955
|
+
codecId,
|
|
956
|
+
typeParams
|
|
957
|
+
});
|
|
958
|
+
return nativeType;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Describes a column default for display purposes.
|
|
962
|
+
*/
|
|
963
|
+
function describeColumnDefault(columnDefault) {
|
|
964
|
+
switch (columnDefault.kind) {
|
|
965
|
+
case "literal": return `literal(${formatLiteralValue(columnDefault.value)})`;
|
|
966
|
+
case "function": return columnDefault.expression;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Compares a contract ColumnDefault against a schema raw default string for semantic equality.
|
|
971
|
+
*
|
|
972
|
+
* When a normalizer is provided, the raw schema default is first normalized to a ColumnDefault
|
|
973
|
+
* before comparison. Without a normalizer, falls back to direct string comparison against
|
|
974
|
+
* the contract expression.
|
|
975
|
+
*
|
|
976
|
+
* @param contractDefault - The expected default from the contract (normalized ColumnDefault)
|
|
977
|
+
* @param schemaDefault - The raw default expression from the database (string)
|
|
978
|
+
* @param normalizer - Optional target-specific normalizer to convert raw defaults
|
|
979
|
+
* @param nativeType - The column's native type, passed to normalizer for context
|
|
980
|
+
*/
|
|
981
|
+
function columnDefaultsEqual(contractDefault, schemaDefault, normalizer, nativeType) {
|
|
982
|
+
if (!normalizer) {
|
|
983
|
+
if (contractDefault.kind === "function") return contractDefault.expression === schemaDefault;
|
|
984
|
+
const normalizedValue = normalizeLiteralValue(contractDefault.value, nativeType);
|
|
985
|
+
if (typeof normalizedValue === "string") return normalizedValue === schemaDefault || `'${normalizedValue}'` === schemaDefault;
|
|
986
|
+
return String(normalizedValue) === schemaDefault;
|
|
987
|
+
}
|
|
988
|
+
const normalizedSchema = normalizer(schemaDefault, nativeType ?? "");
|
|
989
|
+
if (!normalizedSchema) return false;
|
|
990
|
+
if (contractDefault.kind !== normalizedSchema.kind) return false;
|
|
991
|
+
if (contractDefault.kind === "literal" && normalizedSchema.kind === "literal") return literalValuesEqual(normalizeLiteralValue(contractDefault.value, nativeType), normalizeLiteralValue(normalizedSchema.value, nativeType));
|
|
992
|
+
if (contractDefault.kind === "function" && normalizedSchema.kind === "function") {
|
|
993
|
+
const normalizeExpr = (expr) => expr.toLowerCase().replace(/\s+/g, "");
|
|
994
|
+
return normalizeExpr(contractDefault.expression) === normalizeExpr(normalizedSchema.expression);
|
|
995
|
+
}
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
function isTemporalNativeType(nativeType) {
|
|
999
|
+
if (!nativeType) return false;
|
|
1000
|
+
const normalized = nativeType.toLowerCase();
|
|
1001
|
+
return normalized.includes("timestamp") || normalized === "date";
|
|
1002
|
+
}
|
|
1003
|
+
function isBigIntNativeType(nativeType) {
|
|
1004
|
+
if (!nativeType) return false;
|
|
1005
|
+
const normalized = nativeType.toLowerCase();
|
|
1006
|
+
return normalized === "bigint" || normalized === "int8";
|
|
1007
|
+
}
|
|
1008
|
+
function normalizeLiteralValue(value, nativeType) {
|
|
1009
|
+
if (value instanceof Date) return value.toISOString();
|
|
1010
|
+
if (isTaggedBigInt(value) && isBigIntNativeType(nativeType)) return value.value;
|
|
1011
|
+
if (typeof value === "bigint") return value.toString();
|
|
1012
|
+
if (typeof value === "string" && isTemporalNativeType(nativeType)) {
|
|
1013
|
+
const parsed = new Date(value);
|
|
1014
|
+
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
|
|
1015
|
+
}
|
|
1016
|
+
return value;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Recursively sorts object keys for deterministic JSON comparison.
|
|
1020
|
+
* Postgres jsonb may canonicalize key order, so two semantically equal
|
|
1021
|
+
* objects can have different key insertion order.
|
|
1022
|
+
*/
|
|
1023
|
+
function stableStringify(value) {
|
|
1024
|
+
return JSON.stringify(value, (_key, val) => {
|
|
1025
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
1026
|
+
const sorted = {};
|
|
1027
|
+
for (const k of Object.keys(val).sort()) sorted[k] = val[k];
|
|
1028
|
+
return sorted;
|
|
1029
|
+
}
|
|
1030
|
+
return val;
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
function literalValuesEqual(a, b) {
|
|
1034
|
+
if (a === b) return true;
|
|
1035
|
+
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) return stableStringify(a) === stableStringify(b);
|
|
1036
|
+
if (typeof a === "object" && a !== null && typeof b === "string") try {
|
|
1037
|
+
return stableStringify(a) === stableStringify(JSON.parse(b));
|
|
1038
|
+
} catch {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
if (typeof a === "string" && typeof b === "object" && b !== null) try {
|
|
1042
|
+
return stableStringify(JSON.parse(a)) === stableStringify(b);
|
|
1043
|
+
} catch {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
function formatLiteralValue(value) {
|
|
1049
|
+
if (value instanceof Date) return value.toISOString();
|
|
1050
|
+
if (isTaggedBigInt(value)) return value.value;
|
|
1051
|
+
if (typeof value === "bigint") return value.toString();
|
|
1052
|
+
if (typeof value === "string") return value;
|
|
1053
|
+
return JSON.stringify(value);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
//#endregion
|
|
1057
|
+
export { verifyDatabaseDependencies as a, isUniqueConstraintSatisfied as i, arraysEqual as n, isIndexSatisfied as r, verifySqlSchema as t };
|
|
1058
|
+
//# sourceMappingURL=verify-sql-schema-CpAVEi8A.mjs.map
|