@prisma-next/family-sql 0.14.0-dev.49 → 0.14.0-dev.50

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.
Files changed (46) hide show
  1. package/dist/{control-adapter-DHYFuOBy.d.mts → control-adapter-BVOj4gXI.d.mts} +34 -5
  2. package/dist/control-adapter-BVOj4gXI.d.mts.map +1 -0
  3. package/dist/control-adapter.d.mts +1 -1
  4. package/dist/control.d.mts +65 -50
  5. package/dist/control.d.mts.map +1 -1
  6. package/dist/control.mjs +308 -56
  7. package/dist/control.mjs.map +1 -1
  8. package/dist/diff.d.mts +141 -30
  9. package/dist/diff.d.mts.map +1 -1
  10. package/dist/diff.mjs +155 -1384
  11. package/dist/diff.mjs.map +1 -1
  12. package/dist/ir.d.mts +5 -5
  13. package/dist/ir.d.mts.map +1 -1
  14. package/dist/ir.mjs +2 -2
  15. package/dist/ir.mjs.map +1 -1
  16. package/dist/migration.d.mts +1 -1
  17. package/dist/schema-differ-DnoopSXm.d.mts +45 -0
  18. package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
  19. package/dist/schema-verify-W3r631Jh.mjs +226 -0
  20. package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
  21. package/dist/{sql-contract-serializer-DwUSJ4PO.mjs → sql-contract-serializer-C75cfMSS.mjs} +2 -2
  22. package/dist/{sql-contract-serializer-DwUSJ4PO.mjs.map → sql-contract-serializer-C75cfMSS.mjs.map} +1 -1
  23. package/dist/{types-DBxrwN1d.d.mts → types-BW7pzb2j.d.mts} +24 -15
  24. package/dist/types-BW7pzb2j.d.mts.map +1 -0
  25. package/package.json +21 -21
  26. package/src/core/control-instance.ts +99 -80
  27. package/src/core/control-target-descriptor.ts +29 -14
  28. package/src/core/diff/diff-tree-normalization.ts +211 -0
  29. package/src/core/diff/schema-verify.ts +324 -0
  30. package/src/core/diff/sql-schema-diff.ts +40 -1466
  31. package/src/core/diff/verifier-disposition.ts +14 -42
  32. package/src/core/ir/sql-schema-verifier-base.ts +5 -5
  33. package/src/core/migrations/contract-to-schema-ir.ts +63 -20
  34. package/src/core/migrations/native-type-expander.ts +28 -0
  35. package/src/core/migrations/schema-differ.ts +30 -29
  36. package/src/core/migrations/types.ts +23 -14
  37. package/src/exports/control.ts +6 -0
  38. package/src/exports/diff.ts +28 -8
  39. package/dist/contract-to-schema-ir-S-evq8E6.mjs +0 -264
  40. package/dist/contract-to-schema-ir-S-evq8E6.mjs.map +0 -1
  41. package/dist/control-adapter-DHYFuOBy.d.mts.map +0 -1
  42. package/dist/sql-schema-diff-6z36dZt6.d.mts +0 -105
  43. package/dist/sql-schema-diff-6z36dZt6.d.mts.map +0 -1
  44. package/dist/types-DBxrwN1d.d.mts.map +0 -1
  45. package/src/core/diff/control-verify-emit.ts +0 -46
  46. package/src/core/diff/verify-helpers.ts +0 -832
package/dist/diff.mjs CHANGED
@@ -1,93 +1,160 @@
1
- import { a as extractCodecControlHooks, i as resolveValueSetValues } from "./contract-to-schema-ir-S-evq8E6.mjs";
2
- import { blindCast } from "@prisma-next/utils/casts";
3
- import { effectiveControlPolicy } from "@prisma-next/contract/types";
4
- import { dispositionForCategory } from "@prisma-next/framework-components/control";
5
- import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
6
- import { ifDefined } from "@prisma-next/utils/defined";
7
- import { StorageTable, isStorageTypeInstance } from "@prisma-next/sql-contract/types";
8
- import { canonicalStringify } from "@prisma-next/utils/canonical-stringify";
9
- //#region src/core/diff/verifier-disposition.ts
10
- /**
11
- * Classifies the relational verifier issue kinds the SQL family emits (tables,
12
- * columns, constraints, indexes, defaults, enum types) into the target-neutral
13
- * categories the framework grades. The relational vocabulary lives here, in the
14
- * SQL domainthe framework never switches over `extra_foreign_key` and friends.
15
- */
16
- function classifySqlVerifierIssueKind(kind) {
17
- switch (kind) {
18
- case "extra_column": return "extraNestedElement";
19
- case "extra_primary_key":
20
- case "extra_foreign_key":
21
- case "extra_unique_constraint":
22
- case "extra_index":
23
- case "extra_validator":
24
- case "extra_default": return "extraAuxiliary";
25
- case "extra_table": return "extraTopLevelObject";
26
- case "missing_schema":
27
- case "missing_table":
28
- case "missing_column":
29
- case "type_missing":
30
- case "default_missing": return "declaredMissing";
31
- case "type_values_mismatch":
32
- case "enum_values_changed":
33
- case "check_mismatch": return "valueDrift";
34
- case "type_mismatch":
35
- case "nullability_mismatch":
36
- case "primary_key_mismatch":
37
- case "foreign_key_mismatch":
38
- case "unique_constraint_mismatch":
39
- case "index_mismatch":
40
- case "default_mismatch": return "declaredIncompatible";
41
- case "check_missing": return "declaredMissing";
42
- case "check_removed": return "extraAuxiliary";
1
+ import { a as computeStorageTypeVerdict, i as computeSqlDiffVerdict, n as classifyDiffSubjectGranularity, o as verifySqlSchemaByDiff, r as classifySqlDiffIssue } from "./schema-verify-W3r631Jh.mjs";
2
+ import { SqlForeignKeyIR, SqlIndexIR, SqlSchemaIR, SqlTableIR, SqlUniqueIR } from "@prisma-next/sql-schema-ir/types";
3
+ //#region src/core/diff/diff-tree-normalization.ts
4
+ /**
5
+ * ⚠️ ACCEPTED-UNDER-PROTEST DEBT DELETE IN SLICE 2.6 (`unify-unique-and-index-nodes`).
6
+ *
7
+ * A unique constraint IS a unique index. This slice modeled uniques and indexes
8
+ * as two separate node kinds (`SqlUniqueIR` vs `SqlIndexIR` — even though
9
+ * `SqlIndexIR` already carries `unique: boolean`), so the strict-by-kind differ
10
+ * cannot pair a contract unique with a live unique index. This whole file
11
+ * exists only to reconcile those two representations after the fact it
12
+ * should not exist.
13
+ *
14
+ * The fixdelete `SqlUniqueIR`, model unique enforcement as
15
+ * `SqlIndexIR { unique: true }` on both derivation and introspection, and
16
+ * delete `diff-tree-normalization.ts` — is specified in
17
+ * `projects/postgres-rls/slices/unify-unique-and-index-nodes/spec.md` and
18
+ * scheduled as the very next slice. Do NOT extend this.
19
+ */
20
+ /**
21
+ * Pre-diff tree normalization: adjustments to the actual/expected trees the
22
+ * generic differ (`diffSchemas`) runs over, so legacy semantic-equivalence
23
+ * rules (a unique constraint satisfied by a unique index, an FK schema
24
+ * segment that differs only in spelling) surface as same-kind node pairs
25
+ * instead of spurious drift.
26
+ */
27
+ function sameColumns(a, b) {
28
+ return a.length === b.length && a.every((c, i) => c === b[i]);
29
+ }
30
+ /**
31
+ * Adjusts a table pair's ACTUAL unique/index child lists so the legacy
32
+ * walk's cross-kind semantic satisfaction materializes as same-kind node
33
+ * pairs for the differ (the differ pairs strictly by id, so a `unique:`
34
+ * node can never pair with an `index:` node). Three legacy rules, ported
35
+ * from `isUniqueConstraintSatisfied` / `isIndexSatisfied` and the
36
+ * strict-extras loops of the retired relational walk:
37
+ *
38
+ * 1. A contract unique satisfied by a live unique INDEX: the actual index
39
+ * node is reclassified as a unique node (it pairs with the expected
40
+ * unique and stops being a candidate extra).
41
+ * 2. A contract index (with no type/options demands) satisfied by a live
42
+ * unique CONSTRAINT: a same-kind actual index node is synthesized so
43
+ * the expected index pairs — the unique constraint itself stays (the
44
+ * legacy strict-extras loop still reports it as an undeclared unique).
45
+ * 3. Live unique indexes are never extras in the legacy walk (its
46
+ * strict-extras loop skips `unique: true` rows), so any remaining
47
+ * actual unique-index node with no expected index counterpart is
48
+ * dropped rather than surfacing as `not-expected`.
49
+ */
50
+ function resolveSemanticSatisfaction(input) {
51
+ let actualIndexes = [...input.actualIndexes];
52
+ const actualUniques = [...input.actualUniques];
53
+ for (const expectedUnique of input.expectedUniques) {
54
+ if (actualUniques.some((u) => sameColumns(u.columns, expectedUnique.columns))) continue;
55
+ const satisfyingIndex = actualIndexes.find((idx) => idx.unique && sameColumns(idx.columns, expectedUnique.columns));
56
+ if (satisfyingIndex) {
57
+ actualIndexes = actualIndexes.filter((idx) => idx !== satisfyingIndex);
58
+ actualUniques.push(new SqlUniqueIR({
59
+ columns: satisfyingIndex.columns,
60
+ ...satisfyingIndex.name !== void 0 ? { name: satisfyingIndex.name } : {}
61
+ }));
62
+ }
43
63
  }
64
+ for (const expectedIndex of input.expectedIndexes) {
65
+ if (expectedIndex.type !== void 0 || expectedIndex.options !== void 0) continue;
66
+ if (actualIndexes.some((idx) => sameColumns(idx.columns, expectedIndex.columns))) continue;
67
+ const satisfyingUnique = actualUniques.find((u) => sameColumns(u.columns, expectedIndex.columns));
68
+ if (satisfyingUnique) actualIndexes.push(new SqlIndexIR({
69
+ columns: satisfyingUnique.columns,
70
+ unique: false
71
+ }));
72
+ }
73
+ actualIndexes = actualIndexes.filter((idx) => !idx.unique || input.expectedIndexes.some((exp) => sameColumns(exp.columns, idx.columns)));
74
+ return {
75
+ actualUniques,
76
+ actualIndexes
77
+ };
44
78
  }
45
- function verifierDisposition(controlPolicy, issueKind) {
46
- return dispositionForCategory(controlPolicy, classifySqlVerifierIssueKind(issueKind));
47
- }
48
- //#endregion
49
- //#region src/core/diff/control-verify-emit.ts
50
79
  /**
51
- * Grades `issue` under `controlPolicy` and, unless suppressed, pushes both the
52
- * issue and a status-stamped verification node. Returns the resolved outcome so
53
- * the caller never re-grades the same issue.
80
+ * Applies {@link resolveSemanticSatisfaction} across a flat table pair set:
81
+ * every actual table with an expected counterpart gets its unique/index
82
+ * child lists adjusted; unpaired tables pass through untouched.
54
83
  */
55
- function emitIssueAndNodeUnderControlPolicy(controlPolicy, issue, node, issues, nodes) {
56
- const disposition = verifierDisposition(controlPolicy, issue.kind);
57
- if (disposition === "suppress") return disposition;
58
- issues.push(issue);
59
- nodes.push({
60
- ...node,
61
- status: disposition
84
+ function normalizeFlatActualForDiff(expected, actual) {
85
+ const tables = {};
86
+ for (const [name, actualTable] of Object.entries(actual.tables)) {
87
+ const expectedTable = expected.tables[name];
88
+ if (expectedTable === void 0) {
89
+ tables[name] = actualTable;
90
+ continue;
91
+ }
92
+ const adjusted = resolveSemanticSatisfaction({
93
+ expectedUniques: expectedTable.uniques,
94
+ expectedIndexes: expectedTable.indexes,
95
+ actualUniques: actualTable.uniques,
96
+ actualIndexes: actualTable.indexes
97
+ });
98
+ tables[name] = new SqlTableIR({
99
+ name: actualTable.name,
100
+ columns: actualTable.columns,
101
+ foreignKeys: actualTable.foreignKeys,
102
+ uniques: adjusted.actualUniques,
103
+ indexes: adjusted.actualIndexes,
104
+ ...actualTable.primaryKey !== void 0 ? { primaryKey: actualTable.primaryKey } : {},
105
+ ...actualTable.annotations !== void 0 ? { annotations: actualTable.annotations } : {},
106
+ ...actualTable.checks !== void 0 ? { checks: actualTable.checks } : {}
107
+ });
108
+ }
109
+ return new SqlSchemaIR({
110
+ tables,
111
+ ...actual.annotations !== void 0 ? { annotations: actual.annotations } : {}
62
112
  });
63
- return disposition;
64
113
  }
65
114
  /**
66
- * Grades `issue` under `controlPolicy` and, unless suppressed, pushes the issue
67
- * (no verification node). Returns the resolved outcome so the caller maps it to
68
- * a node status itself without re-grading.
115
+ * Neutralizes the FK schema segment on a flat expected tree so its FK diff
116
+ * nodes pair with introspected FKs on single-schema targets: the family
117
+ * converter stamps `referencedSchema` with the contract namespace id
118
+ * verbatim (the unbound sentinel on non-namespaced targets), while a
119
+ * single-schema introspection stamps none — resolving both sides to the
120
+ * empty segment makes the ids meet.
69
121
  */
70
- function emitIssueUnderControlPolicy(controlPolicy, issue, issues) {
71
- const disposition = verifierDisposition(controlPolicy, issue.kind);
72
- if (disposition === "suppress") return disposition;
73
- issues.push(issue);
74
- return disposition;
122
+ function neutralizeFlatExpectedFkSchemas(expected) {
123
+ const tables = {};
124
+ for (const [name, table] of Object.entries(expected.tables)) {
125
+ if (table.foreignKeys.length === 0) {
126
+ tables[name] = table;
127
+ continue;
128
+ }
129
+ const foreignKeys = table.foreignKeys.map((fk) => new SqlForeignKeyIR({
130
+ columns: fk.columns,
131
+ referencedTable: fk.referencedTable,
132
+ referencedColumns: fk.referencedColumns,
133
+ ...fk.referencedSchema !== void 0 ? { referencedSchema: fk.referencedSchema } : {},
134
+ ...fk.name !== void 0 ? { name: fk.name } : {},
135
+ ...fk.onDelete !== void 0 ? { onDelete: fk.onDelete } : {},
136
+ ...fk.onUpdate !== void 0 ? { onUpdate: fk.onUpdate } : {},
137
+ ...fk.annotations !== void 0 ? { annotations: fk.annotations } : {},
138
+ resolvedReferencedNamespace: ""
139
+ }));
140
+ tables[name] = new SqlTableIR({
141
+ name: table.name,
142
+ columns: table.columns,
143
+ foreignKeys,
144
+ uniques: table.uniques,
145
+ indexes: table.indexes,
146
+ ...table.primaryKey !== void 0 ? { primaryKey: table.primaryKey } : {},
147
+ ...table.annotations !== void 0 ? { annotations: table.annotations } : {},
148
+ ...table.checks !== void 0 ? { checks: table.checks } : {}
149
+ });
150
+ }
151
+ return new SqlSchemaIR({
152
+ tables,
153
+ ...expected.annotations !== void 0 ? { annotations: expected.annotations } : {}
154
+ });
75
155
  }
76
156
  //#endregion
77
- //#region src/core/diff/verify-helpers.ts
78
- function indexOptionsLooselyEqual(a, b) {
79
- const aKeys = a ? Object.keys(a).sort() : [];
80
- const bKeys = b ? Object.keys(b).sort() : [];
81
- if (aKeys.length !== bKeys.length) return false;
82
- for (let i = 0; i < aKeys.length; i += 1) if (aKeys[i] !== bKeys[i]) return false;
83
- if (aKeys.length === 0) return true;
84
- for (const key of aKeys) if (String(a[key]) !== String(b[key])) return false;
85
- return true;
86
- }
87
- function indexExtrasMatch(contractIndex, schemaIndex) {
88
- if ((contractIndex.type ?? null) !== (schemaIndex.type ?? null)) return false;
89
- return indexOptionsLooselyEqual(contractIndex.options, schemaIndex.options);
90
- }
157
+ //#region src/core/diff/sql-schema-diff.ts
91
158
  /**
92
159
  * Compares two arrays of strings for equality (order-sensitive).
93
160
  */
@@ -97,1321 +164,25 @@ function arraysEqual(a, b) {
97
164
  return true;
98
165
  }
99
166
  /**
100
- * Checks if a unique constraint requirement is satisfied by the given columns.
101
- *
102
- * Semantic satisfaction: a unique constraint requirement can be satisfied by:
103
- * - A unique constraint with the same columns, OR
104
- * - A unique index with the same columns
105
- *
106
- * @param uniques - The unique constraints in the schema table
107
- * @param indexes - The indexes in the schema table
108
- * @param columns - The columns required by the unique constraint
109
- * @returns true if the requirement is satisfied
167
+ * Checks if a unique constraint requirement is satisfied by the given columns:
168
+ * a unique constraint with the same columns, or a unique index with the same
169
+ * columns (semantic satisfaction). Used by the planners to keep the
170
+ * "stronger satisfies weaker" behavior consistent across the control plane.
110
171
  */
111
172
  function isUniqueConstraintSatisfied(uniques, indexes, columns) {
112
173
  if (uniques.some((unique) => arraysEqual(unique.columns, columns))) return true;
113
174
  return indexes.some((index) => index.unique && arraysEqual(index.columns, columns));
114
175
  }
115
176
  /**
116
- * Checks if an index requirement is satisfied by the given columns.
117
- *
118
- * Semantic satisfaction: a non-unique index requirement can be satisfied by:
119
- * - Any index (unique or non-unique) with the same columns, OR
120
- * - A unique constraint with the same columns (stronger satisfies weaker)
121
- *
122
- * @param indexes - The indexes in the schema table
123
- * @param uniques - The unique constraints in the schema table
124
- * @param columns - The columns required by the index
125
- * @returns true if the requirement is satisfied
177
+ * Checks if an index requirement is satisfied by the given columns: any index
178
+ * (unique or non-unique) with the same columns, or a unique constraint with
179
+ * the same columns (stronger satisfies weaker).
126
180
  */
127
181
  function isIndexSatisfied(indexes, uniques, columns) {
128
182
  if (indexes.some((index) => arraysEqual(index.columns, columns))) return true;
129
183
  return uniques.some((unique) => arraysEqual(unique.columns, columns));
130
184
  }
131
- /**
132
- * Verifies primary key matches between contract and schema.
133
- * Returns 'pass' or 'fail'.
134
- *
135
- * Uses semantic satisfaction: identity is based on (table + kind + columns).
136
- * Name differences are ignored by default (names are for DDL/diagnostics, not identity).
137
- */
138
- function verifyPrimaryKey(contractPK, schemaPK, tableName, namespaceId, tableControlPolicy, issues) {
139
- if (!schemaPK) {
140
- const outcome = emitIssueUnderControlPolicy(tableControlPolicy, {
141
- kind: "primary_key_mismatch",
142
- reason: "not-equal",
143
- table: tableName,
144
- namespaceId,
145
- expected: contractPK.columns.join(", "),
146
- message: `Table "${tableName}" is missing primary key`
147
- }, issues);
148
- return outcome === "suppress" ? "pass" : outcome;
149
- }
150
- if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
151
- const outcome = emitIssueUnderControlPolicy(tableControlPolicy, {
152
- kind: "primary_key_mismatch",
153
- reason: "not-equal",
154
- table: tableName,
155
- namespaceId,
156
- expected: contractPK.columns.join(", "),
157
- actual: schemaPK.columns.join(", "),
158
- message: `Table "${tableName}" has primary key mismatch: expected columns [${contractPK.columns.join(", ")}], got [${schemaPK.columns.join(", ")}]`
159
- }, issues);
160
- return outcome === "suppress" ? "pass" : outcome;
161
- }
162
- return "pass";
163
- }
164
- /**
165
- * Verifies foreign keys match between contract and schema.
166
- * Returns verification nodes for the tree.
167
- *
168
- * Uses semantic satisfaction: identity is based on (table + columns + referenced table + referenced columns).
169
- * Name differences are ignored by default (names are for DDL/diagnostics, not identity).
170
- */
171
- function verifyForeignKeys(contractFKs, schemaFKs, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict) {
172
- const nodes = [];
173
- for (const contractFK of contractFKs) {
174
- const fkPath = `${tablePath}.foreignKeys[${contractFK.source.columns.join(",")}]`;
175
- const matchingFK = schemaFKs.find((fk) => {
176
- 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;
177
- return arraysEqual(fk.columns, contractFK.source.columns) && tablesMatch && arraysEqual(fk.referencedColumns, contractFK.target.columns);
178
- });
179
- if (!matchingFK) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
180
- kind: "foreign_key_mismatch",
181
- reason: "not-equal",
182
- table: tableName,
183
- namespaceId,
184
- expected: `${contractFK.source.columns.join(", ")} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(", ")})`,
185
- message: `Table "${tableName}" is missing foreign key: ${contractFK.source.columns.join(", ")} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(", ")})`
186
- }, {
187
- status: "fail",
188
- kind: "foreignKey",
189
- name: `foreignKey(${contractFK.source.columns.join(", ")})`,
190
- contractPath: fkPath,
191
- code: "foreign_key_mismatch",
192
- message: "Foreign key missing",
193
- expected: contractFK,
194
- actual: void 0,
195
- children: []
196
- }, issues, nodes);
197
- else {
198
- const actionMismatches = getReferentialActionMismatches(contractFK, matchingFK);
199
- if (actionMismatches.length > 0) {
200
- const combinedMessage = actionMismatches.map((m) => m.message).join("; ");
201
- const combinedExpected = actionMismatches.map((m) => m.expected).join(", ");
202
- const combinedActual = actionMismatches.map((m) => m.actual).join(", ");
203
- emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
204
- kind: "foreign_key_mismatch",
205
- reason: "not-equal",
206
- table: tableName,
207
- namespaceId,
208
- indexOrConstraint: matchingFK.name ?? `fk(${contractFK.source.columns.join(",")})`,
209
- expected: combinedExpected,
210
- actual: combinedActual,
211
- message: `Table "${tableName}" foreign key ${contractFK.source.columns.join(", ")} -> ${contractFK.target.tableName}: ${combinedMessage}`
212
- }, {
213
- status: "fail",
214
- kind: "foreignKey",
215
- name: `foreignKey(${contractFK.source.columns.join(", ")})`,
216
- contractPath: fkPath,
217
- code: "foreign_key_mismatch",
218
- message: combinedMessage,
219
- expected: contractFK,
220
- actual: matchingFK,
221
- children: []
222
- }, issues, nodes);
223
- } else nodes.push({
224
- status: "pass",
225
- kind: "foreignKey",
226
- name: `foreignKey(${contractFK.source.columns.join(", ")})`,
227
- contractPath: fkPath,
228
- code: "",
229
- message: "",
230
- expected: void 0,
231
- actual: void 0,
232
- children: []
233
- });
234
- }
235
- }
236
- if (strict) {
237
- for (const schemaFK of schemaFKs) if (!contractFKs.find((fk) => {
238
- 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;
239
- return arraysEqual(fk.source.columns, schemaFK.columns) && tablesMatch && arraysEqual(fk.target.columns, schemaFK.referencedColumns);
240
- })) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
241
- kind: "extra_foreign_key",
242
- reason: "not-expected",
243
- table: tableName,
244
- namespaceId,
245
- indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(",")})`,
246
- message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(", ")} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(", ")})`
247
- }, {
248
- status: "fail",
249
- kind: "foreignKey",
250
- name: `foreignKey(${schemaFK.columns.join(", ")})`,
251
- contractPath: `${tablePath}.foreignKeys[${schemaFK.columns.join(",")}]`,
252
- code: "extra_foreign_key",
253
- message: "Extra foreign key found",
254
- expected: void 0,
255
- actual: schemaFK,
256
- children: []
257
- }, issues, nodes);
258
- }
259
- return nodes;
260
- }
261
- /**
262
- * Verifies unique constraints match between contract and schema.
263
- * Returns verification nodes for the tree.
264
- *
265
- * Uses semantic satisfaction: identity is based on (table + kind + columns).
266
- * A unique constraint requirement can be satisfied by either:
267
- * - A unique constraint with the same columns, or
268
- * - A unique index with the same columns
269
- *
270
- * Name differences are ignored by default (names are for DDL/diagnostics, not identity).
271
- */
272
- function verifyUniqueConstraints(contractUniques, schemaUniques, schemaIndexes, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict) {
273
- const nodes = [];
274
- for (const contractUnique of contractUniques) {
275
- const uniquePath = `${tablePath}.uniques[${contractUnique.columns.join(",")}]`;
276
- const matchingUnique = schemaUniques.find((u) => arraysEqual(u.columns, contractUnique.columns));
277
- const matchingUniqueIndex = !matchingUnique && schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
278
- if (!matchingUnique && !matchingUniqueIndex) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
279
- kind: "unique_constraint_mismatch",
280
- reason: "not-equal",
281
- table: tableName,
282
- namespaceId,
283
- expected: contractUnique.columns.join(", "),
284
- message: `Table "${tableName}" is missing unique constraint: ${contractUnique.columns.join(", ")}`
285
- }, {
286
- status: "fail",
287
- kind: "unique",
288
- name: `unique(${contractUnique.columns.join(", ")})`,
289
- contractPath: uniquePath,
290
- code: "unique_constraint_mismatch",
291
- message: "Unique constraint missing",
292
- expected: contractUnique,
293
- actual: void 0,
294
- children: []
295
- }, issues, nodes);
296
- else nodes.push({
297
- status: "pass",
298
- kind: "unique",
299
- name: `unique(${contractUnique.columns.join(", ")})`,
300
- contractPath: uniquePath,
301
- code: "",
302
- message: "",
303
- expected: void 0,
304
- actual: void 0,
305
- children: []
306
- });
307
- }
308
- if (strict) {
309
- for (const schemaUnique of schemaUniques) if (!contractUniques.find((u) => arraysEqual(u.columns, schemaUnique.columns))) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
310
- kind: "extra_unique_constraint",
311
- reason: "not-expected",
312
- table: tableName,
313
- namespaceId,
314
- indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(",")})`,
315
- message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(", ")}`
316
- }, {
317
- status: "fail",
318
- kind: "unique",
319
- name: `unique(${schemaUnique.columns.join(", ")})`,
320
- contractPath: `${tablePath}.uniques[${schemaUnique.columns.join(",")}]`,
321
- code: "extra_unique_constraint",
322
- message: "Extra unique constraint found",
323
- expected: void 0,
324
- actual: schemaUnique,
325
- children: []
326
- }, issues, nodes);
327
- }
328
- return nodes;
329
- }
330
- /**
331
- * Verifies indexes match between contract and schema.
332
- * Returns verification nodes for the tree.
333
- *
334
- * Uses semantic satisfaction: identity is based on (table + kind + columns).
335
- * A non-unique index requirement can be satisfied by either:
336
- * - A non-unique index with the same columns, or
337
- * - A unique index with the same columns (stronger satisfies weaker)
338
- *
339
- * Name differences are ignored by default (names are for DDL/diagnostics, not identity).
340
- */
341
- function verifyIndexes(contractIndexes, schemaIndexes, schemaUniques, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict) {
342
- const nodes = [];
343
- for (const contractIndex of contractIndexes) {
344
- const indexPath = `${tablePath}.indexes[${contractIndex.columns.join(",")}]`;
345
- const matchingIndex = schemaIndexes.find((idx) => arraysEqual(idx.columns, contractIndex.columns) && indexExtrasMatch(contractIndex, idx));
346
- const matchingUniqueConstraint = !matchingIndex && contractIndex.type === void 0 && contractIndex.options === void 0 && schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
347
- if (!matchingIndex && !matchingUniqueConstraint) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
348
- kind: "index_mismatch",
349
- reason: "not-equal",
350
- table: tableName,
351
- namespaceId,
352
- expected: contractIndex.columns.join(", "),
353
- message: `Table "${tableName}" is missing index: ${contractIndex.columns.join(", ")}`
354
- }, {
355
- status: "fail",
356
- kind: "index",
357
- name: `index(${contractIndex.columns.join(", ")})`,
358
- contractPath: indexPath,
359
- code: "index_mismatch",
360
- message: "Index missing",
361
- expected: contractIndex,
362
- actual: void 0,
363
- children: []
364
- }, issues, nodes);
365
- else nodes.push({
366
- status: "pass",
367
- kind: "index",
368
- name: `index(${contractIndex.columns.join(", ")})`,
369
- contractPath: indexPath,
370
- code: "",
371
- message: "",
372
- expected: void 0,
373
- actual: void 0,
374
- children: []
375
- });
376
- }
377
- if (strict) for (const schemaIndex of schemaIndexes) {
378
- if (schemaIndex.unique) continue;
379
- if (!contractIndexes.find((idx) => arraysEqual(idx.columns, schemaIndex.columns) && indexExtrasMatch(idx, schemaIndex))) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
380
- kind: "extra_index",
381
- reason: "not-expected",
382
- table: tableName,
383
- namespaceId,
384
- indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(",")})`,
385
- message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(", ")}`
386
- }, {
387
- status: "fail",
388
- kind: "index",
389
- name: `index(${schemaIndex.columns.join(", ")})`,
390
- contractPath: `${tablePath}.indexes[${schemaIndex.columns.join(",")}]`,
391
- code: "extra_index",
392
- message: "Extra index found",
393
- expected: void 0,
394
- actual: schemaIndex,
395
- children: []
396
- }, issues, nodes);
397
- }
398
- return nodes;
399
- }
400
- /**
401
- * Computes counts of pass/warn/fail nodes by traversing the tree.
402
- */
403
- function computeCounts(node) {
404
- let pass = 0;
405
- let warn = 0;
406
- let fail = 0;
407
- function traverse(n) {
408
- if (n.status === "pass") pass++;
409
- else if (n.status === "warn") warn++;
410
- else if (n.status === "fail") fail++;
411
- if (n.children) for (const child of n.children) traverse(child);
412
- }
413
- traverse(node);
414
- return {
415
- pass,
416
- warn,
417
- fail,
418
- totalNodes: pass + warn + fail
419
- };
420
- }
421
- /**
422
- * Compares referential actions between a contract FK and a schema FK.
423
- * Only compares when the contract FK explicitly specifies onDelete or onUpdate.
424
- * Returns all mismatches (both onDelete and onUpdate) so both are reported at once.
425
- *
426
- * Note: 'noAction' in the contract is semantically equivalent to undefined in the
427
- * schema IR, because the introspection adapter omits 'NO ACTION' (the database default)
428
- * to keep the IR sparse. We normalize both sides before comparing.
429
- */
430
- function getReferentialActionMismatches(contractFK, schemaFK) {
431
- const mismatches = [];
432
- const contractOnDelete = normalizeReferentialAction(contractFK.onDelete);
433
- const schemaOnDelete = normalizeReferentialAction(schemaFK.onDelete);
434
- if (contractOnDelete !== void 0 && contractOnDelete !== schemaOnDelete) mismatches.push({
435
- expected: `onDelete: ${contractFK.onDelete}`,
436
- actual: `onDelete: ${schemaFK.onDelete ?? "noAction (default)"}`,
437
- message: `onDelete mismatch: expected ${contractFK.onDelete}, got ${schemaFK.onDelete ?? "noAction (default)"}`
438
- });
439
- const contractOnUpdate = normalizeReferentialAction(contractFK.onUpdate);
440
- const schemaOnUpdate = normalizeReferentialAction(schemaFK.onUpdate);
441
- if (contractOnUpdate !== void 0 && contractOnUpdate !== schemaOnUpdate) mismatches.push({
442
- expected: `onUpdate: ${contractFK.onUpdate}`,
443
- actual: `onUpdate: ${schemaFK.onUpdate ?? "noAction (default)"}`,
444
- message: `onUpdate mismatch: expected ${contractFK.onUpdate}, got ${schemaFK.onUpdate ?? "noAction (default)"}`
445
- });
446
- return mismatches;
447
- }
448
- /**
449
- * Normalizes a referential action value for comparison.
450
- * 'noAction' is the database default and equivalent to undefined (omitted) in the sparse IR.
451
- */
452
- function normalizeReferentialAction(action) {
453
- return action === "noAction" ? void 0 : action;
454
- }
455
- /**
456
- * Compares two value arrays as unordered sets.
457
- * Returns true when both sides contain exactly the same values.
458
- */
459
- function valueSetsEqual(a, b) {
460
- const aSet = new Set(a);
461
- const bSet = new Set(b);
462
- if (aSet.size !== bSet.size) return false;
463
- return [...aSet].every((v) => bSet.has(v));
464
- }
465
- /**
466
- * Verifies check constraints match between contract-projected checks and
467
- * introspected live checks.
468
- *
469
- * Comparison is value-set-based, not SQL-string-based. Postgres rewrites
470
- * `col IN ('a','b')` as `col = ANY (ARRAY['a','b'])` in
471
- * `pg_get_constraintdef`, so comparing the extracted value sets (after
472
- * the introspection adapter parses the predicate) avoids false mismatches
473
- * from the `IN`-vs-`= ANY (ARRAY…)` rendering difference.
474
- *
475
- * Issues emitted:
476
- * - `check_missing` — check expected by contract but absent from live DB
477
- * - `check_removed` — check present in live DB but not in contract
478
- * - `check_mismatch` — check present on both sides but permitted values differ
479
- *
480
- * `check_removed` is emitted only when `strict` is true so non-strict
481
- * verification (the normal path) does not complain about extra constraints.
482
- */
483
- function verifyCheckConstraints(contractChecks, schemaChecks, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict) {
484
- const nodes = [];
485
- for (const contractCheck of contractChecks) {
486
- const checkPath = `${tablePath}.checks[${contractCheck.name}]`;
487
- const liveCheck = schemaChecks.find((c) => c.name === contractCheck.name);
488
- if (!liveCheck) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
489
- kind: "check_missing",
490
- reason: "not-found",
491
- table: tableName,
492
- namespaceId,
493
- indexOrConstraint: contractCheck.name,
494
- expected: contractCheck.permittedValues.join(", "),
495
- message: `Table "${tableName}" is missing check constraint "${contractCheck.name}" (column "${contractCheck.column}" IN (${contractCheck.permittedValues.join(", ")}))`
496
- }, {
497
- status: "fail",
498
- kind: "checkConstraint",
499
- name: `check(${contractCheck.name})`,
500
- contractPath: checkPath,
501
- code: "check_missing",
502
- message: `Check constraint "${contractCheck.name}" missing`,
503
- expected: contractCheck,
504
- actual: void 0,
505
- children: []
506
- }, issues, nodes);
507
- else if (!valueSetsEqual(contractCheck.permittedValues, liveCheck.permittedValues)) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
508
- kind: "check_mismatch",
509
- reason: "not-equal",
510
- table: tableName,
511
- namespaceId,
512
- indexOrConstraint: contractCheck.name,
513
- expected: contractCheck.permittedValues.join(", "),
514
- actual: liveCheck.permittedValues.join(", "),
515
- message: `Table "${tableName}" check constraint "${contractCheck.name}" has different permitted values: expected [${contractCheck.permittedValues.join(", ")}], got [${liveCheck.permittedValues.join(", ")}]`
516
- }, {
517
- status: "fail",
518
- kind: "checkConstraint",
519
- name: `check(${contractCheck.name})`,
520
- contractPath: checkPath,
521
- code: "check_mismatch",
522
- message: `Check constraint "${contractCheck.name}" values mismatch`,
523
- expected: contractCheck,
524
- actual: liveCheck,
525
- children: []
526
- }, issues, nodes);
527
- else nodes.push({
528
- status: "pass",
529
- kind: "checkConstraint",
530
- name: `check(${contractCheck.name})`,
531
- contractPath: checkPath,
532
- code: "",
533
- message: "",
534
- expected: void 0,
535
- actual: void 0,
536
- children: []
537
- });
538
- }
539
- if (strict) {
540
- for (const liveCheck of schemaChecks) if (!contractChecks.find((c) => c.name === liveCheck.name)) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
541
- kind: "check_removed",
542
- reason: "not-equal",
543
- table: tableName,
544
- namespaceId,
545
- indexOrConstraint: liveCheck.name,
546
- actual: liveCheck.permittedValues.join(", "),
547
- message: `Table "${tableName}" has extra check constraint "${liveCheck.name}" in database (not in contract)`
548
- }, {
549
- status: "fail",
550
- kind: "checkConstraint",
551
- name: `check(${liveCheck.name})`,
552
- contractPath: `${tablePath}.checks[${liveCheck.name}]`,
553
- code: "check_removed",
554
- message: `Extra check constraint "${liveCheck.name}" found`,
555
- expected: void 0,
556
- actual: liveCheck,
557
- children: []
558
- }, issues, nodes);
559
- }
560
- return nodes;
561
- }
562
- //#endregion
563
- //#region src/core/diff/sql-schema-diff.ts
564
- /**
565
- * Returns the per-schema namespace nodes of an introspected schema node, for
566
- * the relational verify to consume one at a time. Structure-agnostic — imports
567
- * no target node class. A root exposing a `namespaces` record (Postgres) yields
568
- * its namespace nodes (never merged, so same-named tables in different schemas
569
- * cannot collide); a flat schema (SQLite) is its own single namespace and
570
- * yields itself. Handles spread-flattened input (own-enumerable fields survive).
571
- */
572
- function namespaceSchemaNodes(schema) {
573
- const obj = blindCast(schema);
574
- if (obj.namespaces !== void 0) return Object.values(obj.namespaces);
575
- return [blindCast(schema)];
576
- }
577
- /**
578
- * Verifies that a SqlSchemaIR matches a Contract.
579
- *
580
- * This is a pure function that does NOT perform any database I/O.
581
- * It takes an already-introspected schema IR and compares it against
582
- * the contract requirements.
583
- *
584
- * @param options - Verification options
585
- * @returns VerifyDatabaseSchemaResult with verification tree and issues
586
- */
587
- function verifySqlSchema(options) {
588
- const { contract, schema, strict, context, typeMetadataRegistry, normalizeDefault, normalizeNativeType } = options;
589
- const startTime = Date.now();
590
- const codecHooks = extractCodecControlHooks(options.frameworkComponents);
591
- const { contractStorageHash, contractProfileHash, contractTarget } = extractContractMetadata(contract);
592
- const { issues, rootChildren } = verifySchemaTables({
593
- contract,
594
- schema,
595
- strict,
596
- typeMetadataRegistry,
597
- codecHooks,
598
- storageTypes: contract.storage.types ?? {},
599
- ...ifDefined("normalizeDefault", normalizeDefault),
600
- ...ifDefined("normalizeNativeType", normalizeNativeType),
601
- ...ifDefined("restrictToNamespaceIds", options.restrictToNamespaceIds)
602
- });
603
- validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
604
- const typeNodes = [];
605
- const pushTypeNode = (typeName, contractPath, typeIssues, controlPolicy) => {
606
- let status = "pass";
607
- let code = "";
608
- let emitted = 0;
609
- for (const issue of typeIssues) {
610
- const disposition = verifierDisposition(controlPolicy, issue.kind);
611
- if (disposition === "suppress") continue;
612
- issues.push(issue);
613
- emitted += 1;
614
- if (code === "") code = issue.kind;
615
- if (disposition === "fail") status = "fail";
616
- else if (disposition === "warn" && status !== "fail") status = "warn";
617
- }
618
- typeNodes.push({
619
- status,
620
- kind: "storageType",
621
- name: `type ${typeName}`,
622
- contractPath,
623
- code: status === "pass" ? "" : code,
624
- message: emitted > 0 ? `${emitted} issue${emitted === 1 ? "" : "s"}` : "",
625
- expected: void 0,
626
- actual: void 0,
627
- children: []
628
- });
629
- };
630
- for (const [typeName, typeInstance] of Object.entries(contract.storage.types ?? {})) if (isStorageTypeInstance(typeInstance)) {
631
- const hook = codecHooks.get(typeInstance.codecId);
632
- pushTypeNode(typeName, `storage.types.${typeName}`, hook?.verifyType ? hook.verifyType({
633
- typeName,
634
- typeInstance,
635
- schema
636
- }) : [], effectiveControlPolicy(void 0, contract.defaultControlPolicy));
637
- }
638
- if (typeNodes.length > 0) {
639
- const typesStatus = typeNodes.some((n) => n.status === "fail") ? "fail" : typeNodes.some((n) => n.status === "warn") ? "warn" : "pass";
640
- rootChildren.push({
641
- status: typesStatus,
642
- kind: "storageTypes",
643
- name: "types",
644
- contractPath: "storage.types",
645
- code: typesStatus === "fail" ? "type_mismatch" : "",
646
- message: "",
647
- expected: void 0,
648
- actual: void 0,
649
- children: typeNodes
650
- });
651
- }
652
- const root = buildRootNode(rootChildren);
653
- const counts = computeCounts(root);
654
- const ok = counts.fail === 0;
655
- const code = ok ? void 0 : "PN-SCHEMA-0001";
656
- const summary = ok ? "Database schema satisfies contract" : `Database schema does not satisfy contract (${counts.fail} failure${counts.fail === 1 ? "" : "s"})`;
657
- const totalTime = Date.now() - startTime;
658
- return {
659
- ok,
660
- ...ifDefined("code", code),
661
- summary,
662
- contract: {
663
- storageHash: contractStorageHash,
664
- ...ifDefined("profileHash", contractProfileHash)
665
- },
666
- target: {
667
- expected: contractTarget,
668
- actual: contractTarget
669
- },
670
- schema: {
671
- issues,
672
- schemaDiffIssues: [],
673
- root,
674
- counts
675
- },
676
- meta: {
677
- strict,
678
- ...ifDefined("contractPath", context?.contractPath),
679
- ...ifDefined("configPath", context?.configPath)
680
- },
681
- timings: { total: totalTime }
682
- };
683
- }
684
- function extractContractMetadata(contract) {
685
- return {
686
- contractStorageHash: contract.storage.storageHash,
687
- contractProfileHash: "profileHash" in contract && typeof contract.profileHash === "string" ? contract.profileHash : void 0,
688
- contractTarget: contract.target
689
- };
690
- }
691
- function verifySchemaTables(options) {
692
- const { contract, schema, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType, restrictToNamespaceIds } = options;
693
- const contractDefaultControl = contract.defaultControlPolicy;
694
- const issues = [];
695
- const rootChildren = [];
696
- const schemaTables = schema.tables;
697
- const namespaceIds = Object.keys(contract.storage.namespaces).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
698
- for (const namespaceId of namespaceIds) {
699
- if (restrictToNamespaceIds !== void 0 && !restrictToNamespaceIds.has(namespaceId)) continue;
700
- const ns = contract.storage.namespaces[namespaceId];
701
- if (!ns) continue;
702
- for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table ?? {})) {
703
- StorageTable.assert(contractTableRaw, `storage.namespaces.${namespaceId}.entries.table.${tableName}`);
704
- const contractTable = contractTableRaw;
705
- const tableControlPolicy = effectiveControlPolicy(contractTable.control, contractDefaultControl);
706
- const schemaTable = schemaTables[tableName];
707
- const tablePath = `storage.namespaces.${namespaceId}.entries.table.${tableName}`;
708
- if (!schemaTable) {
709
- emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
710
- kind: "missing_table",
711
- reason: "not-found",
712
- table: tableName,
713
- namespaceId,
714
- message: `Table "${tableName}" is missing from database`
715
- }, {
716
- status: "fail",
717
- kind: "table",
718
- name: `table ${tableName}`,
719
- contractPath: tablePath,
720
- code: "missing_table",
721
- message: `Table "${tableName}" is missing`,
722
- expected: void 0,
723
- actual: void 0,
724
- children: []
725
- }, issues, rootChildren);
726
- continue;
727
- }
728
- const tableChildren = verifyTableChildren({
729
- contractTable,
730
- schemaTable,
731
- tableName,
732
- namespaceId,
733
- tablePath,
734
- tableControlPolicy,
735
- issues,
736
- strict,
737
- typeMetadataRegistry,
738
- codecHooks,
739
- storageTypes,
740
- contractStorage: contract.storage,
741
- ...ifDefined("normalizeDefault", normalizeDefault),
742
- ...ifDefined("normalizeNativeType", normalizeNativeType)
743
- });
744
- rootChildren.push(buildTableNode(tableName, tablePath, tableChildren));
745
- }
746
- }
747
- if (strict) {
748
- for (const tableName of Object.keys(schemaTables)) if (!namespaceIds.some((namespaceId) => contract.storage.namespaces[namespaceId]?.entries.table?.[tableName] !== void 0)) emitIssueAndNodeUnderControlPolicy(effectiveControlPolicy(void 0, contractDefaultControl), {
749
- kind: "extra_table",
750
- reason: "not-expected",
751
- table: tableName,
752
- message: `Extra table "${tableName}" found in database (not in contract)`
753
- }, {
754
- status: "fail",
755
- kind: "table",
756
- name: `table ${tableName}`,
757
- contractPath: `storage.namespaces.*.entries.table.${tableName}`,
758
- code: "extra_table",
759
- message: `Extra table "${tableName}" found`,
760
- expected: void 0,
761
- actual: void 0,
762
- children: [],
763
- reason: "not-expected"
764
- }, issues, rootChildren);
765
- }
766
- return {
767
- issues,
768
- rootChildren
769
- };
770
- }
771
- function verifyTableChildren(options) {
772
- const { contractTable, schemaTable, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType, contractStorage } = options;
773
- const tableChildren = [];
774
- const columnNodes = collectContractColumnNodes({
775
- contractTable,
776
- schemaTable,
777
- tableName,
778
- namespaceId,
779
- tablePath,
780
- tableControlPolicy,
781
- issues,
782
- strict,
783
- typeMetadataRegistry,
784
- codecHooks,
785
- storageTypes,
786
- ...ifDefined("normalizeDefault", normalizeDefault),
787
- ...ifDefined("normalizeNativeType", normalizeNativeType)
788
- });
789
- if (columnNodes.length > 0) tableChildren.push(buildColumnsNode(tablePath, columnNodes));
790
- if (strict) appendExtraColumnNodes({
791
- contractTable,
792
- schemaTable,
793
- tableName,
794
- namespaceId,
795
- tablePath,
796
- tableControlPolicy,
797
- issues,
798
- columnNodes
799
- });
800
- if (contractTable.primaryKey) {
801
- const pkStatus = verifyPrimaryKey(contractTable.primaryKey, schemaTable.primaryKey, tableName, namespaceId, tableControlPolicy, issues);
802
- if (pkStatus === "fail") tableChildren.push({
803
- status: "fail",
804
- kind: "primaryKey",
805
- name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
806
- contractPath: `${tablePath}.primaryKey`,
807
- code: "primary_key_mismatch",
808
- message: "Primary key mismatch",
809
- expected: contractTable.primaryKey,
810
- actual: schemaTable.primaryKey,
811
- children: []
812
- });
813
- else if (pkStatus === "warn") tableChildren.push({
814
- status: "warn",
815
- kind: "primaryKey",
816
- name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
817
- contractPath: `${tablePath}.primaryKey`,
818
- code: "primary_key_mismatch",
819
- message: "Primary key mismatch",
820
- expected: contractTable.primaryKey,
821
- actual: schemaTable.primaryKey,
822
- children: []
823
- });
824
- else tableChildren.push({
825
- status: "pass",
826
- kind: "primaryKey",
827
- name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
828
- contractPath: `${tablePath}.primaryKey`,
829
- code: "",
830
- message: "",
831
- expected: void 0,
832
- actual: void 0,
833
- children: []
834
- });
835
- } else if (schemaTable.primaryKey && strict) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
836
- kind: "extra_primary_key",
837
- reason: "not-expected",
838
- table: tableName,
839
- namespaceId,
840
- message: "Extra primary key found in database (not in contract)"
841
- }, {
842
- status: "fail",
843
- kind: "primaryKey",
844
- name: `primary key: ${schemaTable.primaryKey.columns.join(", ")}`,
845
- contractPath: `${tablePath}.primaryKey`,
846
- code: "extra_primary_key",
847
- message: "Extra primary key found",
848
- expected: void 0,
849
- actual: schemaTable.primaryKey,
850
- children: []
851
- }, issues, tableChildren);
852
- const constraintFks = contractTable.foreignKeys.filter((fk) => fk.constraint === true);
853
- if (constraintFks.length > 0 || strict) {
854
- const fkStatuses = verifyForeignKeys(constraintFks, schemaTable.foreignKeys, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict);
855
- tableChildren.push(...fkStatuses);
856
- }
857
- const uniqueStatuses = verifyUniqueConstraints(contractTable.uniques, schemaTable.uniques, schemaTable.indexes, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict);
858
- tableChildren.push(...uniqueStatuses);
859
- 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 }));
860
- const indexStatuses = verifyIndexes([...contractTable.indexes, ...fkBackingIndexes], schemaTable.indexes, schemaTable.uniques, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict);
861
- tableChildren.push(...indexStatuses);
862
- const contractCheckIRs = (contractTable.checks ?? []).map((c) => ({
863
- name: c.name,
864
- column: c.column,
865
- permittedValues: resolveValueSetValues(c.valueSet, contractStorage, `check "${c.name}"`)
866
- }));
867
- if (strict || contractCheckIRs.length > 0) {
868
- const checkStatuses = verifyCheckConstraints(contractCheckIRs, schemaTable.checks ?? [], tableName, namespaceId, tablePath, tableControlPolicy, issues, strict);
869
- tableChildren.push(...checkStatuses);
870
- }
871
- return tableChildren;
872
- }
873
- function collectContractColumnNodes(options) {
874
- const { contractTable, schemaTable, tableName, namespaceId, tablePath, tableControlPolicy, issues, strict, typeMetadataRegistry, codecHooks, storageTypes, normalizeDefault, normalizeNativeType } = options;
875
- const columnNodes = [];
876
- for (const [columnName, contractColumn] of Object.entries(contractTable.columns)) {
877
- const schemaColumn = schemaTable.columns[columnName];
878
- const columnPath = `${tablePath}.columns.${columnName}`;
879
- if (!schemaColumn) {
880
- emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
881
- kind: "missing_column",
882
- reason: "not-found",
883
- table: tableName,
884
- namespaceId,
885
- column: columnName,
886
- message: `Column "${tableName}"."${columnName}" is missing from database`
887
- }, {
888
- status: "fail",
889
- kind: "column",
890
- name: `${columnName}: missing`,
891
- contractPath: columnPath,
892
- code: "missing_column",
893
- message: `Column "${columnName}" is missing`,
894
- expected: void 0,
895
- actual: void 0,
896
- children: []
897
- }, issues, columnNodes);
898
- continue;
899
- }
900
- columnNodes.push(verifyColumn({
901
- tableName,
902
- namespaceId,
903
- columnName,
904
- contractColumn,
905
- schemaColumn,
906
- columnPath,
907
- tableControlPolicy,
908
- issues,
909
- strict,
910
- typeMetadataRegistry,
911
- codecHooks,
912
- storageTypes,
913
- ...ifDefined("normalizeDefault", normalizeDefault),
914
- ...ifDefined("normalizeNativeType", normalizeNativeType)
915
- }));
916
- }
917
- return columnNodes;
918
- }
919
- function appendExtraColumnNodes(options) {
920
- const { contractTable, schemaTable, tableName, namespaceId, tablePath, tableControlPolicy, issues, columnNodes } = options;
921
- for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) if (!contractTable.columns[columnName]) emitIssueAndNodeUnderControlPolicy(tableControlPolicy, {
922
- kind: "extra_column",
923
- reason: "not-expected",
924
- table: tableName,
925
- namespaceId,
926
- column: columnName,
927
- message: `Extra column "${tableName}"."${columnName}" found in database (not in contract)`
928
- }, {
929
- status: "fail",
930
- kind: "column",
931
- name: `${columnName}: extra`,
932
- contractPath: `${tablePath}.columns.${columnName}`,
933
- code: "extra_column",
934
- message: `Extra column "${columnName}" found`,
935
- expected: void 0,
936
- actual: nativeType,
937
- children: []
938
- }, issues, columnNodes);
939
- }
940
- function verifyColumn(options) {
941
- const { tableName, namespaceId, columnName, contractColumn, schemaColumn, columnPath, tableControlPolicy, issues, strict, codecHooks, storageTypes, normalizeDefault, normalizeNativeType } = options;
942
- const columnChildren = [];
943
- let columnStatus = "pass";
944
- const resolvedContractColumn = resolveContractColumnTypeMetadata(contractColumn, storageTypes, {
945
- tableName,
946
- columnName
947
- });
948
- const contractNativeType = renderExpectedNativeType(contractColumn, storageTypes, codecHooks, {
949
- tableName,
950
- columnName
951
- });
952
- const schemaBaseNativeType = normalizeNativeType?.(schemaColumn.nativeType) ?? schemaColumn.nativeType;
953
- const schemaNativeType = schemaColumn.many ? `${schemaBaseNativeType}[]` : schemaBaseNativeType;
954
- if (!(contractNativeType === schemaNativeType)) {
955
- const issue = {
956
- kind: "type_mismatch",
957
- reason: "not-equal",
958
- table: tableName,
959
- namespaceId,
960
- column: columnName,
961
- expected: contractNativeType,
962
- actual: schemaNativeType,
963
- message: `Column "${tableName}"."${columnName}" has type mismatch: expected "${contractNativeType}", got "${schemaNativeType}"`
964
- };
965
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
966
- if (disposition !== "suppress") {
967
- issues.push(issue);
968
- columnChildren.push({
969
- status: disposition,
970
- kind: "type",
971
- name: "type",
972
- contractPath: `${columnPath}.nativeType`,
973
- code: "type_mismatch",
974
- message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
975
- expected: contractNativeType,
976
- actual: schemaNativeType,
977
- children: []
978
- });
979
- columnStatus = disposition;
980
- }
981
- }
982
- if (resolvedContractColumn.codecId) {
983
- const typeMetadata = options.typeMetadataRegistry.get(resolvedContractColumn.codecId);
984
- if (!typeMetadata) columnChildren.push({
985
- status: "warn",
986
- kind: "type",
987
- name: "type_metadata_missing",
988
- contractPath: `${columnPath}.codecId`,
989
- code: "type_metadata_missing",
990
- message: `codecId "${resolvedContractColumn.codecId}" not found in type metadata registry`,
991
- expected: resolvedContractColumn.codecId,
992
- actual: void 0,
993
- children: []
994
- });
995
- else if (typeMetadata.nativeType && typeMetadata.nativeType !== resolvedContractColumn.nativeType) columnChildren.push({
996
- status: "warn",
997
- kind: "type",
998
- name: "type_consistency",
999
- contractPath: `${columnPath}.codecId`,
1000
- code: "type_consistency_warning",
1001
- message: `codecId "${resolvedContractColumn.codecId}" maps to nativeType "${typeMetadata.nativeType}" in registry, but contract has "${resolvedContractColumn.nativeType}"`,
1002
- expected: typeMetadata.nativeType,
1003
- actual: resolvedContractColumn.nativeType,
1004
- children: []
1005
- });
1006
- }
1007
- if (contractColumn.nullable !== schemaColumn.nullable) {
1008
- const issue = {
1009
- kind: "nullability_mismatch",
1010
- reason: "not-equal",
1011
- table: tableName,
1012
- namespaceId,
1013
- column: columnName,
1014
- expected: String(contractColumn.nullable),
1015
- actual: String(schemaColumn.nullable),
1016
- message: `Column "${tableName}"."${columnName}" has nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`
1017
- };
1018
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1019
- if (disposition !== "suppress") {
1020
- issues.push(issue);
1021
- columnChildren.push({
1022
- status: disposition,
1023
- kind: "nullability",
1024
- name: "nullability",
1025
- contractPath: `${columnPath}.nullable`,
1026
- code: "nullability_mismatch",
1027
- message: `Nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`,
1028
- expected: contractColumn.nullable,
1029
- actual: schemaColumn.nullable,
1030
- children: []
1031
- });
1032
- columnStatus = disposition;
1033
- }
1034
- }
1035
- if (contractColumn.default) {
1036
- if (!schemaColumn.default) {
1037
- const defaultDescription = describeColumnDefault(contractColumn.default);
1038
- const issue = {
1039
- kind: "default_missing",
1040
- reason: "not-found",
1041
- table: tableName,
1042
- namespaceId,
1043
- column: columnName,
1044
- expected: defaultDescription,
1045
- message: `Column "${tableName}"."${columnName}" should have default ${defaultDescription} but database has no default`
1046
- };
1047
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1048
- if (disposition !== "suppress") {
1049
- issues.push(issue);
1050
- columnChildren.push({
1051
- status: disposition,
1052
- kind: "default",
1053
- name: "default",
1054
- contractPath: `${columnPath}.default`,
1055
- code: "default_missing",
1056
- message: `Default missing: expected ${defaultDescription}`,
1057
- expected: defaultDescription,
1058
- actual: void 0,
1059
- children: []
1060
- });
1061
- columnStatus = disposition;
1062
- }
1063
- } else if (!columnDefaultsEqual(contractColumn.default, schemaColumn.default, normalizeDefault, schemaNativeType)) {
1064
- const expectedDescription = describeColumnDefault(contractColumn.default);
1065
- const actualDescription = schemaColumn.default;
1066
- const issue = {
1067
- kind: "default_mismatch",
1068
- reason: "not-equal",
1069
- table: tableName,
1070
- namespaceId,
1071
- column: columnName,
1072
- expected: expectedDescription,
1073
- actual: actualDescription,
1074
- message: `Column "${tableName}"."${columnName}" has default mismatch: expected ${expectedDescription}, got ${actualDescription}`
1075
- };
1076
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1077
- if (disposition !== "suppress") {
1078
- issues.push(issue);
1079
- columnChildren.push({
1080
- status: disposition,
1081
- kind: "default",
1082
- name: "default",
1083
- contractPath: `${columnPath}.default`,
1084
- code: "default_mismatch",
1085
- message: `Default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
1086
- expected: expectedDescription,
1087
- actual: actualDescription,
1088
- children: []
1089
- });
1090
- columnStatus = disposition;
1091
- }
1092
- }
1093
- } else if (strict && schemaColumn.default) {
1094
- const issue = {
1095
- kind: "extra_default",
1096
- reason: "not-expected",
1097
- table: tableName,
1098
- namespaceId,
1099
- column: columnName,
1100
- actual: schemaColumn.default,
1101
- message: `Column "${tableName}"."${columnName}" has default ${schemaColumn.default} in database but contract specifies no default`
1102
- };
1103
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
1104
- if (disposition !== "suppress") {
1105
- issues.push(issue);
1106
- columnChildren.push({
1107
- status: disposition,
1108
- kind: "default",
1109
- name: "default",
1110
- contractPath: `${columnPath}.default`,
1111
- code: "extra_default",
1112
- message: `Extra default: ${schemaColumn.default}`,
1113
- expected: void 0,
1114
- actual: schemaColumn.default,
1115
- children: []
1116
- });
1117
- columnStatus = disposition;
1118
- }
1119
- }
1120
- const aggregated = aggregateChildState(columnChildren, columnStatus);
1121
- const nullableText = contractColumn.nullable ? "nullable" : "not nullable";
1122
- const columnTypeDisplay = resolvedContractColumn.codecId ? `${contractNativeType} (${resolvedContractColumn.codecId})` : contractNativeType;
1123
- const columnMessage = aggregated.failureMessages.join("; ");
1124
- return {
1125
- status: aggregated.status,
1126
- kind: "column",
1127
- name: `${columnName}: ${columnTypeDisplay} (${nullableText})`,
1128
- contractPath: columnPath,
1129
- code: aggregated.firstCode,
1130
- message: columnMessage,
1131
- expected: void 0,
1132
- actual: void 0,
1133
- children: columnChildren
1134
- };
1135
- }
1136
- function buildColumnsNode(tablePath, columnNodes) {
1137
- return {
1138
- status: aggregateChildState(columnNodes, "pass").status,
1139
- kind: "columns",
1140
- name: "columns",
1141
- contractPath: `${tablePath}.columns`,
1142
- code: "",
1143
- message: "",
1144
- expected: void 0,
1145
- actual: void 0,
1146
- children: columnNodes
1147
- };
1148
- }
1149
- function buildTableNode(tableName, tablePath, tableChildren) {
1150
- const tableStatus = aggregateChildState(tableChildren, "pass").status;
1151
- const tableFailureMessages = tableChildren.filter((child) => child.status === "fail" && child.message).map((child) => child.message).filter((msg) => typeof msg === "string" && msg.length > 0);
1152
- const tableMessage = tableStatus === "fail" && tableFailureMessages.length > 0 ? `${tableFailureMessages.length} issue${tableFailureMessages.length === 1 ? "" : "s"}` : "";
1153
- const tableCode = tableStatus === "fail" && tableChildren.length > 0 && tableChildren[0] ? tableChildren[0].code : "";
1154
- return {
1155
- status: tableStatus,
1156
- kind: "table",
1157
- name: `table ${tableName}`,
1158
- contractPath: tablePath,
1159
- code: tableCode,
1160
- message: tableMessage,
1161
- expected: void 0,
1162
- actual: void 0,
1163
- children: tableChildren
1164
- };
1165
- }
1166
- function buildRootNode(rootChildren) {
1167
- return {
1168
- status: aggregateChildState(rootChildren, "pass").status,
1169
- kind: "contract",
1170
- name: "contract",
1171
- contractPath: "",
1172
- code: "",
1173
- message: "",
1174
- expected: void 0,
1175
- actual: void 0,
1176
- children: rootChildren
1177
- };
1178
- }
1179
- /**
1180
- * Aggregates status, failure messages, and code from children in a single pass.
1181
- * This is more efficient than calling separate functions that each iterate the array.
1182
- */
1183
- function aggregateChildState(children, fallback) {
1184
- let status = fallback;
1185
- const failureMessages = [];
1186
- let firstCode = "";
1187
- for (const child of children) if (child.status === "fail") {
1188
- status = "fail";
1189
- if (!firstCode) firstCode = child.code;
1190
- if (child.message && typeof child.message === "string" && child.message.length > 0) failureMessages.push(child.message);
1191
- } else if (child.status === "warn" && status !== "fail") {
1192
- status = "warn";
1193
- if (!firstCode) firstCode = child.code;
1194
- }
1195
- return {
1196
- status,
1197
- failureMessages,
1198
- firstCode
1199
- };
1200
- }
1201
- function validateFrameworkComponentsForExtensions(contract, frameworkComponents) {
1202
- const contractExtensionPacks = contract.extensionPacks ?? {};
1203
- 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.`);
1204
- }
1205
- /**
1206
- * Renders the expected native type for a contract column, expanding parameterized types
1207
- * using codec control hooks when available.
1208
- *
1209
- * This function delegates to the `expandNativeType` hook if the codec provides one,
1210
- * ensuring that the SQL family layer remains dialect-agnostic while allowing
1211
- * target-specific adapters (like Postgres) to provide their own expansion logic.
1212
- */
1213
- function renderExpectedNativeType(contractColumn, storageTypes, codecHooks, context) {
1214
- const { codecId, nativeType, typeParams } = resolveContractColumnTypeMetadata(contractColumn, storageTypes, context);
1215
- let baseType;
1216
- if (!typeParams || !codecId) baseType = nativeType;
1217
- else {
1218
- const hooks = codecHooks.get(codecId);
1219
- baseType = hooks?.expandNativeType ? hooks.expandNativeType({
1220
- nativeType,
1221
- codecId,
1222
- typeParams
1223
- }) : nativeType;
1224
- }
1225
- return contractColumn.many ? `${baseType}[]` : baseType;
1226
- }
1227
- function resolveContractColumnTypeMetadata(contractColumn, storageTypes, context) {
1228
- if (!contractColumn.typeRef) return contractColumn;
1229
- const referencedType = storageTypes[contractColumn.typeRef];
1230
- if (!referencedType) {
1231
- const columnLabel = context ? `Column "${context.tableName}"."${context.columnName}"` : "Column";
1232
- throw new Error(`${columnLabel} references storage type "${contractColumn.typeRef}" but it is not defined in storage.types.`);
1233
- }
1234
- if (isStorageTypeInstance(referencedType)) return {
1235
- codecId: referencedType.codecId,
1236
- nativeType: referencedType.nativeType,
1237
- typeParams: referencedType.typeParams
1238
- };
1239
- throw new Error(`Storage type "${contractColumn.typeRef}" has an unknown kind; expected a codec-typed StorageTypeInstance.`);
1240
- }
1241
- /**
1242
- * Describes a column default for display purposes.
1243
- */
1244
- function describeColumnDefault(columnDefault) {
1245
- switch (columnDefault.kind) {
1246
- case "literal": return `literal(${formatLiteralValue(columnDefault.value)})`;
1247
- case "function": return columnDefault.expression;
1248
- }
1249
- }
1250
- /**
1251
- * Compares a contract ColumnDefault against a schema raw default string for semantic equality.
1252
- *
1253
- * When a normalizer is provided, the raw schema default is first normalized to a ColumnDefault
1254
- * before comparison. Without a normalizer, falls back to direct string comparison against
1255
- * the contract expression.
1256
- *
1257
- * @param contractDefault - The expected default from the contract (normalized ColumnDefault)
1258
- * @param schemaDefault - The raw default expression from the database (string)
1259
- * @param normalizer - Optional target-specific normalizer to convert raw defaults
1260
- * @param nativeType - The column's native type, passed to normalizer for context
1261
- */
1262
- function columnDefaultsEqual(contractDefault, schemaDefault, normalizer, nativeType) {
1263
- if (!normalizer) {
1264
- if (contractDefault.kind === "function") return contractDefault.expression === schemaDefault;
1265
- const normalizedValue = normalizeLiteralValue(contractDefault.value, nativeType);
1266
- if (typeof normalizedValue === "string") return normalizedValue === schemaDefault || `'${normalizedValue}'` === schemaDefault;
1267
- return String(normalizedValue) === schemaDefault;
1268
- }
1269
- const normalizedSchema = normalizer(schemaDefault, nativeType ?? "");
1270
- if (!normalizedSchema) return false;
1271
- if (contractDefault.kind !== normalizedSchema.kind) return false;
1272
- if (contractDefault.kind === "literal" && normalizedSchema.kind === "literal") return literalValuesEqual(normalizeLiteralValue(contractDefault.value, nativeType), normalizeLiteralValue(normalizedSchema.value, nativeType));
1273
- if (contractDefault.kind === "function" && normalizedSchema.kind === "function") {
1274
- const normalizeExpr = (expr) => expr.toLowerCase().replace(/\s+/g, "");
1275
- return normalizeExpr(contractDefault.expression) === normalizeExpr(normalizedSchema.expression);
1276
- }
1277
- return false;
1278
- }
1279
- function isTemporalNativeType(nativeType) {
1280
- if (!nativeType) return false;
1281
- const normalized = nativeType.toLowerCase();
1282
- return normalized.includes("timestamp") || normalized === "date";
1283
- }
1284
- function normalizeLiteralValue(value, nativeType) {
1285
- if (value instanceof Date) return value.toISOString();
1286
- if (typeof value === "string" && isTemporalNativeType(nativeType)) {
1287
- const parsed = new Date(value);
1288
- if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
1289
- }
1290
- return value;
1291
- }
1292
- function literalValuesEqual(a, b) {
1293
- if (a === b) return true;
1294
- if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) return canonicalStringify(a) === canonicalStringify(b);
1295
- if (typeof a === "object" && a !== null && typeof b === "string") try {
1296
- return canonicalStringify(a) === canonicalStringify(JSON.parse(b));
1297
- } catch {
1298
- return false;
1299
- }
1300
- if (typeof a === "string" && typeof b === "object" && b !== null) try {
1301
- return canonicalStringify(JSON.parse(a)) === canonicalStringify(b);
1302
- } catch {
1303
- return false;
1304
- }
1305
- return false;
1306
- }
1307
- function formatLiteralValue(value) {
1308
- if (value instanceof Date) return value.toISOString();
1309
- if (typeof value === "string") return value;
1310
- return JSON.stringify(value);
1311
- }
1312
- /**
1313
- * Reads a namespace node's DDL schema name. Namespaced target nodes (Postgres
1314
- * `PostgresNamespaceSchemaNode`) carry `schemaName`; a flat schema (SQLite) has
1315
- * none and pairs by position as the sole namespace.
1316
- */
1317
- function namespaceSchemaName(node) {
1318
- return blindCast(node).schemaName;
1319
- }
1320
- /**
1321
- * Returns a shallow copy of `contract` exposing only the one named namespace —
1322
- * used solely to resolve that namespace's live DDL schema via the target's
1323
- * expected-tree projection (never for verification, so global value-sets it may
1324
- * reference are not consulted).
1325
- */
1326
- function scopeContractToNamespace(contract, namespaceId) {
1327
- const namespace = contract.storage.namespaces[namespaceId];
1328
- const scopedNamespaces = namespace === void 0 ? {} : { [namespaceId]: namespace };
1329
- return blindCast({
1330
- ...contract,
1331
- storage: blindCast({
1332
- ...contract.storage,
1333
- namespaces: scopedNamespaces
1334
- })
1335
- });
1336
- }
1337
- /**
1338
- * Combines two `VerifyDatabaseSchemaResult`s by concatenating issues and summing
1339
- * counts — used to fold the per-namespace pairings of a multi-schema database
1340
- * into one result. The verification-tree `root` of the first pairing is
1341
- * retained (multi-schema verify-tree shaping is future work).
1342
- */
1343
- function mergeVerifyResults(a, b) {
1344
- return {
1345
- ...a,
1346
- ok: a.ok && b.ok,
1347
- ...ifDefined("code", a.code ?? b.code),
1348
- schema: {
1349
- ...a.schema,
1350
- issues: [...a.schema.issues, ...b.schema.issues],
1351
- schemaDiffIssues: [...a.schema.schemaDiffIssues, ...b.schema.schemaDiffIssues],
1352
- counts: {
1353
- pass: a.schema.counts.pass + b.schema.counts.pass,
1354
- warn: a.schema.counts.warn + b.schema.counts.warn,
1355
- fail: a.schema.counts.fail + b.schema.counts.fail,
1356
- totalNodes: a.schema.counts.totalNodes + b.schema.counts.totalNodes
1357
- }
1358
- }
1359
- };
1360
- }
1361
- /**
1362
- * The single per-namespace-paired relational verify shared by the migration
1363
- * planner and the family schema verify — there is exactly one such operation.
1364
- *
1365
- * Each contract namespace is paired to the introspected namespace node holding
1366
- * the same DDL schema, then `verifySqlSchema` checks that namespace's tables
1367
- * against the matching actual node (a contract table under `auth` is only ever
1368
- * looked up in the `auth` actual node, so a multi-schema database no longer
1369
- * reports tables in other schemas as missing). The full contract is passed every
1370
- * time — `restrictToNamespaceIds` scopes only which tables are checked, so
1371
- * cross-namespace value-set / control-policy resolution is unaffected.
1372
- *
1373
- * The DDL schema of each contract namespace is read from a single-namespace
1374
- * expected projection (`buildExpectedSchema`), which both callers already build
1375
- * the same way. Empty contract namespaces verify nothing and are skipped.
1376
- * Single-schema (one namespace) and SQLite's flat schema are one pairing —
1377
- * byte-identical to the prior per-node verify.
1378
- */
1379
- function verifySqlSchemaTree(options) {
1380
- const baseOptions = {
1381
- contract: options.contract,
1382
- strict: options.strict,
1383
- typeMetadataRegistry: options.typeMetadataRegistry,
1384
- frameworkComponents: options.frameworkComponents,
1385
- ...ifDefined("normalizeDefault", options.normalizeDefault),
1386
- ...ifDefined("normalizeNativeType", options.normalizeNativeType)
1387
- };
1388
- const actualNodes = namespaceSchemaNodes(options.actualSchema);
1389
- const actualByName = /* @__PURE__ */ new Map();
1390
- for (const node of actualNodes) {
1391
- const name = namespaceSchemaName(node);
1392
- if (name !== void 0) actualByName.set(name, node);
1393
- }
1394
- const soleFlatActual = actualByName.size === 0 ? actualNodes[0] : void 0;
1395
- const emptyNamespace = { tables: {} };
1396
- let combined;
1397
- for (const namespaceId of Object.keys(options.contract.storage.namespaces)) {
1398
- const namespace = options.contract.storage.namespaces[namespaceId];
1399
- if (!namespace || Object.keys(namespace.entries.table ?? {}).length === 0) continue;
1400
- const ddlSchema = namespaceSchemaNodes(options.buildExpectedSchema(scopeContractToNamespace(options.contract, namespaceId))).map(namespaceSchemaName).find((name) => name !== void 0);
1401
- const actualNode = (ddlSchema !== void 0 ? actualByName.get(ddlSchema) : soleFlatActual) ?? emptyNamespace;
1402
- const result = verifySqlSchema({
1403
- ...baseOptions,
1404
- schema: actualNode,
1405
- restrictToNamespaceIds: new Set([namespaceId])
1406
- });
1407
- combined = combined === void 0 ? result : mergeVerifyResults(combined, result);
1408
- }
1409
- return combined ?? verifySqlSchema({
1410
- ...baseOptions,
1411
- schema: emptyNamespace
1412
- });
1413
- }
1414
185
  //#endregion
1415
- export { arraysEqual, isIndexSatisfied, isUniqueConstraintSatisfied, verifySqlSchema, verifySqlSchemaTree };
186
+ export { arraysEqual, classifyDiffSubjectGranularity, classifySqlDiffIssue, computeSqlDiffVerdict, computeStorageTypeVerdict, isIndexSatisfied, isUniqueConstraintSatisfied, neutralizeFlatExpectedFkSchemas, normalizeFlatActualForDiff, resolveSemanticSatisfaction, verifySqlSchemaByDiff };
1416
187
 
1417
188
  //# sourceMappingURL=diff.mjs.map