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 }
144 }
145
146 fn pk(name: &str, cols: &[&str]) -> Constraint {
147 Constraint {
148 qname: qn(name),
149 kind: ConstraintKind::PrimaryKey {
150 columns: cols.iter().map(|c| id(c)).collect(),
151 include: vec![],
152 },
153 deferrable: Deferrable::NotDeferrable,
154 comment: None,
155 }
156 }
157
158 fn fk(name: &str, on_delete: ReferentialAction) -> Constraint {
159 Constraint {
160 qname: qn(name),
161 kind: ConstraintKind::ForeignKey(ForeignKey {
162 columns: vec![id("org_id")],
163 referenced_table: qn("orgs"),
164 referenced_columns: vec![id("id")],
165 on_update: ReferentialAction::NoAction,
166 on_delete,
167 match_type: FkMatchType::Simple,
168 }),
169 deferrable: Deferrable::NotDeferrable,
170 comment: None,
171 }
172 }
173
174 fn run(target: &Table, source: &Table) -> Vec<TableOpEntry> {
175 let mut out = Vec::new();
176 diff_constraints(target, source, &mut out);
177 out
178 }
179
180 #[test]
181 fn add_pk_is_safe() {
182 let target = tbl_with(vec![]);
183 let source = tbl_with(vec![pk("users_pkey", &["id"])]);
184 let ops = run(&target, &source);
185 assert_eq!(ops.len(), 1);
186 assert!(matches!(ops[0].op, TableOp::AddConstraint(_)));
187 assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
188 }
189
190 #[test]
191 fn add_fk_is_safe() {
192 let target = tbl_with(vec![]);
193 let source = tbl_with(vec![fk("users_org_fkey", ReferentialAction::NoAction)]);
194 let ops = run(&target, &source);
195 assert_eq!(ops.len(), 1);
196 assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
197 }
198
199 #[test]
200 fn drop_pk_requires_approval() {
201 let target = tbl_with(vec![pk("users_pkey", &["id"])]);
202 let source = tbl_with(vec![]);
203 let ops = run(&target, &source);
204 assert_eq!(ops.len(), 1);
205 assert!(matches!(ops[0].op, TableOp::DropConstraint { .. }));
206 assert!(ops[0].destructiveness.requires_approval());
207 assert!(
208 ops[0]
209 .destructiveness
210 .reason()
211 .unwrap()
212 .contains("primary key")
213 );
214 }
215
216 #[test]
217 fn drop_fk_requires_approval() {
218 let target = tbl_with(vec![fk("users_org_fkey", ReferentialAction::NoAction)]);
219 let source = tbl_with(vec![]);
220 let ops = run(&target, &source);
221 assert_eq!(ops.len(), 1);
222 assert!(ops[0].destructiveness.requires_approval());
223 assert!(
224 ops[0]
225 .destructiveness
226 .reason()
227 .unwrap()
228 .contains("foreign key")
229 );
230 }
231
232 #[test]
233 fn pk_column_change_emits_drop_then_add() {
234 let target = tbl_with(vec![pk("users_pkey", &["id"])]);
235 let source = tbl_with(vec![pk("users_pkey", &["id", "org_id"])]);
236 let ops = run(&target, &source);
237 assert_eq!(ops.len(), 2);
238 assert!(matches!(ops[0].op, TableOp::DropConstraint { .. }));
239 assert!(matches!(ops[1].op, TableOp::AddConstraint(_)));
240 }
241
242 #[test]
243 fn fk_on_delete_change_emits_drop_then_add() {
244 let target = tbl_with(vec![fk("fk1", ReferentialAction::NoAction)]);
245 let source = tbl_with(vec![fk("fk1", ReferentialAction::Cascade)]);
246 let ops = run(&target, &source);
247 assert_eq!(ops.len(), 2);
248 assert!(matches!(ops[0].op, TableOp::DropConstraint { .. }));
249 assert!(matches!(ops[1].op, TableOp::AddConstraint(_)));
250 }
251
252 #[test]
253 fn comment_only_change_emits_set_constraint_comment() {
254 let mut a = pk("users_pkey", &["id"]);
255 a.comment = Some("v1".into());
256 let mut b = pk("users_pkey", &["id"]);
257 b.comment = Some("v2".into());
258 let ops = run(&tbl_with(vec![a]), &tbl_with(vec![b]));
259 assert_eq!(ops.len(), 1);
260 match &ops[0].op {
261 TableOp::SetConstraintComment { name, comment } => {
262 assert_eq!(name, &id("users_pkey"));
263 assert_eq!(comment.as_deref(), Some("v2"));
264 }
265 other => panic!("got {other:?}"),
266 }
267 assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
268 }
269
270 #[test]
271 fn deferrable_change_emits_drop_then_add() {
272 let a = pk("users_pkey", &["id"]);
273 let b = Constraint {
274 deferrable: Deferrable::Deferrable {
275 initially_deferred: true,
276 },
277 ..a.clone()
278 };
279 let ops = run(&tbl_with(vec![a]), &tbl_with(vec![b]));
280 assert_eq!(ops.len(), 2);
281 }
282
283 #[test]
284 fn equal_constraints_emit_nothing() {
285 let a = pk("users_pkey", &["id"]);
286 let b = pk("users_pkey", &["id"]);
287 let ops = run(&tbl_with(vec![a]), &tbl_with(vec![b]));
288 assert!(ops.is_empty());
289 }
290}