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