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