@prisma-next/family-sql 0.14.0-dev.5 → 0.14.0-dev.51

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 (79) hide show
  1. package/dist/{authoring-type-constructors-CjFfO6LM.mjs → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
  2. package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
  3. package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-BVOj4gXI.d.mts} +42 -8
  4. package/dist/control-adapter-BVOj4gXI.d.mts.map +1 -0
  5. package/dist/control-adapter.d.mts +1 -1
  6. package/dist/control.d.mts +238 -6
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +336 -942
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/diff.d.mts +150 -0
  11. package/dist/diff.d.mts.map +1 -0
  12. package/dist/diff.mjs +188 -0
  13. package/dist/diff.mjs.map +1 -0
  14. package/dist/ir.d.mts +26 -9
  15. package/dist/ir.d.mts.map +1 -1
  16. package/dist/ir.mjs +2 -2
  17. package/dist/ir.mjs.map +1 -1
  18. package/dist/migration.d.mts +11 -2
  19. package/dist/migration.d.mts.map +1 -1
  20. package/dist/migration.mjs +7 -0
  21. package/dist/migration.mjs.map +1 -1
  22. package/dist/pack.mjs +1 -1
  23. package/dist/psl-infer.d.mts +101 -0
  24. package/dist/psl-infer.d.mts.map +1 -0
  25. package/dist/psl-infer.mjs +342 -0
  26. package/dist/psl-infer.mjs.map +1 -0
  27. package/dist/schema-differ-DnoopSXm.d.mts +45 -0
  28. package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
  29. package/dist/schema-verify-W3r631Jh.mjs +226 -0
  30. package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
  31. package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-C75cfMSS.mjs} +46 -20
  32. package/dist/sql-contract-serializer-C75cfMSS.mjs.map +1 -0
  33. package/dist/{types-kgstZ_Zd.d.mts → types-BW7pzb2j.d.mts} +34 -139
  34. package/dist/types-BW7pzb2j.d.mts.map +1 -0
  35. package/package.json +23 -23
  36. package/src/core/authoring-entity-types.ts +12 -37
  37. package/src/core/control-adapter.ts +10 -5
  38. package/src/core/control-instance.ts +210 -42
  39. package/src/core/control-target-descriptor.ts +98 -0
  40. package/src/core/diff/diff-tree-normalization.ts +211 -0
  41. package/src/core/diff/schema-verify.ts +324 -0
  42. package/src/core/diff/sql-schema-diff.ts +77 -0
  43. package/src/core/diff/verifier-disposition.ts +30 -0
  44. package/src/core/ir/sql-contract-serializer-base.ts +70 -56
  45. package/src/core/ir/sql-contract-serializer.ts +5 -7
  46. package/src/core/ir/sql-schema-verifier-base.ts +5 -5
  47. package/src/core/migrations/contract-to-schema-ir.ts +110 -30
  48. package/src/core/migrations/field-event-planner.ts +2 -2
  49. package/src/core/migrations/native-type-expander.ts +28 -0
  50. package/src/core/migrations/schema-differ.ts +41 -0
  51. package/src/core/migrations/types.ts +31 -34
  52. package/src/core/psl-contract-infer/printer-config.ts +12 -6
  53. package/src/core/psl-contract-infer/relation-inference.ts +9 -1
  54. package/src/core/sql-migration.ts +12 -1
  55. package/src/exports/control.ts +11 -1
  56. package/src/exports/diff.ts +38 -0
  57. package/src/exports/psl-infer.ts +39 -0
  58. package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
  59. package/dist/control-adapter-Cmw9LvEP.d.mts.map +0 -1
  60. package/dist/schema-verify.d.mts +0 -39
  61. package/dist/schema-verify.d.mts.map +0 -1
  62. package/dist/schema-verify.mjs +0 -2
  63. package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
  64. package/dist/test-utils.d.mts +0 -2
  65. package/dist/test-utils.mjs +0 -2
  66. package/dist/types-kgstZ_Zd.d.mts.map +0 -1
  67. package/dist/verify-sql-schema-thU-jKpf.d.mts +0 -66
  68. package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
  69. package/dist/verify-sql-schema-xT4udQLQ.mjs +0 -1501
  70. package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
  71. package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
  72. package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
  73. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
  74. package/src/core/schema-verify/control-verify-emit.ts +0 -46
  75. package/src/core/schema-verify/verifier-disposition.ts +0 -58
  76. package/src/core/schema-verify/verify-helpers.ts +0 -820
  77. package/src/core/schema-verify/verify-sql-schema.ts +0 -1311
  78. package/src/exports/schema-verify.ts +0 -18
  79. package/src/exports/test-utils.ts +0 -9
@@ -0,0 +1,211 @@
1
+ /**
2
+ * ⚠️ ACCEPTED-UNDER-PROTEST DEBT — DELETE IN SLICE 2.6 (`unify-unique-and-index-nodes`).
3
+ *
4
+ * A unique constraint IS a unique index. This slice modeled uniques and indexes
5
+ * as two separate node kinds (`SqlUniqueIR` vs `SqlIndexIR` — even though
6
+ * `SqlIndexIR` already carries `unique: boolean`), so the strict-by-kind differ
7
+ * cannot pair a contract unique with a live unique index. This whole file
8
+ * exists only to reconcile those two representations after the fact — it
9
+ * should not exist.
10
+ *
11
+ * The fix — delete `SqlUniqueIR`, model unique enforcement as
12
+ * `SqlIndexIR { unique: true }` on both derivation and introspection, and
13
+ * delete `diff-tree-normalization.ts` — is specified in
14
+ * `projects/postgres-rls/slices/unify-unique-and-index-nodes/spec.md` and
15
+ * scheduled as the very next slice. Do NOT extend this.
16
+ */
17
+
18
+ /**
19
+ * Pre-diff tree normalization: adjustments to the actual/expected trees the
20
+ * generic differ (`diffSchemas`) runs over, so legacy semantic-equivalence
21
+ * rules (a unique constraint satisfied by a unique index, an FK schema
22
+ * segment that differs only in spelling) surface as same-kind node pairs
23
+ * instead of spurious drift.
24
+ */
25
+
26
+ import {
27
+ SqlForeignKeyIR,
28
+ SqlIndexIR,
29
+ SqlSchemaIR,
30
+ SqlTableIR,
31
+ SqlUniqueIR,
32
+ } from '@prisma-next/sql-schema-ir/types';
33
+
34
+ // ============================================================================
35
+ // Semantic satisfaction — derivation-side normalization of the actual tree
36
+ // ============================================================================
37
+
38
+ export interface SemanticSatisfactionInput {
39
+ readonly expectedUniques: readonly SqlUniqueIR[];
40
+ readonly expectedIndexes: readonly SqlIndexIR[];
41
+ readonly actualUniques: readonly SqlUniqueIR[];
42
+ readonly actualIndexes: readonly SqlIndexIR[];
43
+ }
44
+
45
+ export interface SemanticSatisfactionResult {
46
+ readonly actualUniques: readonly SqlUniqueIR[];
47
+ readonly actualIndexes: readonly SqlIndexIR[];
48
+ }
49
+
50
+ function sameColumns(a: readonly string[], b: readonly string[]): boolean {
51
+ return a.length === b.length && a.every((c, i) => c === b[i]);
52
+ }
53
+
54
+ /**
55
+ * Adjusts a table pair's ACTUAL unique/index child lists so the legacy
56
+ * walk's cross-kind semantic satisfaction materializes as same-kind node
57
+ * pairs for the differ (the differ pairs strictly by id, so a `unique:`
58
+ * node can never pair with an `index:` node). Three legacy rules, ported
59
+ * from `isUniqueConstraintSatisfied` / `isIndexSatisfied` and the
60
+ * strict-extras loops of the retired relational walk:
61
+ *
62
+ * 1. A contract unique satisfied by a live unique INDEX: the actual index
63
+ * node is reclassified as a unique node (it pairs with the expected
64
+ * unique and stops being a candidate extra).
65
+ * 2. A contract index (with no type/options demands) satisfied by a live
66
+ * unique CONSTRAINT: a same-kind actual index node is synthesized so
67
+ * the expected index pairs — the unique constraint itself stays (the
68
+ * legacy strict-extras loop still reports it as an undeclared unique).
69
+ * 3. Live unique indexes are never extras in the legacy walk (its
70
+ * strict-extras loop skips `unique: true` rows), so any remaining
71
+ * actual unique-index node with no expected index counterpart is
72
+ * dropped rather than surfacing as `not-expected`.
73
+ */
74
+ export function resolveSemanticSatisfaction(
75
+ input: SemanticSatisfactionInput,
76
+ ): SemanticSatisfactionResult {
77
+ let actualIndexes = [...input.actualIndexes];
78
+ const actualUniques = [...input.actualUniques];
79
+
80
+ // Rule 1: reclassify a satisfying unique index as the unique constraint
81
+ // the contract declared.
82
+ for (const expectedUnique of input.expectedUniques) {
83
+ const alreadyPaired = actualUniques.some((u) => sameColumns(u.columns, expectedUnique.columns));
84
+ if (alreadyPaired) continue;
85
+ const satisfyingIndex = actualIndexes.find(
86
+ (idx) => idx.unique && sameColumns(idx.columns, expectedUnique.columns),
87
+ );
88
+ if (satisfyingIndex) {
89
+ actualIndexes = actualIndexes.filter((idx) => idx !== satisfyingIndex);
90
+ actualUniques.push(
91
+ new SqlUniqueIR({
92
+ columns: satisfyingIndex.columns,
93
+ ...(satisfyingIndex.name !== undefined ? { name: satisfyingIndex.name } : {}),
94
+ }),
95
+ );
96
+ }
97
+ }
98
+
99
+ // Rule 2: synthesize an index node from a satisfying unique constraint.
100
+ for (const expectedIndex of input.expectedIndexes) {
101
+ if (expectedIndex.type !== undefined || expectedIndex.options !== undefined) continue;
102
+ const alreadyPaired = actualIndexes.some((idx) =>
103
+ sameColumns(idx.columns, expectedIndex.columns),
104
+ );
105
+ if (alreadyPaired) continue;
106
+ const satisfyingUnique = actualUniques.find((u) =>
107
+ sameColumns(u.columns, expectedIndex.columns),
108
+ );
109
+ if (satisfyingUnique) {
110
+ actualIndexes.push(new SqlIndexIR({ columns: satisfyingUnique.columns, unique: false }));
111
+ }
112
+ }
113
+
114
+ // Rule 3: remaining unique indexes with no expected counterpart are
115
+ // invisible to the legacy extras loop — drop them.
116
+ actualIndexes = actualIndexes.filter(
117
+ (idx) =>
118
+ !idx.unique || input.expectedIndexes.some((exp) => sameColumns(exp.columns, idx.columns)),
119
+ );
120
+
121
+ return { actualUniques, actualIndexes };
122
+ }
123
+
124
+ // ============================================================================
125
+ // Flat-tree helpers (single-schema targets)
126
+ // ============================================================================
127
+
128
+ /**
129
+ * Applies {@link resolveSemanticSatisfaction} across a flat table pair set:
130
+ * every actual table with an expected counterpart gets its unique/index
131
+ * child lists adjusted; unpaired tables pass through untouched.
132
+ */
133
+ export function normalizeFlatActualForDiff(
134
+ expected: SqlSchemaIR,
135
+ actual: SqlSchemaIR,
136
+ ): SqlSchemaIR {
137
+ const tables: Record<string, SqlTableIR> = {};
138
+ for (const [name, actualTable] of Object.entries(actual.tables)) {
139
+ const expectedTable = expected.tables[name];
140
+ if (expectedTable === undefined) {
141
+ tables[name] = actualTable;
142
+ continue;
143
+ }
144
+ const adjusted = resolveSemanticSatisfaction({
145
+ expectedUniques: expectedTable.uniques,
146
+ expectedIndexes: expectedTable.indexes,
147
+ actualUniques: actualTable.uniques,
148
+ actualIndexes: actualTable.indexes,
149
+ });
150
+ tables[name] = new SqlTableIR({
151
+ name: actualTable.name,
152
+ columns: actualTable.columns,
153
+ foreignKeys: actualTable.foreignKeys,
154
+ uniques: adjusted.actualUniques,
155
+ indexes: adjusted.actualIndexes,
156
+ ...(actualTable.primaryKey !== undefined ? { primaryKey: actualTable.primaryKey } : {}),
157
+ ...(actualTable.annotations !== undefined ? { annotations: actualTable.annotations } : {}),
158
+ ...(actualTable.checks !== undefined ? { checks: actualTable.checks } : {}),
159
+ });
160
+ }
161
+ return new SqlSchemaIR({
162
+ tables,
163
+ ...(actual.annotations !== undefined ? { annotations: actual.annotations } : {}),
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Neutralizes the FK schema segment on a flat expected tree so its FK diff
169
+ * nodes pair with introspected FKs on single-schema targets: the family
170
+ * converter stamps `referencedSchema` with the contract namespace id
171
+ * verbatim (the unbound sentinel on non-namespaced targets), while a
172
+ * single-schema introspection stamps none — resolving both sides to the
173
+ * empty segment makes the ids meet.
174
+ */
175
+ export function neutralizeFlatExpectedFkSchemas(expected: SqlSchemaIR): SqlSchemaIR {
176
+ const tables: Record<string, SqlTableIR> = {};
177
+ for (const [name, table] of Object.entries(expected.tables)) {
178
+ if (table.foreignKeys.length === 0) {
179
+ tables[name] = table;
180
+ continue;
181
+ }
182
+ const foreignKeys = table.foreignKeys.map(
183
+ (fk) =>
184
+ new SqlForeignKeyIR({
185
+ columns: fk.columns,
186
+ referencedTable: fk.referencedTable,
187
+ referencedColumns: fk.referencedColumns,
188
+ ...(fk.referencedSchema !== undefined ? { referencedSchema: fk.referencedSchema } : {}),
189
+ ...(fk.name !== undefined ? { name: fk.name } : {}),
190
+ ...(fk.onDelete !== undefined ? { onDelete: fk.onDelete } : {}),
191
+ ...(fk.onUpdate !== undefined ? { onUpdate: fk.onUpdate } : {}),
192
+ ...(fk.annotations !== undefined ? { annotations: fk.annotations } : {}),
193
+ resolvedReferencedNamespace: '',
194
+ }),
195
+ );
196
+ tables[name] = new SqlTableIR({
197
+ name: table.name,
198
+ columns: table.columns,
199
+ foreignKeys,
200
+ uniques: table.uniques,
201
+ indexes: table.indexes,
202
+ ...(table.primaryKey !== undefined ? { primaryKey: table.primaryKey } : {}),
203
+ ...(table.annotations !== undefined ? { annotations: table.annotations } : {}),
204
+ ...(table.checks !== undefined ? { checks: table.checks } : {}),
205
+ });
206
+ }
207
+ return new SqlSchemaIR({
208
+ tables,
209
+ ...(expected.annotations !== undefined ? { annotations: expected.annotations } : {}),
210
+ });
211
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * The differ-based SQL schema verify: post-diff filters and verdict.
3
+ *
4
+ * The generic node differ (`diffSchemas`) reports every node-level
5
+ * difference between the derived expected tree and the introspected actual
6
+ * tree. This module is the consumer side the spec assigns to the SQL
7
+ * family: strict-mode extras gating and control-policy disposition are
8
+ * reason/kind-keyed filters applied AFTER the diff — never inside it — and
9
+ * the verify verdict derives from the filtered issue list.
10
+ *
11
+ * `verifySqlSchemaByDiff` wraps the verdict in the issue-based result
12
+ * envelope — this is THE SQL schema verify (the legacy relational walk and
13
+ * its verification tree are retired).
14
+ */
15
+
16
+ import type { Contract, ControlPolicy } from '@prisma-next/contract/types';
17
+ import { effectiveControlPolicy } from '@prisma-next/contract/types';
18
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
19
+ import type {
20
+ DiffSubjectGranularity,
21
+ SchemaDiffIssue,
22
+ VerifierIssueCategory,
23
+ VerifierOutcome,
24
+ VerifyDatabaseSchemaResult,
25
+ } from '@prisma-next/framework-components/control';
26
+ import { dispositionForCategory } from '@prisma-next/framework-components/control';
27
+ import { isStorageTypeInstance, type SqlStorage } from '@prisma-next/sql-contract/types';
28
+ import { RelationalSchemaNodeKind, type SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
29
+ import { blindCast } from '@prisma-next/utils/casts';
30
+ import { ifDefined } from '@prisma-next/utils/defined';
31
+ import { extractCodecControlHooks } from '../assembly';
32
+ import type { SqlSchemaDiffFn } from '../migrations/schema-differ';
33
+ import type { CodecControlHooks } from '../migrations/types';
34
+ import { verifierDisposition } from './verifier-disposition';
35
+
36
+ // ============================================================================
37
+ // Subject-granularity classification — nodeKind → framework-neutral granularity
38
+ // ============================================================================
39
+
40
+ function issueNode(issue: SchemaDiffIssue): SqlSchemaIRNode | undefined {
41
+ const node = issue.expected ?? issue.actual;
42
+ if (node === undefined) return undefined;
43
+ return blindCast<
44
+ SqlSchemaIRNode,
45
+ 'every node in a SQL schema diff tree is a SqlSchemaIRNode; nodeKind is its identity'
46
+ >(node);
47
+ }
48
+
49
+ /**
50
+ * Resolves an issue's framework-neutral {@link DiffSubjectGranularity} on
51
+ * demand, from the issue's node's `nodeKind` via the target-provided
52
+ * `granularityOf` map. The node carries only its `nodeKind` identity, never a
53
+ * classification, and nothing is stamped onto the issue — every consumer
54
+ * (the family verdict below, the framework aggregate's unclaimed-elements
55
+ * sweep via {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable})
56
+ * calls this the same way, resolved by the family/target that owns the node
57
+ * vocabulary. `undefined` for an issue with no node.
58
+ */
59
+ export function classifyDiffSubjectGranularity(
60
+ issue: SchemaDiffIssue,
61
+ granularityOf: (nodeKind: string) => DiffSubjectGranularity,
62
+ ): DiffSubjectGranularity | undefined {
63
+ const node = issueNode(issue);
64
+ return node === undefined ? undefined : granularityOf(node.nodeKind);
65
+ }
66
+
67
+ /**
68
+ * Resolves an issue's storage `entityKind` on demand, from the issue's
69
+ * node's `nodeKind` via the target-provided `entityKindOf` map — the sibling
70
+ * of {@link classifyDiffSubjectGranularity}, called the same way by the same
71
+ * consumers (via
72
+ * {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}).
73
+ * `undefined` for an issue with no node, or for a node kind with no storage
74
+ * entity of its own.
75
+ */
76
+ export function classifyDiffEntityKind(
77
+ issue: SchemaDiffIssue,
78
+ entityKindOf: (nodeKind: string) => string | undefined,
79
+ ): string | undefined {
80
+ const node = issueNode(issue);
81
+ return node === undefined ? undefined : entityKindOf(node.nodeKind);
82
+ }
83
+
84
+ // ============================================================================
85
+ // Issue classification — subject granularity + reason → target-neutral category
86
+ // ============================================================================
87
+
88
+ /**
89
+ * Re-keys the legacy `classifySqlVerifierIssueKind` category mapping on the
90
+ * issue's {@link DiffSubjectGranularity} (resolved via `granularityOf`) + the
91
+ * issue reason. The vocabulary maps one-to-one: an undeclared live entity or
92
+ * namespace is `extraTopLevelObject`, an undeclared live field
93
+ * `extraNestedElement`, undeclared auxiliaries (constraints, indexes,
94
+ * defaults) and structural leaves (policies) `extraAuxiliary`; a value-set
95
+ * drift on a check node is `valueDrift`; every other paired divergence is
96
+ * `declaredIncompatible`; anything the database lacks is `declaredMissing`.
97
+ * `granularityOf` is the target's classifier, so target and extension node
98
+ * kinds classify without the family importing them.
99
+ */
100
+ export function classifySqlDiffIssue(
101
+ issue: SchemaDiffIssue,
102
+ granularityOf: (nodeKind: string) => DiffSubjectGranularity,
103
+ ): VerifierIssueCategory {
104
+ if (issue.reason === 'not-found') {
105
+ return 'declaredMissing';
106
+ }
107
+ if (issue.reason === 'not-expected') {
108
+ const granularity = classifyDiffSubjectGranularity(issue, granularityOf);
109
+ if (granularity === 'entity' || granularity === 'namespace') {
110
+ return 'extraTopLevelObject';
111
+ }
112
+ if (granularity === 'field') {
113
+ return 'extraNestedElement';
114
+ }
115
+ return 'extraAuxiliary';
116
+ }
117
+ if (issueNode(issue)?.nodeKind === RelationalSchemaNodeKind.check) {
118
+ return 'valueDrift';
119
+ }
120
+ return 'declaredIncompatible';
121
+ }
122
+
123
+ /**
124
+ * Whether a `not-expected` issue is a strict-mode-only finding. The legacy
125
+ * walk detected every relational extra (namespaces, entities, fields, and
126
+ * their auxiliaries) only under `--strict`; the structural diff (roots, RLS
127
+ * policies, roles) was never strict-gated — its extras fail in both modes.
128
+ * Keyed on the issue's granularity, resolved via `granularityOf`.
129
+ */
130
+ function isStrictOnlyExtra(
131
+ issue: SchemaDiffIssue,
132
+ granularityOf: (nodeKind: string) => DiffSubjectGranularity,
133
+ ): boolean {
134
+ const granularity = classifyDiffSubjectGranularity(issue, granularityOf);
135
+ return (
136
+ granularity === 'namespace' ||
137
+ granularity === 'entity' ||
138
+ granularity === 'field' ||
139
+ granularity === 'auxiliary'
140
+ );
141
+ }
142
+
143
+ // ============================================================================
144
+ // The post-diff filter + verdict
145
+ // ============================================================================
146
+
147
+ export interface SqlDiffVerdictInput {
148
+ /** The full, ownership-scoped diff issue list from the target's differ. */
149
+ readonly issues: readonly SchemaDiffIssue[];
150
+ /** Resolves a diff issue's subject table's declared control policy directly from the contract. */
151
+ readonly resolveControlPolicy: (issue: SchemaDiffIssue) => ControlPolicy | undefined;
152
+ readonly strict: boolean;
153
+ readonly defaultControlPolicy: ControlPolicy | undefined;
154
+ /** The target's classifier: a diff issue node's `nodeKind` → its subject granularity. */
155
+ readonly granularityOf: (nodeKind: string) => DiffSubjectGranularity;
156
+ }
157
+
158
+ export interface SqlDiffVerdict {
159
+ readonly failures: readonly SchemaDiffIssue[];
160
+ readonly warnings: readonly SchemaDiffIssue[];
161
+ }
162
+
163
+ /**
164
+ * Applies the two consumer filters to a diff issue list: strict gating
165
+ * (relational `not-expected` findings drop in lenient mode) and
166
+ * control-policy disposition (each surviving issue grades against its
167
+ * subject table's effective policy; suppressed issues drop, `observed`
168
+ * subjects warn). The verify verdict is `failures.length === 0`.
169
+ */
170
+ export function computeSqlDiffVerdict(input: SqlDiffVerdictInput): SqlDiffVerdict {
171
+ const failures: SchemaDiffIssue[] = [];
172
+ const warnings: SchemaDiffIssue[] = [];
173
+ for (const issue of input.issues) {
174
+ if (
175
+ !input.strict &&
176
+ issue.reason === 'not-expected' &&
177
+ isStrictOnlyExtra(issue, input.granularityOf)
178
+ ) {
179
+ continue;
180
+ }
181
+ const tablePolicy = input.resolveControlPolicy(issue);
182
+ const policy = effectiveControlPolicy(tablePolicy, input.defaultControlPolicy);
183
+ const disposition: VerifierOutcome = dispositionForCategory(
184
+ policy,
185
+ classifySqlDiffIssue(issue, input.granularityOf),
186
+ );
187
+ if (disposition === 'suppress') continue;
188
+ if (disposition === 'warn') {
189
+ warnings.push(issue);
190
+ continue;
191
+ }
192
+ failures.push(issue);
193
+ }
194
+ return { failures, warnings };
195
+ }
196
+
197
+ // ============================================================================
198
+ // Storage-types check — the codec verifyType hook path
199
+ // ============================================================================
200
+
201
+ export interface StorageTypeVerdictInput {
202
+ readonly contract: Contract<SqlStorage>;
203
+ /**
204
+ * Expected/actual namespace-node pairs the target's differ input produced:
205
+ * for a namespaced tree, one entry per expected namespace with a
206
+ * non-empty table set, paired by DDL schema name (absent actual side for
207
+ * a schema the database lacks); a flat tree is the sole pair.
208
+ */
209
+ readonly namespacePairs: ReadonlyArray<{
210
+ readonly actual: SqlSchemaIRNode | undefined;
211
+ }>;
212
+ readonly codecHooks: ReadonlyMap<string, CodecControlHooks>;
213
+ }
214
+
215
+ /**
216
+ * Runs the codec `verifyType` hooks the way the legacy walk did: once per
217
+ * contract namespace with tables, against that namespace's paired actual
218
+ * node (the hook reads namespace-scoped state such as
219
+ * `nativeEnumTypeNames` off it). Issue dispositions grade against the
220
+ * contract default policy, matching the legacy `pushTypeNode` semantics.
221
+ */
222
+ export interface StorageTypeVerdict {
223
+ readonly failures: readonly SchemaDiffIssue[];
224
+ readonly warnings: readonly SchemaDiffIssue[];
225
+ }
226
+
227
+ export function computeStorageTypeVerdict(input: StorageTypeVerdictInput): StorageTypeVerdict {
228
+ const failures: SchemaDiffIssue[] = [];
229
+ const warnings: SchemaDiffIssue[] = [];
230
+ const policy = effectiveControlPolicy(undefined, input.contract.defaultControlPolicy);
231
+ for (const pair of input.namespacePairs) {
232
+ if (pair.actual === undefined) continue;
233
+ for (const [typeName, typeInstance] of Object.entries(input.contract.storage.types ?? {})) {
234
+ if (!isStorageTypeInstance(typeInstance)) continue;
235
+ const hook = input.codecHooks.get(typeInstance.codecId);
236
+ if (!hook?.verifyType) continue;
237
+ const typeIssues = hook.verifyType({ typeName, typeInstance, schema: pair.actual });
238
+ for (const issue of typeIssues) {
239
+ const disposition = verifierDisposition(policy, issue);
240
+ if (disposition === 'suppress') continue;
241
+ if (disposition === 'warn') {
242
+ warnings.push(issue);
243
+ continue;
244
+ }
245
+ failures.push(issue);
246
+ }
247
+ }
248
+ }
249
+ return { failures, warnings };
250
+ }
251
+
252
+ // ============================================================================
253
+ // The issue-based verify envelope
254
+ // ============================================================================
255
+
256
+ export interface VerifySqlSchemaByDiffInput {
257
+ readonly contract: Contract<SqlStorage>;
258
+ readonly schema: SqlSchemaIRNode;
259
+ readonly strict: boolean;
260
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
261
+ /** The target's full-tree node diff (`diffSchema` descriptor hook). */
262
+ readonly diffSchema: SqlSchemaDiffFn;
263
+ /** The target's classifier: a diff issue node's `nodeKind` → its subject granularity. */
264
+ readonly granularityOf: (nodeKind: string) => DiffSubjectGranularity;
265
+ }
266
+
267
+ /**
268
+ * THE SQL schema verify: runs the target's full-tree node diff, grades it
269
+ * through the family's post-diff filters (strict gating + control-policy
270
+ * disposition) plus the codec `verifyType` hook findings, and wraps the
271
+ * verdict in the issue-based result envelope. `ok` holds exactly when both
272
+ * issue lists are empty — the lists carry the verdict's failures.
273
+ */
274
+ export function verifySqlSchemaByDiff(
275
+ input: VerifySqlSchemaByDiffInput,
276
+ ): VerifyDatabaseSchemaResult {
277
+ const startTime = Date.now();
278
+ const verdictDiff = input.diffSchema({
279
+ contract: input.contract,
280
+ schema: input.schema,
281
+ frameworkComponents: input.frameworkComponents,
282
+ });
283
+ const diffVerdict = computeSqlDiffVerdict({
284
+ issues: verdictDiff.issues,
285
+ resolveControlPolicy: verdictDiff.resolveControlPolicy,
286
+ strict: input.strict,
287
+ defaultControlPolicy: input.contract.defaultControlPolicy,
288
+ granularityOf: input.granularityOf,
289
+ });
290
+ const storageTypeVerdict = computeStorageTypeVerdict({
291
+ contract: input.contract,
292
+ namespacePairs: verdictDiff.namespacePairs,
293
+ codecHooks: extractCodecControlHooks(input.frameworkComponents),
294
+ });
295
+ const failCount = diffVerdict.failures.length + storageTypeVerdict.failures.length;
296
+ const ok = failCount === 0;
297
+ const profileHash =
298
+ 'profileHash' in input.contract && typeof input.contract.profileHash === 'string'
299
+ ? input.contract.profileHash
300
+ : undefined;
301
+ return {
302
+ ok,
303
+ ...(ok ? {} : { code: 'PN-SCHEMA-0001' }),
304
+ summary: ok
305
+ ? 'Database schema satisfies contract'
306
+ : `Database schema does not satisfy contract (${failCount} failure${failCount === 1 ? '' : 's'})`,
307
+ contract: {
308
+ storageHash: input.contract.storage.storageHash,
309
+ ...ifDefined('profileHash', profileHash),
310
+ },
311
+ target: {
312
+ expected: input.contract.target,
313
+ actual: input.contract.target,
314
+ },
315
+ schema: {
316
+ issues: [...diffVerdict.failures, ...storageTypeVerdict.failures],
317
+ warnings: {
318
+ issues: [...diffVerdict.warnings, ...storageTypeVerdict.warnings],
319
+ },
320
+ },
321
+ meta: { strict: input.strict },
322
+ timings: { total: Date.now() - startTime },
323
+ };
324
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Shared relational-diff types and semantic-satisfaction predicates. The
3
+ * coordinate-based issue diff this module used to house (`collectSqlSchemaIssues`
4
+ * / `collectSqlSchemaIssuesPerNamespace`) retired once the migration planner
5
+ * took `plan(start, end)` over the one differ (`buildPostgresPlanDiff` /
6
+ * `buildSqlitePlanDiff`); what remains here is consumed by the surviving
7
+ * verify verdict (`schema-verify.ts`) and the control adapters.
8
+ */
9
+
10
+ import type { ColumnDefault } from '@prisma-next/contract/types';
11
+ import type { SqlIndexIR, SqlUniqueIR } from '@prisma-next/sql-schema-ir/types';
12
+
13
+ /**
14
+ * Function type for normalizing raw database default expressions into ColumnDefault.
15
+ * Target-specific implementations handle database dialect differences.
16
+ */
17
+ export type DefaultNormalizer = (
18
+ rawDefault: string,
19
+ nativeType: string,
20
+ ) => ColumnDefault | undefined;
21
+
22
+ /**
23
+ * Function type for normalizing schema native types to canonical form for comparison.
24
+ * Target-specific implementations handle dialect-specific type name variations
25
+ * (e.g., Postgres 'varchar' → 'character varying', 'timestamptz' normalization).
26
+ */
27
+ export type NativeTypeNormalizer = (nativeType: string) => string;
28
+
29
+ /**
30
+ * Compares two arrays of strings for equality (order-sensitive).
31
+ */
32
+ export function arraysEqual(a: readonly string[], b: readonly string[]): boolean {
33
+ if (a.length !== b.length) {
34
+ return false;
35
+ }
36
+ for (let i = 0; i < a.length; i++) {
37
+ if (a[i] !== b[i]) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+
44
+ /**
45
+ * Checks if a unique constraint requirement is satisfied by the given columns:
46
+ * a unique constraint with the same columns, or a unique index with the same
47
+ * columns (semantic satisfaction). Used by the planners to keep the
48
+ * "stronger satisfies weaker" behavior consistent across the control plane.
49
+ */
50
+ export function isUniqueConstraintSatisfied(
51
+ uniques: readonly SqlUniqueIR[],
52
+ indexes: readonly SqlIndexIR[],
53
+ columns: readonly string[],
54
+ ): boolean {
55
+ const hasConstraint = uniques.some((unique) => arraysEqual(unique.columns, columns));
56
+ if (hasConstraint) {
57
+ return true;
58
+ }
59
+ return indexes.some((index) => index.unique && arraysEqual(index.columns, columns));
60
+ }
61
+
62
+ /**
63
+ * Checks if an index requirement is satisfied by the given columns: any index
64
+ * (unique or non-unique) with the same columns, or a unique constraint with
65
+ * the same columns (stronger satisfies weaker).
66
+ */
67
+ export function isIndexSatisfied(
68
+ indexes: readonly SqlIndexIR[],
69
+ uniques: readonly SqlUniqueIR[],
70
+ columns: readonly string[],
71
+ ): boolean {
72
+ const hasMatchingIndex = indexes.some((index) => arraysEqual(index.columns, columns));
73
+ if (hasMatchingIndex) {
74
+ return true;
75
+ }
76
+ return uniques.some((unique) => arraysEqual(unique.columns, columns));
77
+ }
@@ -0,0 +1,30 @@
1
+ import type { ControlPolicy } from '@prisma-next/contract/types';
2
+ import type {
3
+ SchemaDiffIssue,
4
+ VerifierIssueCategory,
5
+ VerifierOutcome,
6
+ } from '@prisma-next/framework-components/control';
7
+ import { dispositionForCategory } from '@prisma-next/framework-components/control';
8
+
9
+ /**
10
+ * Classifies a codec `verifyType` hook finding into the target-neutral
11
+ * categories the framework grades. A storage type is a named type instance
12
+ * (e.g. a native enum); the only shape divergence it can carry is a change
13
+ * to its value set, so a paired mismatch always classifies as `valueDrift`.
14
+ */
15
+ export function classifyStorageTypeDiffIssue(issue: SchemaDiffIssue): VerifierIssueCategory {
16
+ if (issue.reason === 'not-found') {
17
+ return 'declaredMissing';
18
+ }
19
+ if (issue.reason === 'not-expected') {
20
+ return 'extraAuxiliary';
21
+ }
22
+ return 'valueDrift';
23
+ }
24
+
25
+ export function verifierDisposition(
26
+ controlPolicy: ControlPolicy,
27
+ issue: SchemaDiffIssue,
28
+ ): VerifierOutcome {
29
+ return dispositionForCategory(controlPolicy, classifyStorageTypeDiffIssue(issue));
30
+ }