1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::identifier::{Identifier, QualifiedName};
8use crate::ir::column::Column;
9use crate::ir::constraint::Constraint;
10use crate::ir::difference::Difference;
11use crate::ir::eq::{Diff, diff_field, prefix_diffs};
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct Table {
16 pub qname: QualifiedName,
18 pub columns: Vec<Column>,
20 pub constraints: Vec<Constraint>,
22 pub partition_by: Option<crate::ir::partition::PartitionBy>,
24 pub partition_of: Option<crate::ir::partition::PartitionOf>,
26 pub comment: Option<String>,
28 pub owner: Option<Identifier>,
31 pub grants: Vec<crate::ir::grant::Grant>,
33 pub rls_enabled: bool,
35 pub rls_forced: bool,
37 pub policies: Vec<crate::ir::policy::Policy>,
39 pub storage: crate::ir::reloptions::TableStorageOptions,
42}
43
44impl Diff for Table {
45 fn diff(&self, other: &Self) -> Vec<Difference> {
46 let mut out = Vec::new();
47 out.extend(diff_field("qname", &self.qname, &other.qname));
48 out.extend(diff_field(
49 "partition_by",
50 &format!("{:?}", self.partition_by),
51 &format!("{:?}", other.partition_by),
52 ));
53 out.extend(diff_field(
54 "partition_of",
55 &format!("{:?}", self.partition_of),
56 &format!("{:?}", other.partition_of),
57 ));
58 out.extend(diff_field(
59 "comment",
60 &format!("{:?}", self.comment),
61 &format!("{:?}", other.comment),
62 ));
63 out.extend(diff_field(
64 "owner",
65 &format!("{:?}", self.owner),
66 &format!("{:?}", other.owner),
67 ));
68 out.extend(diff_field(
69 "grants",
70 &format!("{:?}", self.grants),
71 &format!("{:?}", other.grants),
72 ));
73 out.extend(diff_field(
74 "rls_enabled",
75 &format!("{:?}", self.rls_enabled),
76 &format!("{:?}", other.rls_enabled),
77 ));
78 out.extend(diff_field(
79 "rls_forced",
80 &format!("{:?}", self.rls_forced),
81 &format!("{:?}", other.rls_forced),
82 ));
83 out.extend(diff_field(
84 "policies",
85 &format!("{:?}", self.policies),
86 &format!("{:?}", other.policies),
87 ));
88 out.extend(diff_field(
89 "storage",
90 &format!("{:?}", self.storage),
91 &format!("{:?}", other.storage),
92 ));
93 out.extend(diff_columns(&self.columns, &other.columns));
94 out.extend(diff_constraints(&self.constraints, &other.constraints));
95 out
96 }
97}
98
99fn diff_columns(
101 lhs_cols: &[crate::ir::column::Column],
102 rhs_cols: &[crate::ir::column::Column],
103) -> Vec<Difference> {
104 let mut out = Vec::new();
105 let lhs: BTreeMap<_, _> = lhs_cols.iter().map(|c| (c.name.as_str(), c)).collect();
106 let rhs: BTreeMap<_, _> = rhs_cols.iter().map(|c| (c.name.as_str(), c)).collect();
107 for (name, l) in &lhs {
108 match rhs.get(name) {
109 None => out.push(Difference::new(
110 format!("columns.{name}"),
111 "present",
112 "removed",
113 )),
114 Some(r) => {
115 out.extend(prefix_diffs(&format!("columns.{name}"), l.diff(r)));
116 }
117 }
118 }
119 for name in rhs.keys() {
120 if !lhs.contains_key(name) {
121 out.push(Difference::new(
122 format!("columns.{name}"),
123 "missing",
124 "added",
125 ));
126 }
127 }
128 let lhs_order: Vec<&str> = lhs_cols.iter().map(|c| c.name.as_str()).collect();
129 let rhs_order: Vec<&str> = rhs_cols.iter().map(|c| c.name.as_str()).collect();
130 if lhs_order != rhs_order {
131 out.push(Difference::new(
132 "columns.<order>",
133 lhs_order.join(","),
134 rhs_order.join(","),
135 ));
136 }
137 out
138}
139
140fn diff_constraints(
142 lhs_cs_slice: &[crate::ir::constraint::Constraint],
143 rhs_cs_slice: &[crate::ir::constraint::Constraint],
144) -> Vec<Difference> {
145 let mut out = Vec::new();
146 let lhs_cs: BTreeMap<_, _> = lhs_cs_slice.iter().map(|c| (&c.qname, c)).collect();
147 let rhs_cs: BTreeMap<_, _> = rhs_cs_slice.iter().map(|c| (&c.qname, c)).collect();
148 for (qn, l) in &lhs_cs {
149 match rhs_cs.get(qn) {
150 None => out.push(Difference::new(
151 format!("constraints.{qn}"),
152 "present",
153 "removed",
154 )),
155 Some(r) => {
156 out.extend(prefix_diffs(&format!("constraints.{qn}"), l.diff(r)));
157 }
158 }
159 }
160 for qn in rhs_cs.keys() {
161 if !lhs_cs.contains_key(qn) {
162 out.push(Difference::new(
163 format!("constraints.{qn}"),
164 "missing",
165 "added",
166 ));
167 }
168 }
169 out
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::identifier::Identifier;
176 use crate::ir::column_type::ColumnType;
177 use crate::ir::constraint::{ConstraintKind, Deferrable};
178
179 fn id(s: &str) -> Identifier {
180 Identifier::from_unquoted(s).unwrap()
181 }
182
183 fn qn(name: &str) -> QualifiedName {
184 QualifiedName::new(id("app"), id(name))
185 }
186
187 fn col(name: &str, ty: ColumnType, nullable: bool) -> Column {
188 Column {
189 name: id(name),
190 ty,
191 nullable,
192 default: None,
193 identity: None,
194 generated: None,
195 collation: None,
196 storage: None,
197 compression: None,
198 comment: None,
199 }
200 }
201
202 fn pk(name: &str, cols: &[&str]) -> Constraint {
203 Constraint {
204 qname: qn(name),
205 kind: ConstraintKind::PrimaryKey {
206 columns: cols.iter().map(|c| id(c)).collect(),
207 include: vec![],
208 },
209 deferrable: Deferrable::NotDeferrable,
210 comment: None,
211 }
212 }
213
214 fn base() -> Table {
215 Table {
216 qname: qn("users"),
217 columns: vec![
218 col("id", ColumnType::BigInt, false),
219 col("email", ColumnType::Text, false),
220 ],
221 constraints: vec![pk("users_pkey", &["id"])],
222 partition_by: None,
223 partition_of: None,
224 comment: None,
225 owner: None,
226 grants: vec![],
227 rls_enabled: false,
228 rls_forced: false,
229 policies: vec![],
230 storage: crate::ir::reloptions::TableStorageOptions::default(),
231 }
232 }
233
234 #[test]
235 fn equal_tables_have_no_diff() {
236 assert!(base().canonical_eq(&base()));
237 }
238
239 #[test]
240 fn add_column_diffs() {
241 let mut b = base();
242 b.columns.push(col("name", ColumnType::Text, true));
243 let d = base().diff(&b);
244 assert!(d.iter().any(|x| x.path == "columns.name"));
245 }
246
247 #[test]
248 fn remove_column_diffs() {
249 let mut b = base();
250 b.columns.pop();
251 let d = base().diff(&b);
252 assert!(d.iter().any(|x| x.path == "columns.email"));
253 }
254
255 #[test]
256 fn reorder_columns_diffs_as_order() {
257 let mut b = base();
258 b.columns.reverse();
259 let d = base().diff(&b);
260 assert!(d.iter().any(|x| x.path == "columns.<order>"));
261 }
262
263 #[test]
264 fn add_constraint_diffs() {
265 let mut b = base();
266 b.constraints.push(pk("users_alt_pkey", &["email"]));
267 let d = base().diff(&b);
268 assert!(d.iter().any(|x| x.path == "constraints.app.users_alt_pkey"));
269 }
270
271 #[test]
272 fn changed_column_definition_diffs_under_path() {
273 let mut b = base();
274 b.columns[1].nullable = true;
275 let d = base().diff(&b);
276 assert!(d.iter().any(|x| x.path == "columns.email.nullable"));
277 }
278
279 #[test]
280 fn owner_change_diffs() {
281 let mut b = base();
282 b.owner = Some(id("new_owner"));
283 assert!(base().diff(&b).iter().any(|x| x.path == "owner"));
284 }
285
286 #[test]
287 fn grants_change_diffs() {
288 let mut b = base();
289 b.grants.push(crate::ir::grant::Grant {
290 grantee: crate::ir::grant::GrantTarget::Public,
291 privilege: crate::ir::grant::Privilege::Select,
292 with_grant_option: false,
293 columns: None,
294 });
295 assert!(base().diff(&b).iter().any(|x| x.path == "grants"));
296 }
297
298 #[test]
299 fn rls_enabled_change_diffs() {
300 let mut b = base();
301 b.rls_enabled = true;
302 assert!(base().diff(&b).iter().any(|x| x.path == "rls_enabled"));
303 }
304
305 #[test]
306 fn rls_forced_change_diffs() {
307 let mut b = base();
308 b.rls_forced = true;
309 assert!(base().diff(&b).iter().any(|x| x.path == "rls_forced"));
310 }
311
312 #[test]
313 fn policies_change_diffs() {
314 use crate::ir::grant::GrantTarget;
315 use crate::ir::policy::{Policy, PolicyCommand};
316 let mut b = base();
317 b.policies.push(Policy {
318 name: id("p1"),
319 permissive: true,
320 command: PolicyCommand::All,
321 roles: vec![GrantTarget::Public],
322 using: None,
323 with_check: None,
324 });
325 assert!(base().diff(&b).iter().any(|x| x.path == "policies"));
326 }
327
328 #[test]
329 fn storage_change_diffs() {
330 let mut b = base();
331 b.storage = crate::ir::reloptions::TableStorageOptions {
332 fillfactor: Some(80),
333 ..Default::default()
334 };
335 assert!(base().diff(&b).iter().any(|x| x.path == "storage"));
336 }
337}