1use super::ddl::{
9 CheckConstraint, Column, Enum, ForeignKey, Index, Policy, PostgresEntity, PrimaryKey, Role,
10 Schema, Sequence, Table, UniqueConstraint, View,
11};
12use crate::collection::EntityCollection;
13use crate::traits::EntityKind;
14use std::borrow::Cow;
15use std::collections::HashMap;
16
17impl EntityCollection<Schema> {
23 #[must_use]
24 pub fn one(&self, name: &str) -> Option<&Schema> {
25 self.entities.iter().find(|s| s.name == name)
26 }
27}
28
29impl EntityCollection<Enum> {
31 #[must_use]
32 pub fn one(&self, schema: &str, name: &str) -> Option<&Enum> {
33 self.entities
34 .iter()
35 .find(|e| e.schema == schema && e.name == name)
36 }
37}
38
39impl EntityCollection<Sequence> {
41 #[must_use]
42 pub fn one(&self, schema: &str, name: &str) -> Option<&Sequence> {
43 self.entities
44 .iter()
45 .find(|s| s.schema == schema && s.name == name)
46 }
47}
48
49impl EntityCollection<Role> {
51 #[must_use]
52 pub fn one(&self, name: &str) -> Option<&Role> {
53 self.entities.iter().find(|r| r.name == name)
54 }
55}
56
57impl EntityCollection<Policy> {
59 #[must_use]
60 pub fn one(&self, schema: &str, table: &str, name: &str) -> Option<&Policy> {
61 self.entities
62 .iter()
63 .find(|p| p.schema == schema && p.table == table && p.name == name)
64 }
65 #[must_use]
66 pub fn for_table(&self, schema: &str, table: &str) -> Vec<&Policy> {
67 self.entities
68 .iter()
69 .filter(|p| p.schema == schema && p.table == table)
70 .collect()
71 }
72}
73
74impl EntityCollection<Table> {
76 #[must_use]
77 pub fn one(&self, schema: &str, name: &str) -> Option<&Table> {
78 self.entities
79 .iter()
80 .find(|t| t.schema == schema && t.name == name)
81 }
82}
83
84impl EntityCollection<Column> {
86 #[must_use]
87 pub fn one(&self, schema: &str, table: &str, name: &str) -> Option<&Column> {
88 self.entities
89 .iter()
90 .find(|c| c.schema == schema && c.table == table && c.name == name)
91 }
92 #[must_use]
93 pub fn for_table(&self, schema: &str, table: &str) -> Vec<&Column> {
94 self.entities
95 .iter()
96 .filter(|c| c.schema == schema && c.table == table)
97 .collect()
98 }
99}
100
101impl EntityCollection<Index> {
103 #[must_use]
104 pub fn one(&self, schema: &str, name: &str) -> Option<&Index> {
105 self.entities
106 .iter()
107 .find(|i| i.schema == schema && i.name == name)
108 }
109 #[must_use]
110 pub fn for_table(&self, schema: &str, table: &str) -> Vec<&Index> {
111 self.entities
112 .iter()
113 .filter(|i| i.schema == schema && i.table == table)
114 .collect()
115 }
116}
117
118impl EntityCollection<ForeignKey> {
120 #[must_use]
121 pub fn one(&self, schema: &str, name: &str) -> Option<&ForeignKey> {
122 self.entities
123 .iter()
124 .find(|f| f.schema == schema && f.name == name)
125 }
126 #[must_use]
127 pub fn for_table(&self, schema: &str, table: &str) -> Vec<&ForeignKey> {
128 self.entities
129 .iter()
130 .filter(|f| f.schema == schema && f.table == table)
131 .collect()
132 }
133}
134
135impl EntityCollection<PrimaryKey> {
137 #[must_use]
138 pub fn one(&self, schema: &str, name: &str) -> Option<&PrimaryKey> {
139 self.entities
140 .iter()
141 .find(|p| p.schema == schema && p.name == name)
142 }
143 #[must_use]
144 pub fn for_table(&self, schema: &str, table: &str) -> Option<&PrimaryKey> {
145 self.entities
146 .iter()
147 .find(|p| p.schema == schema && p.table == table)
148 }
149}
150
151impl EntityCollection<UniqueConstraint> {
153 #[must_use]
154 pub fn one(&self, schema: &str, name: &str) -> Option<&UniqueConstraint> {
155 self.entities
156 .iter()
157 .find(|u| u.schema == schema && u.name == name)
158 }
159 #[must_use]
160 pub fn for_table(&self, schema: &str, table: &str) -> Vec<&UniqueConstraint> {
161 self.entities
162 .iter()
163 .filter(|u| u.schema == schema && u.table == table)
164 .collect()
165 }
166}
167
168impl EntityCollection<CheckConstraint> {
170 #[must_use]
171 pub fn one(&self, schema: &str, name: &str) -> Option<&CheckConstraint> {
172 self.entities
173 .iter()
174 .find(|c| c.schema == schema && c.name == name)
175 }
176 #[must_use]
177 pub fn for_table(&self, schema: &str, table: &str) -> Vec<&CheckConstraint> {
178 self.entities
179 .iter()
180 .filter(|c| c.schema == schema && c.table == table)
181 .collect()
182 }
183}
184
185impl EntityCollection<View> {
187 #[must_use]
188 pub fn one(&self, schema: &str, name: &str) -> Option<&View> {
189 self.entities
190 .iter()
191 .find(|v| v.schema == schema && v.name == name)
192 }
193}
194
195#[derive(Debug, Clone, Default)]
201pub struct PostgresDDL {
202 pub schemas: EntityCollection<Schema>,
203 pub enums: EntityCollection<Enum>,
204 pub sequences: EntityCollection<Sequence>,
205 pub roles: EntityCollection<Role>,
206 pub policies: EntityCollection<Policy>,
207 pub tables: EntityCollection<Table>,
208 pub columns: EntityCollection<Column>,
209 pub indexes: EntityCollection<Index>,
210 pub fks: EntityCollection<ForeignKey>,
211 pub pks: EntityCollection<PrimaryKey>,
212 pub uniques: EntityCollection<UniqueConstraint>,
213 pub checks: EntityCollection<CheckConstraint>,
214 pub views: EntityCollection<View>,
215}
216
217impl PostgresDDL {
218 #[must_use]
220 pub fn new() -> Self {
221 Self::default()
222 }
223
224 #[must_use]
226 pub fn from_entities(entities: Vec<PostgresEntity>) -> Self {
227 let mut ddl = Self::new();
228 for entity in entities {
229 ddl.push_entity(entity);
230 }
231 ddl
232 }
233
234 pub fn push_entity(&mut self, entity: PostgresEntity) {
236 match entity {
237 PostgresEntity::Schema(s) => self.schemas.push(s),
238 PostgresEntity::Enum(e) => self.enums.push(e),
239 PostgresEntity::Sequence(s) => self.sequences.push(s),
240 PostgresEntity::Role(r) => self.roles.push(r),
241 PostgresEntity::Policy(p) => self.policies.push(p),
242 PostgresEntity::Table(t) => self.tables.push(t),
243 PostgresEntity::Column(c) => self.columns.push(c),
244 PostgresEntity::Index(i) => self.indexes.push(i),
245 PostgresEntity::ForeignKey(f) => self.fks.push(f),
246 PostgresEntity::PrimaryKey(p) => self.pks.push(p),
247 PostgresEntity::UniqueConstraint(u) => self.uniques.push(u),
248 PostgresEntity::CheckConstraint(c) => self.checks.push(c),
249 PostgresEntity::View(v) => self.views.push(v),
250 PostgresEntity::Privilege(_) => {}
252 }
253 }
254
255 #[must_use]
257 pub fn to_entities(&self) -> Vec<PostgresEntity> {
258 let mut entities = Vec::new();
259
260 for e in self.schemas.list() {
262 entities.push(PostgresEntity::Schema(e.clone()));
263 }
264 for e in self.enums.list() {
265 entities.push(PostgresEntity::Enum(e.clone()));
266 }
267 for e in self.sequences.list() {
268 entities.push(PostgresEntity::Sequence(e.clone()));
269 }
270 for e in self.roles.list() {
271 entities.push(PostgresEntity::Role(e.clone()));
272 }
273
274 for e in self.tables.list() {
275 entities.push(PostgresEntity::Table(e.clone()));
276 }
277
278 for e in self.columns.list() {
279 entities.push(PostgresEntity::Column(e.clone()));
280 }
281 for e in self.indexes.list() {
282 entities.push(PostgresEntity::Index(e.clone()));
283 }
284 for e in self.fks.list() {
285 entities.push(PostgresEntity::ForeignKey(e.clone()));
286 }
287 for e in self.pks.list() {
288 entities.push(PostgresEntity::PrimaryKey(e.clone()));
289 }
290 for e in self.uniques.list() {
291 entities.push(PostgresEntity::UniqueConstraint(e.clone()));
292 }
293 for e in self.checks.list() {
294 entities.push(PostgresEntity::CheckConstraint(e.clone()));
295 }
296 for e in self.policies.list() {
297 entities.push(PostgresEntity::Policy(e.clone()));
298 }
299
300 for e in self.views.list() {
301 entities.push(PostgresEntity::View(e.clone()));
302 }
303
304 entities
305 }
306
307 #[must_use]
309 pub const fn is_empty(&self) -> bool {
310 self.tables.is_empty() && self.enums.is_empty() && self.views.is_empty()
311 }
312}
313
314pub use crate::traits::DiffType;
320
321#[derive(Debug, Clone)]
323pub struct EntityDiff {
324 pub diff_type: DiffType,
325 pub kind: EntityKind,
326 pub name: String,
327 pub changes: HashMap<String, (String, String)>,
329 pub left: Option<PostgresEntity>,
331 pub right: Option<PostgresEntity>,
333}
334
335fn diff_top_level_entities(left: &PostgresDDL, right: &PostgresDDL, diffs: &mut Vec<EntityDiff>) {
336 diff_entity_type(
337 left.schemas.list(),
338 right.schemas.list(),
339 |e| e.name.to_string(),
340 |e| PostgresEntity::Schema(e.clone()),
341 EntityKind::Schema,
342 diffs,
343 );
344 diff_entity_type(
345 left.enums.list(),
346 right.enums.list(),
347 |e| format!("{}.{}", e.schema, e.name),
348 |e| PostgresEntity::Enum(e.clone()),
349 EntityKind::Enum,
350 diffs,
351 );
352 diff_entity_type_with(
353 left.sequences.list(),
354 right.sequences.list(),
355 |e| format!("{}.{}", e.schema, e.name),
356 |e| PostgresEntity::Sequence(e.clone()),
357 EntityKind::Sequence,
358 diffs,
359 sequences_equivalent,
360 );
361 diff_entity_type(
362 left.roles.list(),
363 right.roles.list(),
364 |e| e.name.to_string(),
365 |e| PostgresEntity::Role(e.clone()),
366 EntityKind::Role,
367 diffs,
368 );
369 diff_entity_type_with(
370 left.tables.list(),
371 right.tables.list(),
372 |e| format!("{}.{}", e.schema, e.name),
373 |e| PostgresEntity::Table(e.clone()),
374 EntityKind::Table,
375 diffs,
376 tables_equivalent,
377 );
378 diff_entity_type_with(
379 left.views.list(),
380 right.views.list(),
381 |e| format!("{}.{}", e.schema, e.name),
382 |e| PostgresEntity::View(e.clone()),
383 EntityKind::View,
384 diffs,
385 views_equivalent,
386 );
387}
388
389fn diff_table_entities(left: &PostgresDDL, right: &PostgresDDL, diffs: &mut Vec<EntityDiff>) {
390 diff_entity_type_with(
391 left.columns.list(),
392 right.columns.list(),
393 |e| format!("{}.{}.{}", e.schema, e.table, e.name),
394 |e| PostgresEntity::Column(e.clone()),
395 EntityKind::Column,
396 diffs,
397 columns_equivalent,
398 );
399 diff_entity_type_with(
400 left.indexes.list(),
401 right.indexes.list(),
402 |e| format!("{}.{}", e.schema, e.name),
403 |e| PostgresEntity::Index(e.clone()),
404 EntityKind::Index,
405 diffs,
406 indexes_equivalent,
407 );
408 diff_entity_type_with(
409 left.fks.list(),
410 right.fks.list(),
411 |e| format!("{}.{}", e.schema, e.name),
412 |e| PostgresEntity::ForeignKey(e.clone()),
413 EntityKind::ForeignKey,
414 diffs,
415 foreign_keys_equivalent,
416 );
417 diff_entity_type_with(
418 left.pks.list(),
419 right.pks.list(),
420 |e| format!("{}.{}", e.schema, e.name),
421 |e| PostgresEntity::PrimaryKey(e.clone()),
422 EntityKind::PrimaryKey,
423 diffs,
424 pks_equivalent,
425 );
426 diff_entity_type_with(
427 left.uniques.list(),
428 right.uniques.list(),
429 |e| format!("{}.{}", e.schema, e.name),
430 |e| PostgresEntity::UniqueConstraint(e.clone()),
431 EntityKind::UniqueConstraint,
432 diffs,
433 uniques_equivalent,
434 );
435 diff_entity_type_with(
436 left.checks.list(),
437 right.checks.list(),
438 |e| format!("{}.{}", e.schema, e.name),
439 |e| PostgresEntity::CheckConstraint(e.clone()),
440 EntityKind::CheckConstraint,
441 diffs,
442 checks_equivalent,
443 );
444 diff_entity_type_with(
445 left.policies.list(),
446 right.policies.list(),
447 |e| format!("{}.{}.{}", e.schema, e.table, e.name),
448 |e| PostgresEntity::Policy(e.clone()),
449 EntityKind::Policy,
450 diffs,
451 policies_equivalent,
452 );
453}
454
455#[must_use]
457pub fn diff_ddl(left: &PostgresDDL, right: &PostgresDDL) -> Vec<EntityDiff> {
458 let mut diffs = Vec::new();
459 diff_top_level_entities(left, right, &mut diffs);
460 diff_table_entities(left, right, &mut diffs);
461 diffs
462}
463
464fn diff_entity_type<T: Clone + PartialEq>(
466 left: &[T],
467 right: &[T],
468 key_fn: impl Fn(&T) -> String,
469 to_entity: impl Fn(&T) -> PostgresEntity,
470 kind: EntityKind,
471 diffs: &mut Vec<EntityDiff>,
472) {
473 diff_entity_type_with(left, right, key_fn, to_entity, kind, diffs, PartialEq::eq);
474}
475
476fn diff_entity_type_with<T: Clone>(
477 left: &[T],
478 right: &[T],
479 key_fn: impl Fn(&T) -> String,
480 to_entity: impl Fn(&T) -> PostgresEntity,
481 kind: EntityKind,
482 diffs: &mut Vec<EntityDiff>,
483 equivalent: impl Fn(&T, &T) -> bool,
484) {
485 let left_map: HashMap<String, &T> = left.iter().map(|e| (key_fn(e), e)).collect();
486 let right_map: HashMap<String, &T> = right.iter().map(|e| (key_fn(e), e)).collect();
487
488 for left_entity in left {
490 let key = key_fn(left_entity);
491 if !right_map.contains_key(&key) {
492 diffs.push(EntityDiff {
493 diff_type: DiffType::Drop,
494 kind,
495 name: key,
496 changes: HashMap::new(),
497 left: Some(to_entity(left_entity)),
498 right: None,
499 });
500 }
501 }
502
503 for right_entity in right {
505 let key = key_fn(right_entity);
506 if !left_map.contains_key(&key) {
507 diffs.push(EntityDiff {
508 diff_type: DiffType::Create,
509 kind,
510 name: key,
511 changes: HashMap::new(),
512 left: None,
513 right: Some(to_entity(right_entity)),
514 });
515 }
516 }
517
518 for left_entity in left {
520 let key = key_fn(left_entity);
521 if let Some(right_entity) = right_map.get(&key)
522 && !equivalent(left_entity, right_entity)
523 {
524 diffs.push(EntityDiff {
525 diff_type: DiffType::Alter,
526 kind,
527 name: key,
528 changes: HashMap::new(), left: Some(to_entity(left_entity)),
530 right: Some(to_entity(right_entity)),
531 });
532 }
533 }
534}
535
536fn tables_equivalent(left: &Table, right: &Table) -> bool {
537 let mut left = left.clone();
538 let mut right = right.clone();
539 left.is_rls_enabled = Some(left.is_rls_enabled.unwrap_or(false));
540 right.is_rls_enabled = Some(right.is_rls_enabled.unwrap_or(false));
541 left.is_unlogged = Some(left.is_unlogged.unwrap_or(false));
542 right.is_unlogged = Some(right.is_unlogged.unwrap_or(false));
543 left.is_temporary = Some(left.is_temporary.unwrap_or(false));
544 right.is_temporary = Some(right.is_temporary.unwrap_or(false));
545 left == right
546}
547
548pub(crate) fn columns_equivalent(left: &Column, right: &Column) -> bool {
549 let mut left = left.clone();
550 let mut right = right.clone();
551 left.sql_type = Cow::Owned(normalize_column_type_for_compare(&left));
552 right.sql_type = Cow::Owned(normalize_column_type_for_compare(&right));
553 left.dimensions = None;
554 right.dimensions = None;
555 left.ordinal_position = None;
556 right.ordinal_position = None;
557 left.default = left
558 .default
559 .as_deref()
560 .map(|default| Cow::Owned(normalize_default_for_compare(default)));
561 right.default = right
562 .default
563 .as_deref()
564 .map(|default| Cow::Owned(normalize_default_for_compare(default)));
565 normalize_identity_for_compare(&mut left);
566 normalize_identity_for_compare(&mut right);
567 left == right
568}
569
570fn normalize_identity_for_compare(column: &mut Column) {
574 use super::grammar::IdentityDefaults;
575
576 let sql_type = normalize_type_for_compare(&column.sql_type);
577 if let Some(identity) = column.identity.as_mut() {
578 if identity.increment.is_none() {
579 identity.increment = Some(Cow::Borrowed(IdentityDefaults::INCREMENT));
580 }
581 if identity.start_with.is_none() {
582 identity.start_with = Some(Cow::Borrowed(IdentityDefaults::START_WITH));
583 }
584 if identity.min_value.is_none() {
585 identity.min_value = Some(Cow::Borrowed(IdentityDefaults::MIN));
588 }
589 if identity.max_value.is_none() {
590 identity.max_value = Some(Cow::Borrowed(IdentityDefaults::max_for(&sql_type)));
591 }
592 if identity.cache.is_none() {
593 identity.cache = Some(IdentityDefaults::CACHE);
594 }
595 identity.cycle = Some(identity.cycle.unwrap_or(IdentityDefaults::CYCLE));
596 }
597}
598
599pub(crate) fn normalize_default_for_compare(default: &str) -> String {
604 let mut value = default.trim();
605 while let Some(stripped) = strip_trailing_cast(value) {
606 value = stripped;
607 }
608 value.to_string()
609}
610
611fn strip_trailing_cast(value: &str) -> Option<&str> {
615 let bytes = value.as_bytes();
616 let mut in_quotes = false;
617 let mut cast_pos = None;
618 let mut i = 0;
619 while i < bytes.len() {
620 match bytes[i] {
621 b'\'' => in_quotes = !in_quotes,
622 b':' if !in_quotes && i + 1 < bytes.len() && bytes[i + 1] == b':' => {
623 cast_pos = Some(i);
624 i += 1;
625 }
626 _ => {}
627 }
628 i += 1;
629 }
630
631 let cast_pos = cast_pos?;
632 let suffix = &value[cast_pos + 2..];
633 let is_type_name = !suffix.is_empty()
634 && suffix.chars().all(|c| {
635 c.is_ascii_alphanumeric() || matches!(c, '_' | ' ' | '(' | ')' | ',' | '[' | ']' | '"')
636 });
637 if is_type_name {
638 Some(value[..cast_pos].trim_end())
639 } else {
640 None
641 }
642}
643
644fn checks_equivalent(left: &CheckConstraint, right: &CheckConstraint) -> bool {
647 left.schema == right.schema
648 && left.table == right.table
649 && left.name == right.name
650 && normalize_check_expression(&left.value) == normalize_check_expression(&right.value)
651}
652
653pub(crate) fn normalize_check_expression(value: &str) -> String {
654 let value = super::grammar::parse_check_definition(value);
655 let mut value = collapse_sql_whitespace(&value);
656 while let Some(stripped) = strip_outer_parens(&value) {
657 value = stripped.to_string();
658 }
659 value
660}
661
662fn strip_outer_parens(value: &str) -> Option<&str> {
664 let trimmed = value.trim();
665 let inner = trimmed.strip_prefix('(')?.strip_suffix(')')?;
666 let mut depth = 0_i32;
669 for ch in inner.chars() {
670 match ch {
671 '(' => depth += 1,
672 ')' => {
673 depth -= 1;
674 if depth < 0 {
675 return None;
676 }
677 }
678 _ => {}
679 }
680 }
681 Some(inner.trim())
682}
683
684fn views_equivalent(left: &View, right: &View) -> bool {
696 let mut left = left.clone();
697 let mut right = right.clone();
698 left.definition = left
699 .definition
700 .as_deref()
701 .map(|definition| Cow::Owned(super::grammar::parse_view_definition(definition)));
702 right.definition = right
703 .definition
704 .as_deref()
705 .map(|definition| Cow::Owned(super::grammar::parse_view_definition(definition)));
706 left == right
707}
708
709fn indexes_equivalent(left: &Index, right: &Index) -> bool {
715 let mut left = left.clone();
716 let mut right = right.clone();
717 for index in [&mut left, &mut right] {
718 for column in &mut index.columns {
719 if !column.asc && !column.nulls_first {
720 column.nulls_first = true;
723 }
724 }
725 if index.method.is_none() {
726 index.method = Some(Cow::Borrowed("btree"));
727 }
728 }
729 left == right
730}
731
732fn sequences_equivalent(left: &Sequence, right: &Sequence) -> bool {
736 let mut left = left.clone();
737 let mut right = right.clone();
738 for sequence in [&mut left, &mut right] {
739 if sequence.increment_by.is_none() {
740 sequence.increment_by = Some(Cow::Borrowed("1"));
741 }
742 if sequence.start_with.is_none() {
743 sequence.start_with = Some(Cow::Borrowed("1"));
744 }
745 if sequence.min_value.is_none() {
746 sequence.min_value = Some(Cow::Borrowed("1"));
747 }
748 if sequence.max_value.is_none() {
749 sequence.max_value = Some(Cow::Borrowed("9223372036854775807"));
751 }
752 if sequence.cache_size.is_none() {
753 sequence.cache_size = Some(1);
754 }
755 sequence.cycle = Some(sequence.cycle.unwrap_or(false));
756 }
757 left == right
758}
759
760fn foreign_keys_equivalent(left: &ForeignKey, right: &ForeignKey) -> bool {
761 let mut left = left.clone();
762 let mut right = right.clone();
763 left.on_delete = normalize_fk_action(left.on_delete.as_deref());
764 left.on_update = normalize_fk_action(left.on_update.as_deref());
765 right.on_delete = normalize_fk_action(right.on_delete.as_deref());
766 right.on_update = normalize_fk_action(right.on_update.as_deref());
767 left.name_explicit = false;
770 right.name_explicit = false;
771 left == right
772}
773
774fn pks_equivalent(left: &PrimaryKey, right: &PrimaryKey) -> bool {
775 let mut left = left.clone();
776 let mut right = right.clone();
777 left.name_explicit = false;
779 right.name_explicit = false;
780 left == right
781}
782
783fn uniques_equivalent(left: &UniqueConstraint, right: &UniqueConstraint) -> bool {
784 let mut left = left.clone();
785 let mut right = right.clone();
786 left.name_explicit = false;
788 right.name_explicit = false;
789 left == right
790}
791
792fn policies_equivalent(left: &Policy, right: &Policy) -> bool {
793 let mut left = left.clone();
794 let mut right = right.clone();
795 normalize_policy(&mut left);
796 normalize_policy(&mut right);
797 left == right
798}
799
800fn normalize_fk_action(action: Option<&str>) -> Option<Cow<'static, str>> {
801 match action {
802 None => None,
803 Some(action) if action.eq_ignore_ascii_case("NO ACTION") => None,
804 Some(action) => Some(Cow::Owned(action.to_ascii_uppercase())),
805 }
806}
807
808fn normalize_policy(policy: &mut Policy) {
809 policy.as_clause = Some(Cow::Owned(
810 policy
811 .as_clause
812 .as_deref()
813 .unwrap_or("PERMISSIVE")
814 .to_ascii_uppercase(),
815 ));
816 policy.for_clause = Some(Cow::Owned(
817 policy
818 .for_clause
819 .as_deref()
820 .unwrap_or("ALL")
821 .to_ascii_uppercase(),
822 ));
823 if let Some(roles) = policy.to.as_mut() {
824 for role in roles {
825 if role.eq_ignore_ascii_case("public") {
826 *role = Cow::Borrowed("PUBLIC");
827 }
828 }
829 }
830}
831
832fn collapse_sql_whitespace(value: &str) -> String {
833 value.split_whitespace().collect::<Vec<_>>().join(" ")
834}
835
836fn normalize_type_for_compare(sql_type: &str) -> String {
837 let mut ty = collapse_sql_whitespace(&sql_type.trim().to_ascii_lowercase());
838 let mut dimensions = String::new();
839
840 while let Some(stripped) = ty.strip_suffix("[]") {
841 dimensions.push_str("[]");
842 ty = stripped.trim_end().to_string();
843 }
844
845 if let Some(stripped) = ty.strip_prefix('_') {
846 dimensions.push_str("[]");
847 ty = stripped.to_string();
848 }
849
850 let params = ty
851 .find('(')
852 .map(|idx| ty[idx..].to_string())
853 .unwrap_or_default();
854
855 let canonical = match ty.as_str() {
856 "int" | "int4" | "integer" => "integer".to_string(),
857 "int2" | "smallint" => "smallint".to_string(),
858 "int8" | "bigint" => "bigint".to_string(),
859 "bool" | "boolean" => "boolean".to_string(),
860 "timestamptz" | "timestamp with time zone" => "timestamp with time zone".to_string(),
861 "timestamp" | "timestamp without time zone" => "timestamp".to_string(),
862 "timetz" | "time with time zone" => "time with time zone".to_string(),
863 "time" | "time without time zone" => "time".to_string(),
864 _ if ty.starts_with("varchar") || ty.starts_with("character varying") => {
865 format!("character varying{params}")
866 }
867 _ if ty.starts_with("bpchar") || ty.starts_with("character") || ty.starts_with("char") => {
870 format!("character{params}")
871 }
872 _ => match super::grammar::PgTypeCategory::from_sql_type(&ty) {
873 super::grammar::PgTypeCategory::SmallInt => "smallint".to_string(),
874 super::grammar::PgTypeCategory::Integer => "integer".to_string(),
875 super::grammar::PgTypeCategory::BigInt => "bigint".to_string(),
876 super::grammar::PgTypeCategory::Boolean => "boolean".to_string(),
877 super::grammar::PgTypeCategory::Text => "text".to_string(),
878 super::grammar::PgTypeCategory::Varchar => format!("character varying{params}"),
879 super::grammar::PgTypeCategory::Numeric => format!("numeric{params}"),
880 super::grammar::PgTypeCategory::TimestampTz => "timestamp with time zone".to_string(),
881 super::grammar::PgTypeCategory::Timestamp => "timestamp".to_string(),
882 super::grammar::PgTypeCategory::TimeTz => "time with time zone".to_string(),
883 super::grammar::PgTypeCategory::Time => "time".to_string(),
884 _ => ty,
885 },
886 };
887
888 format!("{canonical}{dimensions}")
889}
890
891pub(crate) fn normalize_column_type_for_compare(column: &Column) -> String {
892 let mut sql_type = column.sql_type.to_string();
893 if let Some(dimensions) = column.dimensions
894 && dimensions > 0
895 {
896 for _ in 0..dimensions {
897 sql_type.push_str("[]");
898 }
899 }
900 normalize_type_for_compare(&sql_type)
901}
902
903#[cfg(test)]
904mod tests {
905 use super::*;
906
907 fn column_with_type(sql_type: &str) -> Column {
908 Column::new("public", "users", "value", sql_type.to_string())
909 }
910
911 #[test]
912 fn postgres_type_aliases_compare_equal() {
913 let cases = [
914 ("int4", "INTEGER"),
915 ("varchar(255)", "character varying(255)"),
916 ("timestamptz", "TIMESTAMP WITH TIME ZONE"),
917 ("bool", "BOOLEAN"),
918 ];
919
920 for (left_type, right_type) in cases {
921 let left = PostgresDDL::from_entities(vec![
922 PostgresEntity::Table(Table::new("public", "users")),
923 PostgresEntity::Column(column_with_type(left_type)),
924 ]);
925 let right = PostgresDDL::from_entities(vec![
926 PostgresEntity::Table(Table::new("public", "users")),
927 PostgresEntity::Column(column_with_type(right_type)),
928 ]);
929
930 let diffs = diff_ddl(&left, &right);
931 assert!(
932 diffs.is_empty(),
933 "expected {left_type:?} and {right_type:?} to compare equal, got {diffs:?}"
934 );
935 }
936 }
937
938 #[test]
939 fn column_defaults_compare_equal_across_introspected_casts() {
940 let cases = [
941 ("'active'::text", "'active'"),
942 ("'{}'::jsonb", "'{}'"),
943 ("'2020-01-01'::date", "'2020-01-01'"),
944 ("'a''::b'::text", "'a''::b'"),
945 ("'x'::character varying", "'x'"),
946 ];
947
948 for (introspected, schema_side) in cases {
949 let mut left = column_with_type("text");
950 left.default = Some(introspected.into());
951 let mut right = column_with_type("text");
952 right.default = Some(schema_side.into());
953 assert!(
954 columns_equivalent(&left, &right),
955 "expected default {introspected:?} to compare equal to {schema_side:?}"
956 );
957 }
958
959 let mut left = column_with_type("text");
961 left.default = Some("'active'::text".into());
962 let mut right = column_with_type("text");
963 right.default = Some("'archived'".into());
964 assert!(!columns_equivalent(&left, &right));
965 }
966
967 #[test]
968 fn identity_options_compare_equal_to_defaults() {
969 use drizzle_types::postgres::ddl::{Identity, IdentityType};
970
971 let mut left = column_with_type("int4");
972 left.identity = Some(Identity {
973 name: "users_value_seq".into(),
974 schema: Some("public".into()),
975 type_: IdentityType::Always,
976 increment: Some("1".into()),
977 min_value: Some("1".into()),
978 max_value: Some("2147483647".into()),
979 start_with: Some("1".into()),
980 cache: Some(1),
981 cycle: Some(false),
982 });
983
984 let mut right = column_with_type("integer");
985 right.identity = Some(Identity {
986 name: "users_value_seq".into(),
987 schema: Some("public".into()),
988 type_: IdentityType::Always,
989 increment: None,
990 min_value: None,
991 max_value: None,
992 start_with: None,
993 cache: None,
994 cycle: None,
995 });
996
997 assert!(columns_equivalent(&left, &right));
998 }
999
1000 #[test]
1001 fn check_expressions_compare_equal_across_paren_spellings() {
1002 let make = |value: &str| CheckConstraint {
1003 schema: Cow::Borrowed("public"),
1004 table: Cow::Borrowed("users"),
1005 name: Cow::Borrowed("users_age_check"),
1006 value: Cow::Owned(value.to_string()),
1007 };
1008
1009 assert!(checks_equivalent(&make("(age > 18)"), &make("age > 18")));
1010 assert!(checks_equivalent(
1011 &make("CHECK ((age > 18))"),
1012 &make("age > 18")
1013 ));
1014 assert!(checks_equivalent(
1015 &make("((a > 1) AND (b > 2))"),
1016 &make("(a > 1) AND (b > 2)")
1017 ));
1018 assert!(!checks_equivalent(&make("(age > 18)"), &make("age > 21")));
1019 }
1020
1021 #[test]
1022 fn view_definitions_compare_equal_across_whitespace_and_semicolon() {
1023 let make = |definition: &str| View {
1024 schema: Cow::Borrowed("public"),
1025 name: Cow::Borrowed("v"),
1026 definition: Some(Cow::Owned(definition.to_string())),
1027 ..View::default()
1028 };
1029
1030 assert!(views_equivalent(
1031 &make(" SELECT id,\n name\n FROM users;"),
1032 &make("SELECT id, name FROM users")
1033 ));
1034 assert!(!views_equivalent(
1035 &make("SELECT id FROM users"),
1036 &make("SELECT id, name FROM users")
1037 ));
1038 }
1039
1040 #[test]
1041 fn varchar_typmod_survives_comparison() {
1042 assert!(columns_equivalent(
1045 &column_with_type("varchar(255)"),
1046 &column_with_type("character varying(255)")
1047 ));
1048 assert!(!columns_equivalent(
1049 &column_with_type("varchar(255)"),
1050 &column_with_type("character varying(64)")
1051 ));
1052 assert!(columns_equivalent(
1053 &column_with_type("numeric(10,2)"),
1054 &column_with_type("decimal(10,2)")
1055 ));
1056 assert!(columns_equivalent(
1057 &column_with_type("bpchar(10)"),
1058 &column_with_type("character(10)")
1059 ));
1060 }
1061
1062 #[test]
1063 fn foreign_key_no_action_compares_equal_to_omitted_actions() {
1064 let mut left = ForeignKey::from_strings(
1065 "public".to_string(),
1066 "posts".to_string(),
1067 "posts_user_fk".to_string(),
1068 vec!["user_id".to_string()],
1069 "public".to_string(),
1070 "users".to_string(),
1071 vec!["id".to_string()],
1072 );
1073 left.on_delete = Some(Cow::Borrowed("NO ACTION"));
1074 left.on_update = Some(Cow::Borrowed("no action"));
1075
1076 let right = ForeignKey::from_strings(
1077 "public".to_string(),
1078 "posts".to_string(),
1079 "posts_user_fk".to_string(),
1080 vec!["user_id".to_string()],
1081 "public".to_string(),
1082 "users".to_string(),
1083 vec!["id".to_string()],
1084 );
1085
1086 assert!(foreign_keys_equivalent(&left, &right));
1087 }
1088
1089 #[test]
1090 fn public_policy_roles_compare_equal_case_insensitively() {
1091 let mut left = Policy::new("public", "users", "users_policy");
1092 left.to = Some(vec![Cow::Borrowed("public")]);
1093
1094 let mut right = Policy::new("public", "users", "users_policy");
1095 right.as_clause = Some(Cow::Borrowed("PERMISSIVE"));
1096 right.for_clause = Some(Cow::Borrowed("ALL"));
1097 right.to = Some(vec![Cow::Borrowed("PUBLIC")]);
1098
1099 assert!(policies_equivalent(&left, &right));
1100 }
1101}