Skip to main content

drizzle_migrations/postgres/
statements.rs

1//! `PostgreSQL` SQL generation from schema metadata
2
3use super::collection::{DiffType, EntityDiff, PostgresDDL};
4use super::ddl::{
5    CheckConstraint, Column, Enum, ForeignKey, Index, Policy, PostgresEntity, PrimaryKey, Role,
6    Schema, Sequence, Table, TableSql, UniqueConstraint, View,
7};
8use crate::traits::EntityKind;
9use serde::Serialize;
10use std::collections::{HashMap, HashSet};
11use std::fmt::Write;
12
13pub const BREAKPOINT: &str = "--> statement-breakpoint";
14
15#[derive(Debug, Clone)]
16struct CreateTableOrder {
17    ordered: Vec<String>,
18    cycle_tables: HashSet<String>,
19}
20
21// =============================================================================
22// JSON Statements
23// =============================================================================
24
25#[derive(Serialize, Debug, Clone)]
26#[serde(tag = "type", rename_all = "snake_case")]
27pub enum JsonStatement {
28    CreateTable {
29        table: RichTable,
30    },
31    DropTable {
32        table: Table,
33        #[serde(rename = "key")]
34        table_key: String,
35    },
36    RenameTable {
37        schema: String,
38        from: String,
39        to: String,
40    },
41    AddColumn {
42        column: Box<Column>,
43        #[serde(rename = "isPK")]
44        is_pk: bool,
45        #[serde(rename = "isCompositePK")]
46        is_composite_pk: bool,
47    },
48    DropColumn {
49        column: Box<Column>,
50    },
51    AlterColumn {
52        to: Box<Column>,
53        #[serde(rename = "wasEnum")]
54        was_enum: bool,
55        #[serde(rename = "isEnum")]
56        is_enum: bool,
57        diff: HashMap<String, serde_json::Value>, // simplified diff structure
58    },
59    RenameColumn {
60        from: Box<Column>,
61        to: Box<Column>,
62    },
63    CreateIndex {
64        index: Index,
65    },
66    DropIndex {
67        index: Index,
68    },
69    CreateFk {
70        fk: ForeignKey,
71    },
72    DropFk {
73        fk: ForeignKey,
74    },
75    AddPk {
76        pk: PrimaryKey,
77    },
78    DropPk {
79        pk: PrimaryKey,
80    },
81    AddUnique {
82        unique: UniqueConstraint,
83    },
84    DropUnique {
85        unique: UniqueConstraint,
86    },
87    AddCheck {
88        check: CheckConstraint,
89    },
90    DropCheck {
91        check: CheckConstraint,
92    },
93    CreateSchema {
94        name: String,
95    },
96    DropSchema {
97        name: String,
98    },
99    RenameSchema {
100        from: Schema,
101        to: Schema,
102    },
103    CreateEnum {
104        #[serde(rename = "enum")]
105        enum_: Enum,
106    },
107    DropEnum {
108        #[serde(rename = "enum")]
109        enum_: Enum,
110    },
111    AlterEnum {
112        from: Enum,
113        to: Enum,
114        diff: Vec<EnumDiff>,
115    },
116    CreateSequence {
117        sequence: Sequence,
118    },
119    DropSequence {
120        sequence: Sequence,
121    },
122    CreateView {
123        view: View,
124    },
125    DropView {
126        view: View,
127    },
128    /// Alter a view by dropping and recreating (`PostgreSQL` doesn't support ALTER VIEW for definition changes)
129    AlterView {
130        old_view: Box<View>,
131        new_view: Box<View>,
132    },
133    CreateRole {
134        role: Role,
135    },
136    DropRole {
137        role: Role,
138    },
139    CreatePolicy {
140        policy: Policy,
141    },
142    DropPolicy {
143        policy: Policy,
144    },
145    AlterTable {
146        old_table: Table,
147        new_table: Table,
148    },
149    RecreateFk {
150        old_fk: ForeignKey,
151        new_fk: ForeignKey,
152    },
153    RecreateUnique {
154        old_unique: UniqueConstraint,
155        new_unique: UniqueConstraint,
156    },
157    /// Recreate column by dropping and re-adding (for generated columns, type changes, etc.)
158    RecreateColumn {
159        old_column: Box<Column>,
160        new_column: Box<Column>,
161    },
162    /// Recreate an index by dropping and re-creating (`PostgreSQL` has no
163    /// general ALTER INDEX for definition changes).
164    RecreateIndex {
165        old_index: Box<Index>,
166        new_index: Box<Index>,
167    },
168    /// Recreate a primary key: DROP CONSTRAINT + ADD CONSTRAINT.
169    RecreatePk {
170        old_pk: PrimaryKey,
171        new_pk: PrimaryKey,
172    },
173    /// Recreate a check constraint: DROP CONSTRAINT + ADD CONSTRAINT.
174    RecreateCheck {
175        old_check: CheckConstraint,
176        new_check: CheckConstraint,
177    },
178    /// Recreate a policy: DROP POLICY + CREATE POLICY (drizzle-kit style).
179    RecreatePolicy {
180        old_policy: Box<Policy>,
181        new_policy: Box<Policy>,
182    },
183    /// ALTER SEQUENCE with the changed options.
184    AlterSequence {
185        old_sequence: Sequence,
186        new_sequence: Sequence,
187    },
188    /// ALTER ROLE with the changed flags.
189    AlterRole {
190        old_role: Role,
191        new_role: Role,
192    },
193    /// Recreate an enum type whose values were removed or reordered
194    /// (drizzle-kit flow: alter dependent columns to text, drop + recreate
195    /// the type, alter the columns back with `USING ::text::type`, restore
196    /// defaults).
197    RecreateEnum {
198        old_enum: Enum,
199        new_enum: Enum,
200        /// Columns (from the current schema) whose type is this enum.
201        columns: Vec<Column>,
202    },
203}
204
205#[derive(Serialize, Debug, Clone)]
206#[serde(rename_all = "camelCase")]
207pub struct EnumDiff {
208    pub r#type: String, // "added"
209    pub value: String,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub before_value: Option<String>,
212}
213
214/// A "Rich" table structure that includes sub-entities (columns, constraints)
215/// needed for CREATE TABLE statement generation.
216#[derive(Serialize, Debug, Clone)]
217#[serde(rename_all = "camelCase")]
218pub struct RichTable {
219    pub name: String,
220    pub schema: String,
221    pub columns: Vec<Column>,
222    pub indexes: Vec<Index>,
223    pub foreign_keys: Vec<ForeignKey>,
224    pub pk: Option<PrimaryKey>,
225    pub uniques: Vec<UniqueConstraint>,
226    pub checks: Vec<CheckConstraint>,
227    pub policies: Vec<Policy>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub comment: Option<String>,
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub is_rls_enabled: Option<bool>,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub is_unlogged: Option<bool>,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub is_temporary: Option<bool>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub inherits: Option<String>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub tablespace: Option<String>,
240}
241
242#[derive(Default)]
243struct CreatedTableEntities<'a> {
244    columns: Vec<&'a Column>,
245    indexes: Vec<&'a Index>,
246    foreign_keys: Vec<&'a ForeignKey>,
247    primary_keys: Vec<&'a PrimaryKey>,
248    unique_constraints: Vec<&'a UniqueConstraint>,
249    check_constraints: Vec<&'a CheckConstraint>,
250    policies: Vec<&'a Policy>,
251}
252
253struct DiffIndex<'a> {
254    table_diffs: HashMap<&'a str, &'a EntityDiff>,
255    created_by_table: HashMap<String, CreatedTableEntities<'a>>,
256}
257
258impl<'a> DiffIndex<'a> {
259    fn new(diffs: &'a [EntityDiff]) -> Self {
260        let mut table_diffs = HashMap::new();
261        let mut created_by_table = HashMap::<String, CreatedTableEntities<'a>>::new();
262
263        for diff in diffs {
264            if diff.kind == EntityKind::Table {
265                table_diffs.insert(diff.name.as_str(), diff);
266            }
267            if diff.diff_type != DiffType::Create {
268                continue;
269            }
270
271            let Some(entity) = diff.right.as_ref() else {
272                continue;
273            };
274            let Some(table_key) = Generator::get_parent_table_key(diff) else {
275                continue;
276            };
277            let entries = created_by_table.entry(table_key).or_default();
278            match entity {
279                PostgresEntity::Column(value) => entries.columns.push(value),
280                PostgresEntity::Index(value) => entries.indexes.push(value),
281                PostgresEntity::ForeignKey(value) => entries.foreign_keys.push(value),
282                PostgresEntity::PrimaryKey(value) => entries.primary_keys.push(value),
283                PostgresEntity::UniqueConstraint(value) => {
284                    entries.unique_constraints.push(value);
285                }
286                PostgresEntity::CheckConstraint(value) => entries.check_constraints.push(value),
287                PostgresEntity::Policy(value) => entries.policies.push(value),
288                _ => {}
289            }
290        }
291
292        Self {
293            table_diffs,
294            created_by_table,
295        }
296    }
297
298    fn table_diff(&self, table_key: &str) -> Option<&'a EntityDiff> {
299        self.table_diffs.get(table_key).copied()
300    }
301}
302
303fn rich_table_to_table(table: &RichTable) -> Table {
304    Table {
305        schema: table.schema.clone().into(),
306        name: table.name.clone().into(),
307        is_unlogged: table.is_unlogged,
308        is_temporary: table.is_temporary,
309        inherits: table.inherits.clone().map(Into::into),
310        tablespace: table.tablespace.clone().map(Into::into),
311        is_rls_enabled: table.is_rls_enabled,
312        comment: table.comment.clone().map(Into::into),
313    }
314}
315
316// =============================================================================
317// Generator
318// =============================================================================
319
320pub struct Generator {
321    pub breakpoints: bool,
322}
323
324impl Default for Generator {
325    fn default() -> Self {
326        Self::new()
327    }
328}
329
330impl Generator {
331    #[must_use]
332    pub const fn new() -> Self {
333        Self { breakpoints: true }
334    }
335
336    #[must_use]
337    pub const fn with_breakpoints(mut self, breakpoints: bool) -> Self {
338        self.breakpoints = breakpoints;
339        self
340    }
341
342    /// Generate SQL statements from a set of entity diffs.
343    ///
344    /// # Panics
345    ///
346    /// Panics if a table listed in `created_tables` is not found in `diff`
347    /// — this cannot happen in practice because `created_tables` is built
348    /// from `diff` itself.
349    #[must_use]
350    pub fn generate(&self, diff: &[EntityDiff]) -> Vec<String> {
351        self.generate_with_ddl(diff, None)
352    }
353
354    /// Generate SQL statements from a set of entity diffs, with access to the
355    /// full *current* DDL for cross-entity lookups (e.g. finding the columns
356    /// that depend on an enum being recreated).
357    ///
358    /// Statement ordering is dependency-phased:
359    ///
360    /// 1. creates of schemas → enums → sequences → roles,
361    /// 2. drops of views,
362    /// 3. drops of table sub-entities on surviving tables
363    ///    (FKs/indexes/constraints/policies before columns),
364    /// 4. table drops in reverse dependency order,
365    /// 5. table creates in dependency order (with inlined sub-entities),
366    /// 6. sub-entity creates on pre-existing tables (columns first),
367    /// 7. alters (including `ALTER COLUMN ... USING` enum conversions),
368    /// 8. enum and sequence drops (after alters so `USING` casts run before
369    ///    `DROP TYPE`),
370    /// 9. view creates,
371    /// 10. role and schema drops.
372    ///
373    /// # Panics
374    ///
375    /// Panics if a table listed in `created_tables` is not found in `diff`
376    /// — this cannot happen in practice because `created_tables` is built
377    /// from `diff` itself.
378    #[must_use]
379    pub fn generate_with_ddl(
380        &self,
381        diff: &[EntityDiff],
382        cur_ddl: Option<&PostgresDDL>,
383    ) -> Vec<String> {
384        let mut sqls = Vec::new();
385        let diff_index = DiffIndex::new(diff);
386
387        // Identify created/dropped tables to group and filter their components.
388        let created_tables: Vec<String> = diff
389            .iter()
390            .filter(|d| d.diff_type == DiffType::Create && d.kind == EntityKind::Table)
391            .map(|d| d.name.clone())
392            .collect();
393        let dropped_tables: Vec<String> = diff
394            .iter()
395            .filter(|d| d.diff_type == DiffType::Drop && d.kind == EntityKind::Table)
396            .map(|d| d.name.clone())
397            .collect();
398
399        let push_diff = |sqls: &mut Vec<String>, d: &EntityDiff| {
400            if let Some(stmt) = Self::diff_to_statement_with_context(d, &diff_index, cur_ddl) {
401                sqls.extend(Self::statement_to_sqls(stmt));
402            }
403        };
404
405        // Phase 1: creates of top-level entities in dependency order.
406        for kind in [
407            EntityKind::Schema,
408            EntityKind::Enum,
409            EntityKind::Sequence,
410            EntityKind::Role,
411        ] {
412            for d in diff
413                .iter()
414                .filter(|d| d.kind == kind && d.diff_type == DiffType::Create)
415            {
416                push_diff(&mut sqls, d);
417            }
418        }
419
420        // Phase 2: view drops (views depend on tables/columns/enums, so they
421        // must go before any of those are dropped or altered).
422        for d in diff
423            .iter()
424            .filter(|d| d.kind == EntityKind::View && d.diff_type == DiffType::Drop)
425        {
426            push_diff(&mut sqls, d);
427        }
428
429        // Phase 3: sub-entity drops on surviving tables. FK, index and
430        // constraint drops must precede column drops — dropping a column
431        // first would implicitly remove its dependent objects and make the
432        // later DROP INDEX / DROP CONSTRAINT fail with "does not exist".
433        for kind in [
434            EntityKind::ForeignKey,
435            EntityKind::Index,
436            EntityKind::PrimaryKey,
437            EntityKind::UniqueConstraint,
438            EntityKind::CheckConstraint,
439            EntityKind::Policy,
440            EntityKind::Column,
441        ] {
442            for d in diff
443                .iter()
444                .filter(|d| d.kind == kind && d.diff_type == DiffType::Drop)
445            {
446                if let Some(parent_table) = Self::get_parent_table_key(d)
447                    && dropped_tables.contains(&parent_table)
448                {
449                    continue; // handled by DROP TABLE
450                }
451                push_diff(&mut sqls, d);
452            }
453        }
454
455        // Phase 4: table drops in reverse dependency order (referencing
456        // tables drop before the tables they point at).
457        let sorted_drops = topological_sort_tables_for_drop(&dropped_tables, diff);
458        for table_key in &sorted_drops {
459            if let Some(table_diff) = diff_index.table_diff(table_key)
460                && table_diff.diff_type == DiffType::Drop
461            {
462                push_diff(&mut sqls, table_diff);
463            }
464        }
465
466        // Phase 5: table creates (Rich tables) in dependency order.
467        let sorted_creates = topological_sort_tables_for_create(&created_tables, diff);
468        if sorted_creates.cycle_tables.is_empty() {
469            for table_key in &sorted_creates.ordered {
470                let table_diff = diff_index
471                    .table_diff(table_key)
472                    .expect("created table must have an indexed table diff");
473                if let Some(PostgresEntity::Table(table)) = &table_diff.right {
474                    let rich_table = Self::build_rich_table(table, &diff_index);
475                    sqls.push(Self::create_table_sql(&rich_table));
476                    Self::push_created_table_extras(&mut sqls, &rich_table);
477                }
478            }
479        } else {
480            let mut deferred_fks = Vec::new();
481            let mut rich_tables = Vec::new();
482
483            for table_key in &sorted_creates.ordered {
484                let table_diff = diff_index
485                    .table_diff(table_key)
486                    .expect("created table must have an indexed table diff");
487                if let Some(PostgresEntity::Table(table)) = &table_diff.right {
488                    let mut rich_table = Self::build_rich_table(table, &diff_index);
489                    let (inline_fks, cycle_fks): (Vec<_>, Vec<_>) = rich_table
490                        .foreign_keys
491                        .into_iter()
492                        .partition(|fk| !Self::is_cycle_fk(fk, &sorted_creates.cycle_tables));
493                    rich_table.foreign_keys = inline_fks;
494                    deferred_fks.extend(cycle_fks);
495                    sqls.push(Self::create_table_sql(&rich_table));
496                    rich_tables.push(rich_table);
497                }
498            }
499
500            for fk in &deferred_fks {
501                sqls.push(Self::add_fk_sql(fk));
502            }
503
504            for rich_table in &rich_tables {
505                Self::push_created_table_extras(&mut sqls, rich_table);
506            }
507        }
508
509        // Phase 6: sub-entity creates on pre-existing tables, columns first so
510        // constraints and indexes can reference them.
511        for kind in [
512            EntityKind::Column,
513            EntityKind::PrimaryKey,
514            EntityKind::UniqueConstraint,
515            EntityKind::CheckConstraint,
516            EntityKind::Index,
517            EntityKind::ForeignKey,
518            EntityKind::Policy,
519        ] {
520            for d in diff
521                .iter()
522                .filter(|d| d.kind == kind && d.diff_type == DiffType::Create)
523            {
524                if let Some(parent_table) = Self::get_parent_table_key(d)
525                    && created_tables.contains(&parent_table)
526                {
527                    continue; // inlined in CREATE TABLE
528                }
529                push_diff(&mut sqls, d);
530            }
531        }
532
533        // Phase 7: alters, in diff order (enum ADD VALUE alters precede
534        // column alters, which precede table/view alters).
535        for d in diff.iter().filter(|d| d.diff_type == DiffType::Alter) {
536            push_diff(&mut sqls, d);
537        }
538
539        // Phase 8: enum and sequence drops. These must come after column
540        // alters: converting a column away from an enum (`ALTER COLUMN ...
541        // USING`) has to run before `DROP TYPE`, and a dropped sequence may
542        // be referenced by a column default until the alter removes it.
543        for kind in [EntityKind::Enum, EntityKind::Sequence] {
544            for d in diff
545                .iter()
546                .filter(|d| d.kind == kind && d.diff_type == DiffType::Drop)
547            {
548                push_diff(&mut sqls, d);
549            }
550        }
551
552        // Phase 9: view creates (after every table/column they may select from).
553        for d in diff
554            .iter()
555            .filter(|d| d.kind == EntityKind::View && d.diff_type == DiffType::Create)
556        {
557            push_diff(&mut sqls, d);
558        }
559
560        // Phase 10: role drops (after the policies that referenced them) and
561        // schema drops (after everything inside the schema is gone).
562        for kind in [EntityKind::Role, EntityKind::Schema] {
563            for d in diff
564                .iter()
565                .filter(|d| d.kind == kind && d.diff_type == DiffType::Drop)
566            {
567                push_diff(&mut sqls, d);
568            }
569        }
570
571        sqls
572    }
573
574    fn get_parent_table_key(d: &EntityDiff) -> Option<String> {
575        // Extract schema.name for table from entity
576        // Uses the conventions from collection.rs keys
577        match d.kind {
578            EntityKind::Column | EntityKind::Policy => {
579                // key: schema.table.name
580                let parts: Vec<&str> = d.name.split('.').collect();
581                if parts.len() >= 3 {
582                    Some(format!("{}.{}", parts[0], parts[1]))
583                } else {
584                    None
585                }
586            }
587            EntityKind::Index
588            | EntityKind::ForeignKey
589            | EntityKind::PrimaryKey
590            | EntityKind::UniqueConstraint
591            | EntityKind::CheckConstraint => {
592                // key: schema.name (constraint/index name).
593                // Need the entity itself to know the table.
594                let entity = d.right.as_ref().or(d.left.as_ref())?;
595                match entity {
596                    PostgresEntity::Index(i) => Some(format!("{}.{}", i.schema, i.table)),
597                    PostgresEntity::ForeignKey(f) => Some(format!("{}.{}", f.schema, f.table)),
598                    PostgresEntity::PrimaryKey(p) => Some(format!("{}.{}", p.schema, p.table)),
599                    PostgresEntity::UniqueConstraint(u) => {
600                        Some(format!("{}.{}", u.schema, u.table))
601                    }
602                    PostgresEntity::CheckConstraint(c) => Some(format!("{}.{}", c.schema, c.table)),
603                    _ => None,
604                }
605            }
606            _ => None,
607        }
608    }
609
610    fn table_key(schema: &str, table: &str) -> String {
611        format!("{schema}.{table}")
612    }
613
614    fn is_cycle_fk(fk: &ForeignKey, cycle_tables: &HashSet<String>) -> bool {
615        cycle_tables.contains(&Self::table_key(&fk.schema, &fk.table))
616            && cycle_tables.contains(&Self::table_key(&fk.schema_to, &fk.table_to))
617    }
618
619    fn push_created_table_extras(sqls: &mut Vec<String>, table: &RichTable) {
620        sqls.extend(Self::created_table_comments_sql(table));
621
622        for index in &table.indexes {
623            sqls.push(Self::create_index_sql(index));
624        }
625
626        if table.is_rls_enabled.unwrap_or(false) {
627            sqls.push(format!(
628                "ALTER TABLE {} ENABLE ROW LEVEL SECURITY;",
629                Self::qualified_name(&table.schema, &table.name)
630            ));
631        }
632
633        for policy in &table.policies {
634            sqls.push(Self::create_policy_sql(policy));
635        }
636    }
637
638    fn build_rich_table(table: &Table, diff_index: &DiffIndex<'_>) -> RichTable {
639        let table_key = format!("{}.{}", table.schema, table.name);
640        let entities = diff_index.created_by_table.get(&table_key);
641        let columns = entities
642            .map(|entries| {
643                entries
644                    .columns
645                    .iter()
646                    .map(|value| (*value).clone())
647                    .collect()
648            })
649            .unwrap_or_default();
650        let indexes = entities
651            .map(|entries| {
652                entries
653                    .indexes
654                    .iter()
655                    .map(|value| (*value).clone())
656                    .collect()
657            })
658            .unwrap_or_default();
659        let foreign_keys = entities
660            .map(|entries| {
661                entries
662                    .foreign_keys
663                    .iter()
664                    .map(|value| (*value).clone())
665                    .collect()
666            })
667            .unwrap_or_default();
668        let uniques = entities
669            .map(|entries| {
670                entries
671                    .unique_constraints
672                    .iter()
673                    .map(|value| (*value).clone())
674                    .collect()
675            })
676            .unwrap_or_default();
677        let checks = entities
678            .map(|entries| {
679                entries
680                    .check_constraints
681                    .iter()
682                    .map(|value| (*value).clone())
683                    .collect()
684            })
685            .unwrap_or_default();
686        let policies = entities
687            .map(|entries| {
688                entries
689                    .policies
690                    .iter()
691                    .map(|value| (*value).clone())
692                    .collect()
693            })
694            .unwrap_or_default();
695        let pk = entities
696            .and_then(|entries| entries.primary_keys.first())
697            .map(|value| (*value).clone());
698
699        RichTable {
700            name: table.name.to_string(),
701            schema: table.schema.to_string(),
702            is_rls_enabled: table.is_rls_enabled,
703            is_unlogged: table.is_unlogged,
704            is_temporary: table.is_temporary,
705            inherits: table.inherits.as_ref().map(ToString::to_string),
706            tablespace: table.tablespace.as_ref().map(ToString::to_string),
707            columns,
708            indexes,
709            foreign_keys,
710            pk,
711            uniques,
712            checks,
713            policies,
714            comment: table.comment.as_ref().map(ToString::to_string),
715        }
716    }
717
718    /// Convert a single diff entry to a JSON statement, with access to the full diff
719    /// for cross-entity lookups (e.g., determining if a column is part of a PK).
720    fn diff_to_statement_with_context(
721        d: &EntityDiff,
722        diff_index: &DiffIndex<'_>,
723        cur_ddl: Option<&PostgresDDL>,
724    ) -> Option<JsonStatement> {
725        match d.diff_type {
726            DiffType::Create => Self::create_diff_to_statement(d.right.as_ref()?, diff_index),
727            DiffType::Drop => Self::drop_diff_to_statement(d.left.as_ref()?),
728            DiffType::Alter => {
729                Self::alter_diff_to_statement(d.left.as_ref(), d.right.as_ref(), cur_ddl)
730            }
731        }
732    }
733
734    fn create_diff_to_statement(
735        right: &PostgresEntity,
736        diff_index: &DiffIndex<'_>,
737    ) -> Option<JsonStatement> {
738        match right {
739            PostgresEntity::Schema(s) => Some(JsonStatement::CreateSchema {
740                name: s.name.to_string(),
741            }),
742            PostgresEntity::Enum(e) => Some(JsonStatement::CreateEnum { enum_: e.clone() }),
743            PostgresEntity::Sequence(s) => Some(JsonStatement::CreateSequence {
744                sequence: s.clone(),
745            }),
746            PostgresEntity::Role(r) => Some(JsonStatement::CreateRole { role: r.clone() }),
747            PostgresEntity::View(v) => Some(JsonStatement::CreateView { view: v.clone() }),
748            PostgresEntity::Column(c) => {
749                let (is_pk, is_composite_pk) = Self::check_column_pk_status(c, diff_index);
750                Some(JsonStatement::AddColumn {
751                    column: Box::new(c.clone()),
752                    is_pk,
753                    is_composite_pk,
754                })
755            }
756            PostgresEntity::Index(i) => Some(JsonStatement::CreateIndex { index: i.clone() }),
757            PostgresEntity::ForeignKey(f) => Some(JsonStatement::CreateFk { fk: f.clone() }),
758            PostgresEntity::PrimaryKey(p) => Some(JsonStatement::AddPk { pk: p.clone() }),
759            PostgresEntity::UniqueConstraint(u) => {
760                Some(JsonStatement::AddUnique { unique: u.clone() })
761            }
762            PostgresEntity::CheckConstraint(c) => {
763                Some(JsonStatement::AddCheck { check: c.clone() })
764            }
765            PostgresEntity::Policy(p) => Some(JsonStatement::CreatePolicy { policy: p.clone() }),
766            // Handled separately in CreateTable; privileges not yet tracked
767            PostgresEntity::Table(_) | PostgresEntity::Privilege(_) => None,
768        }
769    }
770
771    fn drop_diff_to_statement(left: &PostgresEntity) -> Option<JsonStatement> {
772        match left {
773            PostgresEntity::Schema(s) => Some(JsonStatement::DropSchema {
774                name: s.name.to_string(),
775            }),
776            PostgresEntity::Enum(e) => Some(JsonStatement::DropEnum { enum_: e.clone() }),
777            PostgresEntity::Sequence(s) => Some(JsonStatement::DropSequence {
778                sequence: s.clone(),
779            }),
780            PostgresEntity::Role(r) => Some(JsonStatement::DropRole { role: r.clone() }),
781            PostgresEntity::View(v) => Some(JsonStatement::DropView { view: v.clone() }),
782            PostgresEntity::Table(t) => Some(JsonStatement::DropTable {
783                table: t.clone(),
784                table_key: format!("{}.{}", t.schema, t.name),
785            }),
786            PostgresEntity::Column(c) => Some(JsonStatement::DropColumn {
787                column: Box::new(c.clone()),
788            }),
789            PostgresEntity::Index(i) => Some(JsonStatement::DropIndex { index: i.clone() }),
790            PostgresEntity::ForeignKey(f) => Some(JsonStatement::DropFk { fk: f.clone() }),
791            PostgresEntity::PrimaryKey(p) => Some(JsonStatement::DropPk { pk: p.clone() }),
792            PostgresEntity::UniqueConstraint(u) => {
793                Some(JsonStatement::DropUnique { unique: u.clone() })
794            }
795            PostgresEntity::CheckConstraint(c) => {
796                Some(JsonStatement::DropCheck { check: c.clone() })
797            }
798            PostgresEntity::Policy(p) => Some(JsonStatement::DropPolicy { policy: p.clone() }),
799            PostgresEntity::Privilege(_) => None, // Privileges not yet tracked
800        }
801    }
802
803    /// Check whether `old` is a subsequence of `new` — i.e. every old value
804    /// still exists and their relative order is preserved, so the change is
805    /// expressible with `ALTER TYPE ... ADD VALUE`.
806    fn enum_values_are_pure_additions(
807        old: &[std::borrow::Cow<'static, str>],
808        new: &[std::borrow::Cow<'static, str>],
809    ) -> bool {
810        let mut new_iter = new.iter();
811        old.iter()
812            .all(|old_value| new_iter.any(|new_value| new_value == old_value))
813    }
814
815    /// Collect the columns of the current schema whose type is the given enum.
816    fn enum_dependent_columns(ddl: &PostgresDDL, enum_: &Enum) -> Vec<Column> {
817        ddl.columns
818            .list()
819            .iter()
820            .filter(|column| {
821                column.sql_type.as_ref() == enum_.name.as_ref()
822                    && column.type_schema.as_deref().unwrap_or("public") == enum_.schema.as_ref()
823            })
824            .map(|column| (*column).clone())
825            .collect()
826    }
827
828    fn alter_diff_to_statement(
829        left: Option<&PostgresEntity>,
830        right: Option<&PostgresEntity>,
831        cur_ddl: Option<&PostgresDDL>,
832    ) -> Option<JsonStatement> {
833        match (left, right) {
834            (Some(PostgresEntity::Enum(old)), Some(PostgresEntity::Enum(new))) => {
835                if old.values == new.values {
836                    return None;
837                }
838                if !Self::enum_values_are_pure_additions(&old.values, &new.values) {
839                    // Removed or reordered values: PostgreSQL cannot express
840                    // this with ALTER TYPE, so recreate the type
841                    // (drizzle-kit parity).
842                    let columns = cur_ddl
843                        .map(|ddl| Self::enum_dependent_columns(ddl, new))
844                        .unwrap_or_default();
845                    return Some(JsonStatement::RecreateEnum {
846                        old_enum: old.clone(),
847                        new_enum: new.clone(),
848                        columns,
849                    });
850                }
851                let mut diffs = Vec::new();
852                for (idx, val) in new.values.iter().enumerate() {
853                    if !old.values.iter().any(|v| v == val) {
854                        let before_value = new
855                            .values
856                            .iter()
857                            .skip(idx + 1)
858                            .find(|candidate| {
859                                old.values.iter().any(|old_value| old_value == *candidate)
860                            })
861                            .map(ToString::to_string);
862                        diffs.push(EnumDiff {
863                            r#type: "added".to_string(),
864                            value: val.to_string(),
865                            before_value,
866                        });
867                    }
868                }
869                if diffs.is_empty() {
870                    None
871                } else {
872                    Some(JsonStatement::AlterEnum {
873                        from: old.clone(),
874                        to: new.clone(),
875                        diff: diffs,
876                    })
877                }
878            }
879            (Some(PostgresEntity::Column(old)), Some(PostgresEntity::Column(new))) => {
880                // PostgreSQL doesn't support ALTER COLUMN ... ADD GENERATED AS
881                let needs_recreate = old.generated.is_none() && new.generated.is_some();
882                if needs_recreate {
883                    Some(JsonStatement::RecreateColumn {
884                        old_column: Box::new(old.clone()),
885                        new_column: Box::new(new.clone()),
886                    })
887                } else {
888                    let diff = Self::build_column_diff(old, new);
889                    let was_enum = old.type_schema.is_some();
890                    let is_enum = new.type_schema.is_some();
891                    Some(JsonStatement::AlterColumn {
892                        to: Box::new(new.clone()),
893                        was_enum,
894                        is_enum,
895                        diff,
896                    })
897                }
898            }
899            (Some(PostgresEntity::Table(old)), Some(PostgresEntity::Table(new))) => {
900                if Self::alter_table_sql(old, new).is_some() {
901                    Some(JsonStatement::AlterTable {
902                        old_table: old.clone(),
903                        new_table: new.clone(),
904                    })
905                } else {
906                    None
907                }
908            }
909            (Some(PostgresEntity::ForeignKey(old)), Some(PostgresEntity::ForeignKey(new))) => {
910                Some(JsonStatement::RecreateFk {
911                    old_fk: old.clone(),
912                    new_fk: new.clone(),
913                })
914            }
915            (
916                Some(PostgresEntity::UniqueConstraint(old)),
917                Some(PostgresEntity::UniqueConstraint(new)),
918            ) => Some(JsonStatement::RecreateUnique {
919                old_unique: old.clone(),
920                new_unique: new.clone(),
921            }),
922            // PostgreSQL doesn't support ALTER VIEW for definition changes,
923            // so we drop and recreate the view.
924            (Some(PostgresEntity::View(old)), Some(PostgresEntity::View(new))) => {
925                if old.is_existing || new.is_existing {
926                    None
927                } else {
928                    Some(JsonStatement::AlterView {
929                        old_view: Box::new(old.clone()),
930                        new_view: Box::new(new.clone()),
931                    })
932                }
933            }
934            // Index definition changes require drop + recreate.
935            (Some(PostgresEntity::Index(old)), Some(PostgresEntity::Index(new))) => {
936                Some(JsonStatement::RecreateIndex {
937                    old_index: Box::new(old.clone()),
938                    new_index: Box::new(new.clone()),
939                })
940            }
941            (Some(PostgresEntity::PrimaryKey(old)), Some(PostgresEntity::PrimaryKey(new))) => {
942                Some(JsonStatement::RecreatePk {
943                    old_pk: old.clone(),
944                    new_pk: new.clone(),
945                })
946            }
947            (
948                Some(PostgresEntity::CheckConstraint(old)),
949                Some(PostgresEntity::CheckConstraint(new)),
950            ) => Some(JsonStatement::RecreateCheck {
951                old_check: old.clone(),
952                new_check: new.clone(),
953            }),
954            (Some(PostgresEntity::Policy(old)), Some(PostgresEntity::Policy(new))) => {
955                Some(JsonStatement::RecreatePolicy {
956                    old_policy: Box::new(old.clone()),
957                    new_policy: Box::new(new.clone()),
958                })
959            }
960            (Some(PostgresEntity::Sequence(old)), Some(PostgresEntity::Sequence(new))) => {
961                if Self::alter_sequence_sql(old, new).is_some() {
962                    Some(JsonStatement::AlterSequence {
963                        old_sequence: old.clone(),
964                        new_sequence: new.clone(),
965                    })
966                } else {
967                    None
968                }
969            }
970            (Some(PostgresEntity::Role(old)), Some(PostgresEntity::Role(new))) => {
971                if Self::alter_role_sql(old, new).is_some() {
972                    Some(JsonStatement::AlterRole {
973                        old_role: old.clone(),
974                        new_role: new.clone(),
975                    })
976                } else {
977                    None
978                }
979            }
980            _ => None,
981        }
982    }
983
984    /// Check if a column is part of a newly created primary key.
985    /// Returns (`is_pk`, `is_composite_pk)`:
986    /// - `is_pk`: true if this is a single-column PK with default naming
987    ///   **and the parent table itself is created in this diff**. Columns
988    ///   added to a pre-existing table never render an inline ` PRIMARY KEY`
989    ///   — the PK arrives via a separate `ADD CONSTRAINT ..._pkey`
990    ///   statement, and emitting both would create two constraints.
991    /// - `is_composite_pk`: true if this column is part of a multi-column PK
992    fn check_column_pk_status(col: &Column, diff_index: &DiffIndex<'_>) -> (bool, bool) {
993        let table_key = format!("{}.{}", col.schema, col.table);
994        let table_created_this_diff = diff_index
995            .table_diff(&table_key)
996            .is_some_and(|table_diff| table_diff.diff_type == DiffType::Create);
997
998        if let Some(entries) = diff_index.created_by_table.get(&table_key) {
999            for pk in &entries.primary_keys {
1000                if pk.columns.contains(&col.name) {
1001                    let is_composite = pk.columns.len() > 1;
1002                    let default_pk_name = format!("{}_pkey", col.table);
1003                    let is_single_pk = table_created_this_diff
1004                        && pk.columns.len() == 1
1005                        && pk.name == default_pk_name;
1006                    return (is_single_pk, is_composite);
1007                }
1008            }
1009        }
1010
1011        (false, false)
1012    }
1013
1014    /// Build a granular diff structure for column alterations.
1015    /// Tracks changes to type, default, notNull, generated, and identity.
1016    fn build_column_diff(old: &Column, new: &Column) -> HashMap<String, serde_json::Value> {
1017        let mut diff = HashMap::new();
1018
1019        // Type change
1020        if old.sql_type != new.sql_type
1021            || old.type_schema != new.type_schema
1022            || old.dimensions != new.dimensions
1023        {
1024            let mut type_diff = serde_json::Map::new();
1025            type_diff.insert("from".to_string(), serde_json::json!(old.sql_type));
1026            type_diff.insert("to".to_string(), serde_json::json!(new.sql_type));
1027            type_diff.insert(
1028                "fromDimensions".to_string(),
1029                serde_json::json!(old.dimensions),
1030            );
1031            type_diff.insert(
1032                "toDimensions".to_string(),
1033                serde_json::json!(new.dimensions),
1034            );
1035            diff.insert("type".to_string(), serde_json::Value::Object(type_diff));
1036
1037            if old.type_schema != new.type_schema {
1038                let mut ts_diff = serde_json::Map::new();
1039                ts_diff.insert("from".to_string(), serde_json::json!(old.type_schema));
1040                ts_diff.insert("to".to_string(), serde_json::json!(new.type_schema));
1041                diff.insert("typeSchema".to_string(), serde_json::Value::Object(ts_diff));
1042            }
1043        }
1044
1045        // Default change
1046        if old.default != new.default {
1047            let mut default_diff = serde_json::Map::new();
1048            default_diff.insert("from".to_string(), serde_json::json!(old.default));
1049            default_diff.insert("to".to_string(), serde_json::json!(new.default));
1050            diff.insert(
1051                "default".to_string(),
1052                serde_json::Value::Object(default_diff),
1053            );
1054        }
1055
1056        // NOT NULL change
1057        if old.not_null != new.not_null {
1058            let mut nn_diff = serde_json::Map::new();
1059            nn_diff.insert("from".to_string(), serde_json::json!(old.not_null));
1060            nn_diff.insert("to".to_string(), serde_json::json!(new.not_null));
1061            diff.insert("notNull".to_string(), serde_json::Value::Object(nn_diff));
1062        }
1063
1064        // Generated column change
1065        if old.generated != new.generated {
1066            let mut gen_diff = serde_json::Map::new();
1067            gen_diff.insert("from".to_string(), serde_json::json!(old.generated));
1068            gen_diff.insert("to".to_string(), serde_json::json!(new.generated));
1069            diff.insert("generated".to_string(), serde_json::Value::Object(gen_diff));
1070        }
1071
1072        // Identity change
1073        if old.identity != new.identity {
1074            let mut id_diff = serde_json::Map::new();
1075            id_diff.insert("from".to_string(), serde_json::json!(old.identity));
1076            id_diff.insert("to".to_string(), serde_json::json!(new.identity));
1077            diff.insert("identity".to_string(), serde_json::Value::Object(id_diff));
1078        }
1079
1080        if old.comment != new.comment {
1081            let mut comment_diff = serde_json::Map::new();
1082            comment_diff.insert("from".to_string(), serde_json::json!(old.comment));
1083            comment_diff.insert("to".to_string(), serde_json::json!(new.comment));
1084            diff.insert(
1085                "comment".to_string(),
1086                serde_json::Value::Object(comment_diff),
1087            );
1088        }
1089
1090        diff
1091    }
1092
1093    fn schema_prefix(schema: &str) -> String {
1094        if schema == "public" {
1095            String::new()
1096        } else {
1097            format!("{}.", Self::quote_ident(schema))
1098        }
1099    }
1100
1101    fn quote_ident(ident: &str) -> String {
1102        format!("\"{}\"", ident.replace('"', "\"\""))
1103    }
1104
1105    fn quote_literal(value: &str) -> String {
1106        format!("'{}'", value.replace('\'', "''"))
1107    }
1108
1109    fn qualified_name(schema: &str, name: &str) -> String {
1110        format!("{}{}", Self::schema_prefix(schema), Self::quote_ident(name))
1111    }
1112
1113    fn comment_value_sql(comment: Option<&str>) -> String {
1114        comment.map_or_else(|| "NULL".to_string(), Self::quote_literal)
1115    }
1116
1117    fn comment_on_table_sql(schema: &str, table: &str, comment: Option<&str>) -> String {
1118        format!(
1119            "COMMENT ON TABLE {} IS {};",
1120            Self::qualified_name(schema, table),
1121            Self::comment_value_sql(comment)
1122        )
1123    }
1124
1125    fn comment_on_column_sql(
1126        schema: &str,
1127        table: &str,
1128        column: &str,
1129        comment: Option<&str>,
1130    ) -> String {
1131        format!(
1132            "COMMENT ON COLUMN {}.{} IS {};",
1133            Self::qualified_name(schema, table),
1134            Self::quote_ident(column),
1135            Self::comment_value_sql(comment)
1136        )
1137    }
1138
1139    fn created_table_comments_sql(table: &RichTable) -> Vec<String> {
1140        let ddl_table = rich_table_to_table(table);
1141        TableSql::new(&ddl_table)
1142            .columns(&table.columns)
1143            .create_comments_sql()
1144    }
1145
1146    /// Render a column's full type, schema-qualifying and quoting custom
1147    /// (enum) type names and appending array dimensions.
1148    fn column_type_sql(col: &Column) -> String {
1149        col.type_sql()
1150    }
1151
1152    fn identity_sql(col: &Column, id: &super::ddl::Identity) -> Option<String> {
1153        use super::ddl::IdentityType;
1154        use super::grammar::PgTypeCategory;
1155
1156        if PgTypeCategory::from_sql_type(&col.sql_type).is_serial() {
1157            return None;
1158        }
1159
1160        let type_str = match id.type_ {
1161            IdentityType::Always => "ALWAYS",
1162            IdentityType::ByDefault => "BY DEFAULT",
1163        };
1164
1165        let mut sql = format!(" GENERATED {type_str} AS IDENTITY");
1166        let mut options = Vec::new();
1167        if let Some(increment) = id.increment.as_ref() {
1168            options.push(format!("INCREMENT BY {increment}"));
1169        }
1170        if let Some(min) = id.min_value.as_ref() {
1171            options.push(format!("MINVALUE {min}"));
1172        }
1173        if let Some(max) = id.max_value.as_ref() {
1174            options.push(format!("MAXVALUE {max}"));
1175        }
1176        if let Some(start) = id.start_with.as_ref() {
1177            options.push(format!("START WITH {start}"));
1178        }
1179        if let Some(cache) = id.cache {
1180            options.push(format!("CACHE {cache}"));
1181        }
1182        if id.cycle.unwrap_or(false) {
1183            options.push("CYCLE".to_string());
1184        }
1185        if !options.is_empty() {
1186            let _ = write!(sql, " ({})", options.join(" "));
1187        }
1188        Some(sql)
1189    }
1190
1191    /// Build the `SET GENERATED ... / SET <sequence option>` chain for an
1192    /// identity column whose configuration changed. `old` is the serialized
1193    /// [`super::ddl::Identity`] taken from the column diff (camelCase keys).
1194    /// Returns `None` when nothing tracked actually changed.
1195    fn identity_set_options_sql(
1196        new: &super::ddl::Identity,
1197        old: &serde_json::Value,
1198    ) -> Option<String> {
1199        use super::ddl::IdentityType;
1200
1201        let old_str = |key: &str| old.get(key).and_then(|v| v.as_str());
1202        let mut pieces = Vec::new();
1203
1204        let new_type = match new.type_ {
1205            IdentityType::Always => "always",
1206            IdentityType::ByDefault => "byDefault",
1207        };
1208        if old_str("type").is_some_and(|t| t != new_type) {
1209            pieces.push(match new.type_ {
1210                IdentityType::Always => "SET GENERATED ALWAYS".to_string(),
1211                IdentityType::ByDefault => "SET GENERATED BY DEFAULT".to_string(),
1212            });
1213        }
1214
1215        if old_str("increment") != new.increment.as_deref() {
1216            let increment = new.increment.as_deref().unwrap_or("1");
1217            pieces.push(format!("SET INCREMENT BY {increment}"));
1218        }
1219        if old_str("minValue") != new.min_value.as_deref() {
1220            match new.min_value.as_deref() {
1221                Some(min) => pieces.push(format!("SET MINVALUE {min}")),
1222                None => pieces.push("SET NO MINVALUE".to_string()),
1223            }
1224        }
1225        if old_str("maxValue") != new.max_value.as_deref() {
1226            match new.max_value.as_deref() {
1227                Some(max) => pieces.push(format!("SET MAXVALUE {max}")),
1228                None => pieces.push("SET NO MAXVALUE".to_string()),
1229            }
1230        }
1231        if old_str("startWith") != new.start_with.as_deref()
1232            && let Some(start) = new.start_with.as_deref()
1233        {
1234            pieces.push(format!("SET START WITH {start}"));
1235        }
1236        let old_cache = old.get("cache").and_then(serde_json::Value::as_i64);
1237        if old_cache != new.cache.map(i64::from) {
1238            let cache = new.cache.unwrap_or(1);
1239            pieces.push(format!("SET CACHE {cache}"));
1240        }
1241        let old_cycle = old
1242            .get("cycle")
1243            .and_then(serde_json::Value::as_bool)
1244            .unwrap_or(false);
1245        let new_cycle = new.cycle.unwrap_or(false);
1246        if old_cycle != new_cycle {
1247            pieces.push(if new_cycle {
1248                "SET CYCLE".to_string()
1249            } else {
1250                "SET NO CYCLE".to_string()
1251            });
1252        }
1253
1254        if pieces.is_empty() {
1255            None
1256        } else {
1257            Some(pieces.join(" "))
1258        }
1259    }
1260
1261    fn create_sequence_sql(s: &super::ddl::Sequence) -> String {
1262        s.create_sequence_sql()
1263    }
1264
1265    fn create_table_sql(table: &RichTable) -> String {
1266        let ddl_table = rich_table_to_table(table);
1267        TableSql::new(&ddl_table)
1268            .columns(&table.columns)
1269            .primary_key(table.pk.as_ref())
1270            .foreign_keys(&table.foreign_keys)
1271            .unique_constraints(&table.uniques)
1272            .check_constraints(&table.checks)
1273            .create_table_sql()
1274    }
1275
1276    fn drop_constraint_sql(schema: &str, table: &str, name: &str) -> String {
1277        format!(
1278            "ALTER TABLE {} DROP CONSTRAINT {};",
1279            Self::qualified_name(schema, table),
1280            Self::quote_ident(name)
1281        )
1282    }
1283
1284    fn create_enum_sql(e: &super::ddl::Enum) -> String {
1285        e.create_enum_sql()
1286    }
1287
1288    fn alter_enum_sql(to: &super::ddl::Enum, diff: &[EnumDiff]) -> String {
1289        diff.iter()
1290            .map(|d| to.add_value_sql(&d.value, d.before_value.as_deref()))
1291            .collect::<Vec<_>>()
1292            .join("\n")
1293    }
1294
1295    fn add_pk_sql(pk: &super::ddl::PrimaryKey) -> String {
1296        pk.add_pk_sql()
1297    }
1298
1299    fn add_unique_sql(unique: &super::ddl::UniqueConstraint) -> String {
1300        unique.add_unique_sql()
1301    }
1302
1303    fn recreate_column_sql(old_column: &Column, new_column: &Column) -> String {
1304        // Recreate column by dropping and adding.
1305        // Used for adding generated expressions, which PostgreSQL doesn't support via ALTER.
1306        let table_key = Self::qualified_name(&new_column.schema, &new_column.table);
1307        let drop_sql = format!(
1308            "ALTER TABLE {} DROP COLUMN {};",
1309            table_key,
1310            Self::quote_ident(&old_column.name)
1311        );
1312        let add_sql = format!(
1313            "ALTER TABLE {} ADD COLUMN {};",
1314            table_key,
1315            new_column.to_column_sql()
1316        );
1317        if new_column.comment.is_some() {
1318            format!(
1319                "{drop_sql}\n{add_sql}\n{}",
1320                Self::comment_on_column_sql(
1321                    &new_column.schema,
1322                    &new_column.table,
1323                    &new_column.name,
1324                    new_column.comment.as_deref(),
1325                )
1326            )
1327        } else {
1328            format!("{drop_sql}\n{add_sql}")
1329        }
1330    }
1331
1332    fn add_column_sql(column: &Column, is_pk: bool) -> String {
1333        let pk_clause = if is_pk { " PRIMARY KEY" } else { "" };
1334        let add_sql = format!(
1335            "ALTER TABLE {} ADD COLUMN {}{};",
1336            Self::qualified_name(&column.schema, &column.table),
1337            column.to_column_sql(),
1338            pk_clause
1339        );
1340        if column.comment.is_some() {
1341            format!(
1342                "{add_sql}\n{}",
1343                Self::comment_on_column_sql(
1344                    &column.schema,
1345                    &column.table,
1346                    &column.name,
1347                    column.comment.as_deref(),
1348                )
1349            )
1350        } else {
1351            add_sql
1352        }
1353    }
1354
1355    fn add_check_sql(check: &super::ddl::CheckConstraint) -> String {
1356        check.add_check_sql()
1357    }
1358
1359    fn drop_policy_sql(policy: &super::ddl::Policy) -> String {
1360        format!(
1361            "DROP POLICY {} ON {};",
1362            Self::quote_ident(&policy.name),
1363            Self::qualified_name(&policy.schema, &policy.table)
1364        )
1365    }
1366
1367    fn alter_view_sql(old_view: &View, new_view: &View) -> String {
1368        // PostgreSQL doesn't support ALTER VIEW for definition changes,
1369        // so we drop and recreate the view.
1370        let drop_sql = Self::drop_view_sql(old_view);
1371        let create_sql = Self::create_view_sql(new_view);
1372        format!("{drop_sql}\n{create_sql}")
1373    }
1374
1375    /// DROP INDEX + CREATE INDEX from the new definition. The old index's
1376    /// CONCURRENTLY flag drives the drop, the new definition's flag drives
1377    /// the create.
1378    fn recreate_index_sql(old_index: &Index, new_index: &Index) -> String {
1379        let concurrently = if old_index.concurrently {
1380            "CONCURRENTLY "
1381        } else {
1382            ""
1383        };
1384        let drop_sql = format!(
1385            "DROP INDEX {}{};",
1386            concurrently,
1387            Self::qualified_name(&old_index.schema, &old_index.name)
1388        );
1389        format!("{drop_sql}\n{}", Self::create_index_sql(new_index))
1390    }
1391
1392    /// ALTER SEQUENCE with only the changed options. Returns `None` when no
1393    /// tracked option differs.
1394    fn alter_sequence_sql(old: &Sequence, new: &Sequence) -> Option<String> {
1395        let mut options = Vec::new();
1396
1397        if old.increment_by != new.increment_by {
1398            let increment = new.increment_by.as_deref().unwrap_or("1");
1399            options.push(format!("INCREMENT BY {increment}"));
1400        }
1401        if old.min_value != new.min_value {
1402            match new.min_value.as_deref() {
1403                Some(min) => options.push(format!("MINVALUE {min}")),
1404                None => options.push("NO MINVALUE".to_string()),
1405            }
1406        }
1407        if old.max_value != new.max_value {
1408            match new.max_value.as_deref() {
1409                Some(max) => options.push(format!("MAXVALUE {max}")),
1410                None => options.push("NO MAXVALUE".to_string()),
1411            }
1412        }
1413        if old.start_with != new.start_with
1414            && let Some(start) = new.start_with.as_deref()
1415        {
1416            options.push(format!("START WITH {start}"));
1417        }
1418        if old.cache_size != new.cache_size {
1419            let cache = new.cache_size.unwrap_or(1);
1420            options.push(format!("CACHE {cache}"));
1421        }
1422        if old.cycle.unwrap_or(false) != new.cycle.unwrap_or(false) {
1423            options.push(if new.cycle.unwrap_or(false) {
1424                "CYCLE".to_string()
1425            } else {
1426                "NO CYCLE".to_string()
1427            });
1428        }
1429
1430        if options.is_empty() {
1431            return None;
1432        }
1433        Some(format!(
1434            "ALTER SEQUENCE {} {};",
1435            Self::qualified_name(&new.schema, &new.name),
1436            options.join(" ")
1437        ))
1438    }
1439
1440    /// ALTER ROLE with only the changed flags. Returns `None` when no
1441    /// tracked flag differs.
1442    fn alter_role_sql(old: &Role, new: &Role) -> Option<String> {
1443        let mut options = Vec::new();
1444
1445        if old.create_db.unwrap_or(false) != new.create_db.unwrap_or(false) {
1446            options.push(if new.create_db.unwrap_or(false) {
1447                "CREATEDB"
1448            } else {
1449                "NOCREATEDB"
1450            });
1451        }
1452        if old.create_role.unwrap_or(false) != new.create_role.unwrap_or(false) {
1453            options.push(if new.create_role.unwrap_or(false) {
1454                "CREATEROLE"
1455            } else {
1456                "NOCREATEROLE"
1457            });
1458        }
1459        if old.inherit.unwrap_or(true) != new.inherit.unwrap_or(true) {
1460            options.push(if new.inherit.unwrap_or(true) {
1461                "INHERIT"
1462            } else {
1463                "NOINHERIT"
1464            });
1465        }
1466        if old.can_login.unwrap_or(false) != new.can_login.unwrap_or(false) {
1467            options.push(if new.can_login.unwrap_or(false) {
1468                "LOGIN"
1469            } else {
1470                "NOLOGIN"
1471            });
1472        }
1473        if old.bypass_rls.unwrap_or(false) != new.bypass_rls.unwrap_or(false) {
1474            options.push(if new.bypass_rls.unwrap_or(false) {
1475                "BYPASSRLS"
1476            } else {
1477                "NOBYPASSRLS"
1478            });
1479        }
1480
1481        let mut sql_options: Vec<String> = options.iter().map(ToString::to_string).collect();
1482        if old.conn_limit != new.conn_limit {
1483            sql_options.push(format!("CONNECTION LIMIT {}", new.conn_limit.unwrap_or(-1)));
1484        }
1485
1486        if sql_options.is_empty() {
1487            return None;
1488        }
1489        Some(format!(
1490            "ALTER ROLE {} WITH {};",
1491            Self::quote_ident(&new.name),
1492            sql_options.join(" ")
1493        ))
1494    }
1495
1496    /// Recreate an enum whose values were removed or reordered, drizzle-kit
1497    /// style: convert dependent columns to text, drop and recreate the type,
1498    /// convert the columns back with `USING ::text::type`, restore defaults.
1499    fn recreate_enum_sql(new_enum: &Enum, columns: &[Column]) -> String {
1500        let type_name = Self::qualified_name(&new_enum.schema, &new_enum.name);
1501        let mut stmts = Vec::new();
1502
1503        for column in columns {
1504            if column.default.is_some() {
1505                stmts.push(format!(
1506                    "ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT;",
1507                    Self::qualified_name(&column.schema, &column.table),
1508                    Self::quote_ident(&column.name)
1509                ));
1510            }
1511        }
1512        for column in columns {
1513            stmts.push(format!(
1514                "ALTER TABLE {} ALTER COLUMN {} SET DATA TYPE text USING {}::text;",
1515                Self::qualified_name(&column.schema, &column.table),
1516                Self::quote_ident(&column.name),
1517                Self::quote_ident(&column.name)
1518            ));
1519        }
1520        stmts.push(format!("DROP TYPE {type_name};"));
1521        stmts.push(Self::create_enum_sql(new_enum));
1522        for column in columns {
1523            stmts.push(format!(
1524                "ALTER TABLE {} ALTER COLUMN {} SET DATA TYPE {} USING {}::text::{};",
1525                Self::qualified_name(&column.schema, &column.table),
1526                Self::quote_ident(&column.name),
1527                type_name,
1528                Self::quote_ident(&column.name),
1529                type_name
1530            ));
1531        }
1532        for column in columns {
1533            if let Some(default) = column.default.as_deref() {
1534                stmts.push(format!(
1535                    "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {};",
1536                    Self::qualified_name(&column.schema, &column.table),
1537                    Self::quote_ident(&column.name),
1538                    default
1539                ));
1540            }
1541        }
1542
1543        stmts.join("\n")
1544    }
1545
1546    /// Render a statement as one or more single-command SQL strings.
1547    ///
1548    /// Several renderers pack multiple commands into one string separated by
1549    /// newlines (constraint/index/policy/enum recreates, chained alters,
1550    /// COMMENT ON riders). Drivers execute each returned entry through a
1551    /// prepared statement, which rejects multi-command text — so split them
1552    /// here. Joined output for display stays available via
1553    /// [`Self::statement_to_sql`].
1554    pub(crate) fn statement_to_sqls(stmt: JsonStatement) -> Vec<String> {
1555        Self::split_joined_commands(Self::statement_to_sql(stmt))
1556    }
1557
1558    /// Split renderer output holding several `;`-terminated commands
1559    /// separated by newlines into individual statements. A boundary exists
1560    /// only after a line ending in `;` — interior lines of a single
1561    /// multi-line command (e.g. a CREATE TABLE body) end with `,` or `(`,
1562    /// never `;`, so such commands stay whole.
1563    fn split_joined_commands(sql: String) -> Vec<String> {
1564        if !sql.contains('\n') {
1565            return vec![sql];
1566        }
1567        let mut out = Vec::new();
1568        let mut current = String::new();
1569        for line in sql.lines() {
1570            if !current.is_empty() {
1571                current.push('\n');
1572            }
1573            current.push_str(line);
1574            if line.trim_end().ends_with(';') {
1575                out.push(std::mem::take(&mut current));
1576            }
1577        }
1578        if !current.trim().is_empty() {
1579            out.push(current);
1580        }
1581        out
1582    }
1583
1584    pub(crate) fn statement_to_sql(stmt: JsonStatement) -> String {
1585        match stmt {
1586            JsonStatement::CreateSchema { name } => {
1587                format!("CREATE SCHEMA {};", Self::quote_ident(&name))
1588            }
1589            JsonStatement::DropSchema { name } => {
1590                format!("DROP SCHEMA {};", Self::quote_ident(&name))
1591            }
1592            JsonStatement::RenameSchema { from, to } => {
1593                format!(
1594                    "ALTER SCHEMA {} RENAME TO {};",
1595                    Self::quote_ident(&from.name),
1596                    Self::quote_ident(&to.name)
1597                )
1598            }
1599            JsonStatement::CreateEnum { enum_: e } => Self::create_enum_sql(&e),
1600            JsonStatement::DropEnum { enum_: e } => {
1601                format!("DROP TYPE {};", Self::qualified_name(&e.schema, &e.name))
1602            }
1603            JsonStatement::AlterEnum { from: _, to, diff } => Self::alter_enum_sql(&to, &diff),
1604            JsonStatement::CreateSequence { sequence: s } => Self::create_sequence_sql(&s),
1605            JsonStatement::DropSequence { sequence: s } => format!(
1606                "DROP SEQUENCE {};",
1607                Self::qualified_name(&s.schema, &s.name)
1608            ),
1609            JsonStatement::CreateTable { table } => Self::create_table_sql(&table),
1610            JsonStatement::DropTable { table, .. } => format!(
1611                "DROP TABLE {};",
1612                Self::qualified_name(&table.schema, &table.name)
1613            ),
1614            JsonStatement::RenameTable { schema, from, to } => format!(
1615                "ALTER TABLE {} RENAME TO {};",
1616                Self::qualified_name(&schema, &from),
1617                Self::quote_ident(&to)
1618            ),
1619            JsonStatement::AddColumn { column, is_pk, .. } => Self::add_column_sql(&column, is_pk),
1620            JsonStatement::DropColumn { column } => format!(
1621                "ALTER TABLE {} DROP COLUMN {};",
1622                Self::qualified_name(&column.schema, &column.table),
1623                Self::quote_ident(&column.name)
1624            ),
1625            JsonStatement::RenameColumn { from, to } => format!(
1626                "ALTER TABLE {} RENAME COLUMN {} TO {};",
1627                Self::qualified_name(&from.schema, &from.table),
1628                Self::quote_ident(&from.name),
1629                Self::quote_ident(&to.name)
1630            ),
1631            JsonStatement::AlterColumn {
1632                to,
1633                was_enum,
1634                is_enum,
1635                diff,
1636            } => Self::alter_column_sql(&to, was_enum, is_enum, &diff),
1637            JsonStatement::RecreateColumn {
1638                old_column,
1639                new_column,
1640            } => Self::recreate_column_sql(&old_column, &new_column),
1641            JsonStatement::CreateIndex { index } => Self::create_index_sql(&index),
1642            JsonStatement::DropIndex { index } => format!(
1643                "DROP INDEX {};",
1644                Self::qualified_name(&index.schema, &index.name)
1645            ),
1646            JsonStatement::CreateFk { fk } => Self::add_fk_sql(&fk),
1647            JsonStatement::DropFk { fk } => {
1648                Self::drop_constraint_sql(&fk.schema, &fk.table, &fk.name)
1649            }
1650            JsonStatement::CreateView { view } => Self::create_view_sql(&view),
1651            JsonStatement::DropView { view } => Self::drop_view_sql(&view),
1652            JsonStatement::AlterView { old_view, new_view } => {
1653                Self::alter_view_sql(&old_view, &new_view)
1654            }
1655            JsonStatement::AddPk { pk } => Self::add_pk_sql(&pk),
1656            JsonStatement::DropPk { pk } => {
1657                Self::drop_constraint_sql(&pk.schema, &pk.table, &pk.name)
1658            }
1659            JsonStatement::AddUnique { unique } => Self::add_unique_sql(&unique),
1660            JsonStatement::DropUnique { unique } => {
1661                Self::drop_constraint_sql(&unique.schema, &unique.table, &unique.name)
1662            }
1663            JsonStatement::AddCheck { check } => Self::add_check_sql(&check),
1664            JsonStatement::DropCheck { check } => {
1665                Self::drop_constraint_sql(&check.schema, &check.table, &check.name)
1666            }
1667            JsonStatement::CreateRole { role } => Self::create_role_sql(&role),
1668            JsonStatement::DropRole { role } => {
1669                format!("DROP ROLE {};", Self::quote_ident(&role.name))
1670            }
1671            JsonStatement::CreatePolicy { policy } => Self::create_policy_sql(&policy),
1672            JsonStatement::DropPolicy { policy } => Self::drop_policy_sql(&policy),
1673            JsonStatement::AlterTable {
1674                old_table,
1675                new_table,
1676            } => Self::alter_table_sql(&old_table, &new_table)
1677                .expect("alter table statement was prechecked"),
1678            JsonStatement::RecreateFk { old_fk, new_fk } => format!(
1679                "{}\n{}",
1680                Self::drop_constraint_sql(&old_fk.schema, &old_fk.table, &old_fk.name),
1681                Self::add_fk_sql(&new_fk)
1682            ),
1683            JsonStatement::RecreateUnique {
1684                old_unique,
1685                new_unique,
1686            } => format!(
1687                "{}\n{}",
1688                Self::drop_constraint_sql(&old_unique.schema, &old_unique.table, &old_unique.name),
1689                Self::add_unique_sql(&new_unique)
1690            ),
1691            JsonStatement::RecreateIndex {
1692                old_index,
1693                new_index,
1694            } => Self::recreate_index_sql(&old_index, &new_index),
1695            JsonStatement::RecreatePk { old_pk, new_pk } => format!(
1696                "{}\n{}",
1697                Self::drop_constraint_sql(&old_pk.schema, &old_pk.table, &old_pk.name),
1698                Self::add_pk_sql(&new_pk)
1699            ),
1700            JsonStatement::RecreateCheck {
1701                old_check,
1702                new_check,
1703            } => format!(
1704                "{}\n{}",
1705                Self::drop_constraint_sql(&old_check.schema, &old_check.table, &old_check.name),
1706                Self::add_check_sql(&new_check)
1707            ),
1708            JsonStatement::RecreatePolicy {
1709                old_policy,
1710                new_policy,
1711            } => format!(
1712                "{}\n{}",
1713                Self::drop_policy_sql(&old_policy),
1714                Self::create_policy_sql(&new_policy)
1715            ),
1716            JsonStatement::AlterSequence {
1717                old_sequence,
1718                new_sequence,
1719            } => Self::alter_sequence_sql(&old_sequence, &new_sequence)
1720                .expect("alter sequence statement was prechecked"),
1721            JsonStatement::AlterRole { old_role, new_role } => {
1722                Self::alter_role_sql(&old_role, &new_role)
1723                    .expect("alter role statement was prechecked")
1724            }
1725            JsonStatement::RecreateEnum {
1726                new_enum, columns, ..
1727            } => Self::recreate_enum_sql(&new_enum, &columns),
1728        }
1729    }
1730
1731    fn create_index_sql(index: &Index) -> String {
1732        index.create_index_sql()
1733    }
1734
1735    fn create_view_sql(view: &View) -> String {
1736        view.create_view_sql()
1737    }
1738
1739    fn drop_view_sql(view: &View) -> String {
1740        let mat = if view.materialized {
1741            "MATERIALIZED "
1742        } else {
1743            ""
1744        };
1745        format!(
1746            "DROP {}VIEW {};",
1747            mat,
1748            Self::qualified_name(&view.schema, &view.name)
1749        )
1750    }
1751
1752    fn alter_column_sql(
1753        to: &Column,
1754        was_enum: bool,
1755        is_enum: bool,
1756        diff: &HashMap<String, serde_json::Value>,
1757    ) -> String {
1758        let table_key = Self::qualified_name(&to.schema, &to.table);
1759        let mut stmts = Vec::new();
1760
1761        if diff.contains_key("type") {
1762            let type_sql = Self::column_type_sql(to);
1763            // Enum-to-enum conversions must round-trip through text
1764            // (drizzle-kit parity): PostgreSQL has no direct cast between
1765            // distinct enum types.
1766            let using_cast = if was_enum && is_enum {
1767                format!("{}::text::{type_sql}", Self::quote_ident(&to.name))
1768            } else {
1769                format!("{}::{type_sql}", Self::quote_ident(&to.name))
1770            };
1771            stmts.push(format!(
1772                "ALTER TABLE {} ALTER COLUMN {} SET DATA TYPE {} USING {};",
1773                table_key,
1774                Self::quote_ident(&to.name),
1775                type_sql,
1776                using_cast
1777            ));
1778        }
1779
1780        if diff.contains_key("notNull") {
1781            if to.not_null {
1782                stmts.push(format!(
1783                    "ALTER TABLE {} ALTER COLUMN {} SET NOT NULL;",
1784                    table_key,
1785                    Self::quote_ident(&to.name)
1786                ));
1787            } else {
1788                stmts.push(format!(
1789                    "ALTER TABLE {} ALTER COLUMN {} DROP NOT NULL;",
1790                    table_key,
1791                    Self::quote_ident(&to.name)
1792                ));
1793            }
1794        }
1795
1796        if diff.contains_key("default") {
1797            if let Some(default) = &to.default {
1798                stmts.push(format!(
1799                    "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {};",
1800                    table_key,
1801                    Self::quote_ident(&to.name),
1802                    default
1803                ));
1804            } else {
1805                stmts.push(format!(
1806                    "ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT;",
1807                    table_key,
1808                    Self::quote_ident(&to.name)
1809                ));
1810            }
1811        }
1812
1813        if diff.contains_key("generated") && to.generated.is_none() {
1814            stmts.push(format!(
1815                "ALTER TABLE {} ALTER COLUMN {} DROP EXPRESSION;",
1816                table_key,
1817                Self::quote_ident(&to.name)
1818            ));
1819        }
1820
1821        if diff.contains_key("identity")
1822            && !super::grammar::PgTypeCategory::from_sql_type(&to.sql_type).is_serial()
1823        {
1824            let old_identity = diff
1825                .get("identity")
1826                .and_then(|change| change.get("from"))
1827                .filter(|from| !from.is_null());
1828            match (&to.identity, old_identity) {
1829                // None -> Some: ADD GENERATED ... AS IDENTITY.
1830                (Some(id), None) => {
1831                    if let Some(identity_sql) = Self::identity_sql(to, id) {
1832                        stmts.push(format!(
1833                            "ALTER TABLE {} ALTER COLUMN {} ADD{};",
1834                            table_key,
1835                            Self::quote_ident(&to.name),
1836                            identity_sql
1837                        ));
1838                    }
1839                }
1840                // Some -> Some: ALTER COLUMN ... SET GENERATED / SET <option>.
1841                // `ADD GENERATED` on an existing identity column is invalid.
1842                (Some(id), Some(old)) => {
1843                    if let Some(set_sql) = Self::identity_set_options_sql(id, old) {
1844                        stmts.push(format!(
1845                            "ALTER TABLE {} ALTER COLUMN {} {};",
1846                            table_key,
1847                            Self::quote_ident(&to.name),
1848                            set_sql
1849                        ));
1850                    }
1851                }
1852                // Some -> None: DROP IDENTITY.
1853                (None, _) => {
1854                    stmts.push(format!(
1855                        "ALTER TABLE {} ALTER COLUMN {} DROP IDENTITY;",
1856                        table_key,
1857                        Self::quote_ident(&to.name)
1858                    ));
1859                }
1860            }
1861        }
1862
1863        if diff.contains_key("comment") {
1864            stmts.push(Self::comment_on_column_sql(
1865                &to.schema,
1866                &to.table,
1867                &to.name,
1868                to.comment.as_deref(),
1869            ));
1870        }
1871
1872        if stmts.is_empty() {
1873            format!("-- No column changes for {}.{}", to.table, to.name)
1874        } else {
1875            stmts.join("\n")
1876        }
1877    }
1878
1879    fn create_role_sql(role: &super::ddl::Role) -> String {
1880        let mut sql = format!("CREATE ROLE {}", Self::quote_ident(&role.name));
1881        if role.create_db.unwrap_or(false) {
1882            sql.push_str(" CREATEDB");
1883        }
1884        if role.create_role.unwrap_or(false) {
1885            sql.push_str(" CREATEROLE");
1886        }
1887        if role.inherit.unwrap_or(true) {
1888            sql.push_str(" INHERIT");
1889        } else {
1890            sql.push_str(" NOINHERIT");
1891        }
1892        if role.can_login.unwrap_or(false) {
1893            sql.push_str(" LOGIN");
1894        }
1895        if role.bypass_rls.unwrap_or(false) {
1896            sql.push_str(" BYPASSRLS");
1897        }
1898        if let Some(conn_limit) = role.conn_limit {
1899            let _ = write!(sql, " CONNECTION LIMIT {conn_limit}");
1900        }
1901        sql.push(';');
1902        sql
1903    }
1904
1905    fn create_policy_sql(policy: &super::ddl::Policy) -> String {
1906        policy.create_policy_sql()
1907    }
1908
1909    fn add_fk_sql(fk: &ForeignKey) -> String {
1910        fk.add_fk_sql()
1911    }
1912
1913    fn alter_table_sql(old: &Table, new: &Table) -> Option<String> {
1914        let mut stmts = Vec::new();
1915        let table_name = Self::qualified_name(&new.schema, &new.name);
1916        let old_unlogged = old.is_unlogged.unwrap_or(false);
1917        let new_unlogged = new.is_unlogged.unwrap_or(false);
1918        if old_unlogged != new_unlogged {
1919            let logged = if new_unlogged { "UNLOGGED" } else { "LOGGED" };
1920            stmts.push(format!("ALTER TABLE {table_name} SET {logged};"));
1921        }
1922
1923        if old.tablespace.as_deref() != new.tablespace.as_deref() {
1924            let tablespace = new.tablespace.as_deref().unwrap_or("pg_default");
1925            stmts.push(format!(
1926                "ALTER TABLE {table_name} SET TABLESPACE {};",
1927                Self::quote_ident(tablespace)
1928            ));
1929        }
1930
1931        if old.comment.as_deref() != new.comment.as_deref() {
1932            stmts.push(Self::comment_on_table_sql(
1933                &new.schema,
1934                &new.name,
1935                new.comment.as_deref(),
1936            ));
1937        }
1938
1939        if stmts.is_empty() {
1940            None
1941        } else {
1942            Some(stmts.join("\n"))
1943        }
1944    }
1945}
1946
1947// =============================================================================
1948// Topological Sort for Table Dependencies
1949// =============================================================================
1950
1951/// Topological sort tables for CREATE: referenced tables come first
1952fn topological_sort_tables_for_create(
1953    table_keys: &[String],
1954    diff: &[EntityDiff],
1955) -> CreateTableOrder {
1956    if table_keys.len() <= 1 {
1957        return CreateTableOrder {
1958            ordered: table_keys.to_vec(),
1959            cycle_tables: HashSet::new(),
1960        };
1961    }
1962
1963    // Build a set of table keys for quick lookup
1964    let table_set: HashSet<&String> = table_keys.iter().collect();
1965
1966    // Build dependency graph: table -> tables it depends on (via FKs)
1967    let mut dependencies: HashMap<String, HashSet<String>> = HashMap::new();
1968    for table_key in table_keys {
1969        dependencies.insert(table_key.clone(), HashSet::new());
1970    }
1971
1972    // Find FK dependencies from created FKs
1973    for d in diff
1974        .iter()
1975        .filter(|d| d.kind == EntityKind::ForeignKey && d.diff_type == DiffType::Create)
1976    {
1977        if let Some(PostgresEntity::ForeignKey(fk)) = &d.right {
1978            let from_table = format!("{}.{}", fk.schema, fk.table);
1979            let to_table = format!("{}.{}", fk.schema_to, fk.table_to);
1980
1981            // from_table depends on to_table (to_table must be created first)
1982            if table_set.contains(&from_table)
1983                && table_set.contains(&to_table)
1984                && let Some(deps) = dependencies.get_mut(&from_table)
1985            {
1986                deps.insert(to_table);
1987            }
1988        }
1989    }
1990
1991    // Tables with no dependencies come first, then tables that depend on them, etc.
1992    let mut result = Vec::new();
1993    let mut remaining: HashSet<String> = table_keys.iter().cloned().collect();
1994    let mut satisfied: HashSet<String> = HashSet::new();
1995    let mut cycle_tables = HashSet::new();
1996
1997    while !remaining.is_empty() {
1998        // Find tables whose dependencies are all satisfied
1999        let ready: Vec<String> = remaining
2000            .iter()
2001            .filter(|t| {
2002                dependencies
2003                    .get(*t)
2004                    .is_none_or(|deps| deps.iter().all(|d| satisfied.contains(d)))
2005            })
2006            .cloned()
2007            .collect();
2008
2009        if ready.is_empty() {
2010            // Circular dependency: create remaining tables without their cycle FKs,
2011            // then add those constraints after all tables exist.
2012            cycle_tables = remaining.clone();
2013            result.extend(remaining);
2014            break;
2015        }
2016
2017        for t in ready {
2018            remaining.remove(&t);
2019            satisfied.insert(t.clone());
2020            result.push(t);
2021        }
2022    }
2023
2024    CreateTableOrder {
2025        ordered: result,
2026        cycle_tables,
2027    }
2028}
2029
2030/// Topological sort tables for DROP: tables with FKs come first (reverse of create)
2031fn topological_sort_tables_for_drop(table_keys: &[String], diff: &[EntityDiff]) -> Vec<String> {
2032    // For drops, reverse the create order: tables that reference others drop first
2033    let create_order = topological_sort_tables_for_create(table_keys, diff);
2034    create_order.ordered.into_iter().rev().collect()
2035}