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