1use std::collections::BTreeMap;
18
19use crate::identifier::QualifiedName;
20use crate::ir::constraint::{Constraint, ConstraintKind};
21use crate::ir::table::Table;
22
23use super::destructiveness::Destructiveness;
24use super::table_op::{TableOp, TableOpEntry};
25
26pub fn diff_constraints(target: &Table, source: &Table, out: &mut Vec<TableOpEntry>) {
28 let target_map: BTreeMap<&QualifiedName, &Constraint> =
29 target.constraints.iter().map(|c| (&c.qname, c)).collect();
30 let source_map: BTreeMap<&QualifiedName, &Constraint> =
31 source.constraints.iter().map(|c| (&c.qname, c)).collect();
32
33 for (qname, source_constraint) in &source_map {
35 if !target_map.contains_key(qname) {
36 out.push(add_constraint_entry((*source_constraint).clone()));
37 }
38 }
39
40 for (qname, target_constraint) in &target_map {
41 match source_map.get(qname) {
42 None => {
43 out.push(drop_constraint_entry(target_constraint));
45 }
46 Some(source_constraint) => {
47 if definition_changed(target_constraint, source_constraint) {
48 out.push(drop_constraint_entry(target_constraint));
50 out.push(add_constraint_entry((*source_constraint).clone()));
51 } else if target_constraint.comment != source_constraint.comment {
52 out.push(TableOpEntry {
53 op: TableOp::SetConstraintComment {
54 name: qname.name.clone(),
55 comment: source_constraint.comment.clone(),
56 },
57 destructiveness: Destructiveness::Safe,
58 });
59 }
60 }
61 }
62 }
63}
64
65const fn add_constraint_entry(c: Constraint) -> TableOpEntry {
66 TableOpEntry {
67 op: TableOp::AddConstraint(c),
68 destructiveness: Destructiveness::Safe,
69 }
70}
71
72fn drop_constraint_entry(c: &Constraint) -> TableOpEntry {
73 TableOpEntry {
74 op: TableOp::DropConstraint {
75 name: c.qname.name.clone(),
76 },
77 destructiveness: Destructiveness::RequiresApproval {
78 reason: format!("drops {} constraint {}", kind_label(&c.kind), c.qname.name),
79 },
80 }
81}
82
83const fn kind_label(k: &ConstraintKind) -> &'static str {
84 match k {
85 ConstraintKind::PrimaryKey { .. } => "primary key",
86 ConstraintKind::Unique { .. } => "unique",
87 ConstraintKind::ForeignKey(_) => "foreign key",
88 ConstraintKind::Check { .. } => "check",
89 }
90}
91
92fn definition_changed(a: &Constraint, b: &Constraint) -> bool {
94 a.kind != b.kind || a.deferrable != b.deferrable
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::identifier::Identifier;
101 use crate::ir::column::Column;
102 use crate::ir::column_type::ColumnType;
103 use crate::ir::constraint::{Deferrable, FkMatchType, ForeignKey, ReferentialAction};
104
105 fn id(s: &str) -> Identifier {
106 Identifier::from_unquoted(s).unwrap()
107 }
108
109 fn qn(name: &str) -> QualifiedName {
110 QualifiedName::new(id("app"), id(name))
111 }
112
113 fn col(name: &str) -> Column {
114 Column {
115 name: id(name),
116 ty: ColumnType::BigInt,
117 nullable: false,
118 default: None,
119 identity: None,
120 generated: None,
121 collation: None,
122 storage: None,
123 compression: None,
124 comment: None,
125 }
126 }
127
128 fn tbl_with(constraints: Vec<Constraint>) -> Table {
129 Table {
130 qname: qn("users"),
131 columns: vec![col("id"), col("org_id")],
132 constraints,
133 partition_by: None,
134 partition_of: None,
135 comment: None,
136 owner: None,
137 grants: vec![],
138 rls_enabled: false,
139 rls_forced: false,
140 policies: vec![],
141 storage: crate::ir::reloptions::TableStorageOptions::default(),
142 access_method: None,
143 tablespace: None,
144 }
145 }
146
147 fn pk(name: &str, cols: &[&str]) -> Constraint {
148 Constraint {
149 qname: qn(name),
150 kind: ConstraintKind::PrimaryKey {
151 columns: cols.iter().map(|c| id(c)).collect(),
152 include: vec![],
153 },
154 deferrable: Deferrable::NotDeferrable,
155 comment: None,
156 }
157 }
158
159 fn fk(name: &str, on_delete: ReferentialAction) -> Constraint {
160 Constraint {
161 qname: qn(name),
162 kind: ConstraintKind::ForeignKey(ForeignKey {
163 columns: vec![id("org_id")],
164 referenced_table: qn("orgs"),
165 referenced_columns: vec![id("id")],
166 on_update: ReferentialAction::NoAction,
167 on_delete,
168 match_type: FkMatchType::Simple,
169 }),
170 deferrable: Deferrable::NotDeferrable,
171 comment: None,
172 }
173 }
174
175 fn run(target: &Table, source: &Table) -> Vec<TableOpEntry> {
176 let mut out = Vec::new();
177 diff_constraints(target, source, &mut out);
178 out
179 }
180
181 #[test]
182 fn add_pk_is_safe() {
183 let target = tbl_with(vec![]);
184 let source = tbl_with(vec![pk("users_pkey", &["id"])]);
185 let ops = run(&target, &source);
186 assert_eq!(ops.len(), 1);
187 assert!(matches!(ops[0].op, TableOp::AddConstraint(_)));
188 assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
189 }
190
191 #[test]
192 fn add_fk_is_safe() {
193 let target = tbl_with(vec![]);
194 let source = tbl_with(vec![fk("users_org_fkey", ReferentialAction::NoAction)]);
195 let ops = run(&target, &source);
196 assert_eq!(ops.len(), 1);
197 assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
198 }
199
200 #[test]
201 fn drop_pk_requires_approval() {
202 let target = tbl_with(vec![pk("users_pkey", &["id"])]);
203 let source = tbl_with(vec![]);
204 let ops = run(&target, &source);
205 assert_eq!(ops.len(), 1);
206 assert!(matches!(ops[0].op, TableOp::DropConstraint { .. }));
207 assert!(ops[0].destructiveness.requires_approval());
208 assert!(
209 ops[0]
210 .destructiveness
211 .reason()
212 .unwrap()
213 .contains("primary key")
214 );
215 }
216
217 #[test]
218 fn drop_fk_requires_approval() {
219 let target = tbl_with(vec![fk("users_org_fkey", ReferentialAction::NoAction)]);
220 let source = tbl_with(vec![]);
221 let ops = run(&target, &source);
222 assert_eq!(ops.len(), 1);
223 assert!(ops[0].destructiveness.requires_approval());
224 assert!(
225 ops[0]
226 .destructiveness
227 .reason()
228 .unwrap()
229 .contains("foreign key")
230 );
231 }
232
233 #[test]
234 fn pk_column_change_emits_drop_then_add() {
235 let target = tbl_with(vec![pk("users_pkey", &["id"])]);
236 let source = tbl_with(vec![pk("users_pkey", &["id", "org_id"])]);
237 let ops = run(&target, &source);
238 assert_eq!(ops.len(), 2);
239 assert!(matches!(ops[0].op, TableOp::DropConstraint { .. }));
240 assert!(matches!(ops[1].op, TableOp::AddConstraint(_)));
241 }
242
243 #[test]
244 fn fk_on_delete_change_emits_drop_then_add() {
245 let target = tbl_with(vec![fk("fk1", ReferentialAction::NoAction)]);
246 let source = tbl_with(vec![fk("fk1", ReferentialAction::Cascade)]);
247 let ops = run(&target, &source);
248 assert_eq!(ops.len(), 2);
249 assert!(matches!(ops[0].op, TableOp::DropConstraint { .. }));
250 assert!(matches!(ops[1].op, TableOp::AddConstraint(_)));
251 }
252
253 #[test]
254 fn comment_only_change_emits_set_constraint_comment() {
255 let mut a = pk("users_pkey", &["id"]);
256 a.comment = Some("v1".into());
257 let mut b = pk("users_pkey", &["id"]);
258 b.comment = Some("v2".into());
259 let ops = run(&tbl_with(vec![a]), &tbl_with(vec![b]));
260 assert_eq!(ops.len(), 1);
261 match &ops[0].op {
262 TableOp::SetConstraintComment { name, comment } => {
263 assert_eq!(name, &id("users_pkey"));
264 assert_eq!(comment.as_deref(), Some("v2"));
265 }
266 other => panic!("got {other:?}"),
267 }
268 assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
269 }
270
271 #[test]
272 fn deferrable_change_emits_drop_then_add() {
273 let a = pk("users_pkey", &["id"]);
274 let b = Constraint {
275 deferrable: Deferrable::Deferrable {
276 initially_deferred: true,
277 },
278 ..a.clone()
279 };
280 let ops = run(&tbl_with(vec![a]), &tbl_with(vec![b]));
281 assert_eq!(ops.len(), 2);
282 }
283
284 #[test]
285 fn equal_constraints_emit_nothing() {
286 let a = pk("users_pkey", &["id"]);
287 let b = pk("users_pkey", &["id"]);
288 let ops = run(&tbl_with(vec![a]), &tbl_with(vec![b]));
289 assert!(ops.is_empty());
290 }
291}