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