Skip to main content

drizzle_migrations/postgres/
collection.rs

1//! `PostgreSQL` DDL collection — typed access to schema entities.
2//!
3//! The generic [`EntityCollection<T>`] storage backbone lives in
4//! [`crate::collection`]; this file supplies the per-entity-type lookup
5//! helpers (`one`, `for_table`, etc.) whose shape depends on each
6//! Postgres entity's identity (`(schema, name)`, `(schema, table, name)`).
7
8use 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
17// =============================================================================
18// Per-entity-type lookup helpers
19// =============================================================================
20
21// Schema-specific operations
22impl 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
29// Enum-specific operations
30impl 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
39// Sequence-specific operations
40impl 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
49// Role-specific operations
50impl 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
57// Policy-specific operations
58impl 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
74// Table-specific operations
75impl 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
84// Column-specific operations
85impl 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
101// Index-specific operations
102impl 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
118// ForeignKey-specific operations
119impl 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
135// PrimaryKey-specific operations
136impl 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
151// UniqueConstraint-specific operations
152impl 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
168// CheckConstraint-specific operations
169impl 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
185// View-specific operations
186impl 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// =============================================================================
196// PostgreSQL DDL - Main Collection Type
197// =============================================================================
198
199/// `PostgreSQL` DDL collection - stores all schema entities
200#[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    /// Create a new empty DDL collection
219    #[must_use]
220    pub fn new() -> Self {
221        Self::default()
222    }
223
224    /// Create DDL from a list of entities
225    #[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    /// Push any entity type
235    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            // Privileges are not yet tracked in the DDL collection.
251            PostgresEntity::Privilege(_) => {}
252        }
253    }
254
255    /// Convert to entity array for snapshot serialization
256    #[must_use]
257    pub fn to_entities(&self) -> Vec<PostgresEntity> {
258        let mut entities = Vec::new();
259
260        // Push in logical order
261        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    /// Check if DDL is empty
308    #[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
314// =============================================================================
315// Diff Types
316// =============================================================================
317
318// Re-export shared DiffType from traits module
319pub use crate::traits::DiffType;
320
321/// A diff statement for any entity
322#[derive(Debug, Clone)]
323pub struct EntityDiff {
324    pub diff_type: DiffType,
325    pub kind: EntityKind,
326    pub name: String,
327    /// For alter: changed fields with (from, to) values
328    pub changes: HashMap<String, (String, String)>,
329    /// Original entity (for drop/alter)
330    pub left: Option<PostgresEntity>,
331    /// New entity (for create/alter)
332    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/// Compute diff between two DDL collections
456#[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
464/// Helper to diff a single entity type
465fn 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    // Find dropped
489    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    // Find created
504    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    // Find altered
519    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(), // Rely on left/right for details
529                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
570/// Fill unset identity sequence options with `PostgreSQL`'s defaults so a
571/// schema-defined identity (options omitted) compares equal to the same
572/// column introspected from the database (options materialized).
573fn 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            // Ascending identity sequences default to MINVALUE 1 (not the
586            // type minimum).
587            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
599/// Normalize a column default for comparison: strip trailing `::type` casts
600/// that `PostgreSQL` appends when it stores the expression, so
601/// `'active'::text` compares equal to `'active'` and `'{}'::jsonb` to
602/// `'{}'`. Non-cast defaults compare exactly.
603pub(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
611/// Strip one trailing `::type` cast if — and only if — the `::` sits outside
612/// any single-quoted literal and everything after it is a plain type name
613/// (identifier characters, spaces, digits, parentheses, and `[]`).
614fn 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
644/// Compare check constraints with normalized expressions, so the introspected
645/// `CHECK ((age > 18))` form compares equal to the schema's `age > 18`.
646fn 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
662/// Strip one pair of outer parentheses when they wrap the entire expression.
663fn strip_outer_parens(value: &str) -> Option<&str> {
664    let trimmed = value.trim();
665    let inner = trimmed.strip_prefix('(')?.strip_suffix(')')?;
666    // Ensure the leading paren matches the trailing one — `(a) AND (b)`
667    // must not become `a) AND (b`.
668    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
684/// Compare views with normalized definitions (whitespace collapsed, trailing
685/// semicolon removed) so cosmetic formatting differences don't trigger a
686/// drop-and-recreate.
687///
688/// Note: `pg_get_viewdef` re-qualifies column references, so an introspected
689/// definition can still differ textually from the schema's definition even
690/// when semantically identical. drizzle-kit handles this by skipping the
691/// definition comparison entirely in *push* mode; our diff engine has no
692/// push/generate distinction yet, so we keep comparing (a definition edit in
693/// the schema must still produce a migration) and only normalize
694/// conservatively.
695fn 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
709/// Compare indexes with NULLS ordering normalized to `PostgreSQL`'s
710/// defaults: `DESC` implies `NULLS FIRST`, `ASC` implies `NULLS LAST`. A
711/// schema-side `col DESC` (with `nulls_first` unset/false) must compare
712/// equal to the introspected column, which records the effective
713/// `nulls_first = true`.
714fn 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                // DESC without explicit NULLS LAST defaults to NULLS FIRST;
721                // both spellings behave identically, so compare them equal.
722                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
732/// Compare sequences with unset options normalized to `PostgreSQL`'s
733/// defaults, so a schema-defined `CREATE SEQUENCE` with no options compares
734/// equal to its introspected counterpart (which materializes every option).
735fn 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            // Sequences default to bigint range.
750            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    // `name_explicit` is provenance metadata (introspection always says true,
768    // macros say false for default names), not DDL semantics.
769    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    // See foreign_keys_equivalent: name_explicit is provenance, not DDL.
778    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    // See foreign_keys_equivalent: name_explicit is provenance, not DDL.
787    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        // `character varying` was handled above, so any remaining
868        // char-family spelling (char, character, bpchar) is fixed-width.
869        _ 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        // Different literals must still differ.
960        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        // Introspected `varchar(255)` (typmod reconstructed by
1043        // COLUMNS_QUERY) vs schema-side `character varying(255)`.
1044        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}