Skip to main content

drizzle_migrations/
generate.rs

1//! Programmatic migration generation API.
2//!
3//! Diff two schema snapshots and get SQL statements — no file I/O, no CLI needed.
4//!
5//! # Snapshot-to-snapshot example
6//!
7//! ```rust
8//! use drizzle_migrations::{Snapshot, diff};
9//!
10//! let prev = Snapshot::empty(drizzle_types::Dialect::SQLite);
11//! let current = Snapshot::empty(drizzle_types::Dialect::SQLite);
12//! let migration = diff(&prev, &current).unwrap();
13//! assert!(migration.is_empty());
14//! ```
15//!
16//! # Schema-to-schema example (recommended for runtime generation)
17//!
18//! ```rust,no_run
19//! use drizzle_migrations::{DiffOptions, diff_schemas_with};
20//! use drizzle_migrations::{Schema, Snapshot};
21//! use drizzle_types::Dialect;
22//!
23//! # #[derive(Default)]
24//! # struct V1;
25//! # #[derive(Default)]
26//! # struct V2;
27//! # impl Schema for V1 {
28//! #     fn to_snapshot(&self) -> Snapshot { Snapshot::empty(Dialect::SQLite) }
29//! #     fn dialect(&self) -> Dialect { Dialect::SQLite }
30//! # }
31//! # impl Schema for V2 {
32//! #     fn to_snapshot(&self) -> Snapshot { Snapshot::empty(Dialect::SQLite) }
33//! #     fn dialect(&self) -> Dialect { Dialect::SQLite }
34//! # }
35//!
36//! let generated = diff_schemas_with(
37//!     &V1,
38//!     &V2,
39//!     &DiffOptions::new()
40//!         .rename_table("users_old", "users")
41//!         .rename_column("users", "full_name", "name")
42//!         .strict_renames(true),
43//! )?;
44//!
45//! if !generated.is_empty() {
46//!     let _sql = generated.to_sql();
47//! }
48//! # Ok::<(), drizzle_migrations::MigrationError>(())
49//! ```
50
51use crate::postgres::collection::PostgresDDL;
52use crate::schema::{Schema, Snapshot};
53use crate::sqlite::collection::SQLiteDDL;
54use crate::version::ORIGIN_UUID;
55use crate::writer::MigrationError;
56use std::borrow::Cow;
57use std::io::{self, Write};
58
59/// Generated migration payload.
60#[derive(Clone, Debug)]
61pub struct Plan {
62    /// SQL statements for the migration.
63    pub statements: Vec<String>,
64    /// Warning messages emitted while planning the migration.
65    pub warnings: Vec<String>,
66    /// Schema snapshot after this migration is applied.
67    pub snapshot: Snapshot,
68}
69
70impl Plan {
71    /// Returns true when there are no executable SQL statements.
72    #[must_use]
73    pub fn is_empty(&self) -> bool {
74        self.statements.is_empty()
75            || self
76                .statements
77                .iter()
78                .all(|statement| statement.trim().is_empty())
79    }
80
81    /// Format statements with `--> statement-breakpoint` markers.
82    #[must_use]
83    pub fn to_sql(&self) -> String {
84        self.statements.join("\n--> statement-breakpoint\n")
85    }
86
87    /// Write formatted migration SQL to a writer.
88    ///
89    /// # Errors
90    ///
91    /// Returns the underlying [`io::Error`] if writing to `writer` fails.
92    pub fn write(&self, writer: impl Write) -> io::Result<()> {
93        let mut writer = writer;
94        writer.write_all(self.to_sql().as_bytes())
95    }
96}
97
98/// Explicit rename hints used during migration generation.
99#[derive(Clone, Debug, Default, PartialEq, Eq)]
100pub struct RenameHints {
101    /// PostgreSQL schema rename hints.
102    pub schema_renames: Vec<SchemaRenameHint>,
103    /// Table rename hints.
104    pub table_renames: Vec<TableRenameHint>,
105    /// Column rename hints.
106    pub column_renames: Vec<ColumnRenameHint>,
107}
108
109impl RenameHints {
110    #[must_use]
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    #[must_use]
116    pub fn rename_schema(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
117        self.schema_renames.push(SchemaRenameHint {
118            from: from.into(),
119            to: to.into(),
120        });
121        self
122    }
123
124    #[must_use]
125    pub fn rename_table(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
126        self.table_renames.push(TableRenameHint {
127            schema: None,
128            from: from.into(),
129            to: to.into(),
130        });
131        self
132    }
133
134    #[must_use]
135    pub fn rename_table_in(
136        mut self,
137        schema: impl Into<String>,
138        from: impl Into<String>,
139        to: impl Into<String>,
140    ) -> Self {
141        self.table_renames.push(TableRenameHint {
142            schema: Some(schema.into()),
143            from: from.into(),
144            to: to.into(),
145        });
146        self
147    }
148
149    #[must_use]
150    pub fn rename_column(
151        mut self,
152        table: impl Into<String>,
153        from: impl Into<String>,
154        to: impl Into<String>,
155    ) -> Self {
156        self.column_renames.push(ColumnRenameHint {
157            schema: None,
158            table: table.into(),
159            from: from.into(),
160            to: to.into(),
161        });
162        self
163    }
164
165    #[must_use]
166    pub fn rename_column_in(
167        mut self,
168        schema: impl Into<String>,
169        table: impl Into<String>,
170        from: impl Into<String>,
171        to: impl Into<String>,
172    ) -> Self {
173        self.column_renames.push(ColumnRenameHint {
174            schema: Some(schema.into()),
175            table: table.into(),
176            from: from.into(),
177            to: to.into(),
178        });
179        self
180    }
181}
182
183/// PostgreSQL schema rename hint.
184#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct SchemaRenameHint {
186    /// Current schema name.
187    pub from: String,
188    /// New schema name.
189    pub to: String,
190}
191
192/// Table rename hint.
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct TableRenameHint {
195    /// Optional schema (`PostgreSQL` only). If omitted, defaults to `public`.
196    pub schema: Option<String>,
197    /// Current table name.
198    pub from: String,
199    /// New table name.
200    pub to: String,
201}
202
203/// Column rename hint.
204#[derive(Clone, Debug, PartialEq, Eq)]
205pub struct ColumnRenameHint {
206    /// Optional schema (`PostgreSQL` only). If omitted, defaults to `public`.
207    pub schema: Option<String>,
208    /// Table containing the column.
209    pub table: String,
210    /// Current column name.
211    pub from: String,
212    /// New column name.
213    pub to: String,
214}
215
216/// Generation options for [`diff_with`] and [`diff_schemas_with`].
217#[derive(Clone, Debug, Default, PartialEq, Eq)]
218pub struct DiffOptions {
219    /// Explicit rename hints applied before heuristic diffing.
220    pub renames: RenameHints,
221    /// If true, every hint must apply; otherwise generation fails.
222    pub strict_renames: bool,
223    /// Typed data movement for SQLite table rebuilds, bound to the exact
224    /// predecessor snapshot.
225    pub sqlite_rebuild_data: Option<crate::sqlite::SqliteRebuildDataPlanRegistry>,
226}
227
228impl DiffOptions {
229    #[must_use]
230    pub fn new() -> Self {
231        Self::default()
232    }
233
234    #[must_use]
235    pub fn with_renames(mut self, renames: RenameHints) -> Self {
236        self.renames = renames;
237        self
238    }
239
240    #[must_use]
241    pub const fn strict_renames(mut self, strict: bool) -> Self {
242        self.strict_renames = strict;
243        self
244    }
245
246    #[must_use]
247    pub fn sqlite_rebuild_data(mut self, plan: crate::sqlite::SqliteRebuildDataPlan) -> Self {
248        self.sqlite_rebuild_data = Some(crate::sqlite::SqliteRebuildDataPlanRegistry::single(plan));
249        self
250    }
251
252    #[must_use]
253    pub fn sqlite_rebuild_data_registry(
254        mut self,
255        registry: crate::sqlite::SqliteRebuildDataPlanRegistry,
256    ) -> Self {
257        self.sqlite_rebuild_data = Some(registry);
258        self
259    }
260
261    #[must_use]
262    pub fn rename_schema(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
263        self.renames = self.renames.rename_schema(from, to);
264        self
265    }
266
267    #[must_use]
268    pub fn rename_table(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
269        self.renames = self.renames.rename_table(from, to);
270        self
271    }
272
273    #[must_use]
274    pub fn rename_table_in(
275        mut self,
276        schema: impl Into<String>,
277        from: impl Into<String>,
278        to: impl Into<String>,
279    ) -> Self {
280        self.renames = self.renames.rename_table_in(schema, from, to);
281        self
282    }
283
284    #[must_use]
285    pub fn rename_column(
286        mut self,
287        table: impl Into<String>,
288        from: impl Into<String>,
289        to: impl Into<String>,
290    ) -> Self {
291        self.renames = self.renames.rename_column(table, from, to);
292        self
293    }
294
295    #[must_use]
296    pub fn rename_column_in(
297        mut self,
298        schema: impl Into<String>,
299        table: impl Into<String>,
300        from: impl Into<String>,
301        to: impl Into<String>,
302    ) -> Self {
303        self.renames = self.renames.rename_column_in(schema, table, from, to);
304        self
305    }
306}
307
308/// Diff two snapshots and return the migration SQL statements.
309///
310/// Both snapshots must be for the same dialect (e.g., both `SQLite` or both `PostgreSQL`).
311/// Returns `Ok(vec![])` if no changes are detected.
312///
313/// This is a pure function — no file I/O, no side effects.
314///
315/// For writing tagged migration directories (`./drizzle/<tag>/...`), prefer
316/// [`crate::build::run`].
317///
318/// # Errors
319///
320/// Returns [`MigrationError::DialectMismatch`] if the two snapshots use
321/// different dialects, or a [`MigrationError::ConfigError`] if applying
322/// rename hints fails under strict mode.
323pub fn diff(prev: &Snapshot, current: &Snapshot) -> Result<Plan, MigrationError> {
324    diff_with(prev, current, &DiffOptions::default())
325}
326
327/// Diff two snapshots with explicit generation options.
328///
329/// Use this when you need rename hints (table/column renames) to avoid
330/// drop-and-recreate diffs.
331///
332/// # Errors
333///
334/// Returns [`MigrationError::DialectMismatch`] if the two snapshots use
335/// different dialects, or a [`MigrationError::ConfigError`] if applying
336/// rename hints fails under strict mode.
337pub fn diff_with(
338    prev: &Snapshot,
339    current: &Snapshot,
340    options: &DiffOptions,
341) -> Result<Plan, MigrationError> {
342    let (statements, warnings) = match (prev, current) {
343        (Snapshot::Sqlite(p), Snapshot::Sqlite(c)) => {
344            let mut prev_ddl = SQLiteDDL::from_entities(p.ddl.clone());
345            let cur_ddl = crate::sqlite::collection::SQLiteDDL::from_entities(c.ddl.clone());
346            let mut statements = apply_sqlite_rename_hints(&mut prev_ddl, &cur_ddl, options)?;
347            let mut diff = crate::sqlite::diff::compute_migration(&prev_ddl, &cur_ddl);
348            crate::sqlite::rebuild_data::apply_rebuild_data_plan(
349                prev.id(),
350                &prev_ddl,
351                &cur_ddl,
352                &mut diff.statements,
353                options.sqlite_rebuild_data.as_ref(),
354            )
355            .map_err(MigrationError::ConfigError)?;
356            diff.sql_statements =
357                crate::sqlite::statements::from_json(diff.statements.clone()).sql_statements;
358            statements.extend(diff.sql_statements);
359            (statements, diff.warnings)
360        }
361        (Snapshot::Postgres(p), Snapshot::Postgres(c)) => {
362            if options.sqlite_rebuild_data.is_some() {
363                return Err(MigrationError::ConfigError(
364                    "SQLite rebuild-data plan cannot be used for a PostgreSQL migration"
365                        .to_string(),
366                ));
367            }
368            let mut prev_ddl = PostgresDDL::from_entities(p.ddl.clone());
369            let cur_ddl = PostgresDDL::from_entities(c.ddl.clone());
370            let mut statements = apply_postgres_rename_hints(&mut prev_ddl, &cur_ddl, options)?;
371            let diff = crate::postgres::diff::compute_migration(&prev_ddl, &cur_ddl);
372            statements.extend(diff.sql_statements);
373            (statements, diff.warnings)
374        }
375        _ => return Err(MigrationError::DialectMismatch),
376    };
377
378    // Link the produced snapshot into the chain: it succeeds `prev`. A fresh
379    // empty baseline (no entities, still pointing at the origin) keeps the
380    // origin marker instead of adopting the baseline's throwaway id.
381    let mut snapshot = current.clone();
382    let prev_is_origin_baseline =
383        prev.is_empty() && matches!(prev.prev_ids(), [only] if only == ORIGIN_UUID);
384    if prev_is_origin_baseline {
385        snapshot.set_prev_ids(vec![ORIGIN_UUID.to_string()]);
386    } else {
387        snapshot.set_prev_ids(vec![prev.id().to_string()]);
388    }
389
390    Ok(Plan {
391        statements,
392        warnings,
393        snapshot,
394    })
395}
396
397/// Generate migration SQL from two schema values implementing [`Schema`].
398///
399/// This is usually the best runtime API when you already have two schema types.
400///
401/// # Errors
402///
403/// Returns [`MigrationError::DialectMismatch`] if the two schemas use
404/// different dialects.
405pub fn diff_schemas<From: Schema, To: Schema>(
406    prev: &From,
407    current: &To,
408) -> Result<Plan, MigrationError> {
409    let prev = prev.to_snapshot();
410    let current = current.to_snapshot();
411    diff(&prev, &current)
412}
413
414/// Generate migration SQL from two schemas with generation options.
415///
416/// # Example
417///
418/// ```rust,no_run
419/// use drizzle_migrations::{DiffOptions, Schema, Snapshot, diff_schemas_with};
420/// use drizzle_types::Dialect;
421///
422/// # #[derive(Default)]
423/// # struct FromSchema;
424/// # #[derive(Default)]
425/// # struct ToSchema;
426/// # impl Schema for FromSchema {
427/// #     fn to_snapshot(&self) -> Snapshot { Snapshot::empty(Dialect::SQLite) }
428/// #     fn dialect(&self) -> Dialect { Dialect::SQLite }
429/// # }
430/// # impl Schema for ToSchema {
431/// #     fn to_snapshot(&self) -> Snapshot { Snapshot::empty(Dialect::SQLite) }
432/// #     fn dialect(&self) -> Dialect { Dialect::SQLite }
433/// # }
434/// let migration = diff_schemas_with(
435///     &FromSchema,
436///     &ToSchema,
437///     &DiffOptions::new().rename_column("users", "displayName", "display_name"),
438/// )?;
439/// # let _ = migration;
440/// # Ok::<(), drizzle_migrations::MigrationError>(())
441/// ```
442///
443/// # Errors
444///
445/// Returns [`MigrationError::DialectMismatch`] if the two schemas use
446/// different dialects, or a [`MigrationError::ConfigError`] if applying
447/// rename hints fails under strict mode.
448pub fn diff_schemas_with<From: Schema, To: Schema>(
449    prev: &From,
450    current: &To,
451    options: &DiffOptions,
452) -> Result<Plan, MigrationError> {
453    let prev = prev.to_snapshot();
454    let current = current.to_snapshot();
455    diff_with(&prev, &current, options)
456}
457
458fn apply_sqlite_rename_hints(
459    prev: &mut SQLiteDDL,
460    cur: &SQLiteDDL,
461    options: &DiffOptions,
462) -> Result<Vec<String>, MigrationError> {
463    let mut statements = Vec::new();
464
465    if !options.renames.schema_renames.is_empty() && options.strict_renames {
466        return Err(MigrationError::ConfigError(
467            "sqlite rename_schema hint is not supported".to_string(),
468        ));
469    }
470
471    for hint in &options.renames.table_renames {
472        if hint.schema.is_some() {
473            if options.strict_renames {
474                return Err(MigrationError::ConfigError(
475                    "sqlite rename_table hint does not support schema".to_string(),
476                ));
477            }
478            continue;
479        }
480
481        if !valid_rename_name(&hint.from) || !valid_rename_name(&hint.to) || hint.from == hint.to {
482            if options.strict_renames {
483                return Err(MigrationError::ConfigError(format!(
484                    "invalid sqlite table rename hint: {} -> {}",
485                    hint.from, hint.to
486                )));
487            }
488            continue;
489        }
490
491        let can_apply = prev.tables.one(&hint.from).is_some()
492            && cur.tables.one(&hint.to).is_some()
493            && prev.tables.one(&hint.to).is_none();
494
495        if !can_apply {
496            if options.strict_renames {
497                return Err(MigrationError::ConfigError(format!(
498                    "sqlite table rename hint did not match snapshots: {} -> {}",
499                    hint.from, hint.to
500                )));
501            }
502            continue;
503        }
504
505        statements.push(format!(
506            "ALTER TABLE `{}` RENAME TO `{}`;",
507            hint.from, hint.to
508        ));
509        apply_sqlite_table_rename(prev, &hint.from, &hint.to);
510    }
511
512    for hint in &options.renames.column_renames {
513        if hint.schema.is_some() {
514            if options.strict_renames {
515                return Err(MigrationError::ConfigError(
516                    "sqlite rename_column hint does not support schema".to_string(),
517                ));
518            }
519            continue;
520        }
521
522        if !valid_rename_name(&hint.table)
523            || !valid_rename_name(&hint.from)
524            || !valid_rename_name(&hint.to)
525            || hint.from == hint.to
526        {
527            if options.strict_renames {
528                return Err(MigrationError::ConfigError(format!(
529                    "invalid sqlite column rename hint: {}.{} -> {}",
530                    hint.table, hint.from, hint.to
531                )));
532            }
533            continue;
534        }
535
536        let can_apply = prev.columns.one(&hint.table, &hint.from).is_some()
537            && cur.columns.one(&hint.table, &hint.to).is_some()
538            && prev.columns.one(&hint.table, &hint.to).is_none();
539
540        if !can_apply {
541            if options.strict_renames {
542                return Err(MigrationError::ConfigError(format!(
543                    "sqlite column rename hint did not match snapshots: {}.{} -> {}",
544                    hint.table, hint.from, hint.to
545                )));
546            }
547            continue;
548        }
549
550        statements.push(format!(
551            "ALTER TABLE `{}` RENAME COLUMN `{}` TO `{}`;",
552            hint.table, hint.from, hint.to
553        ));
554        apply_sqlite_column_rename(prev, &hint.table, &hint.from, &hint.to);
555    }
556
557    Ok(statements)
558}
559
560fn apply_postgres_rename_hints(
561    prev: &mut PostgresDDL,
562    cur: &PostgresDDL,
563    options: &DiffOptions,
564) -> Result<Vec<String>, MigrationError> {
565    let mut statements = Vec::new();
566
567    for hint in &options.renames.schema_renames {
568        if !valid_rename_name(&hint.from) || !valid_rename_name(&hint.to) || hint.from == hint.to {
569            if options.strict_renames {
570                return Err(MigrationError::ConfigError(format!(
571                    "invalid postgres schema rename hint: {} -> {}",
572                    hint.from, hint.to
573                )));
574            }
575            continue;
576        }
577
578        let can_apply = prev.schemas.one(&hint.from).is_some()
579            && cur.schemas.one(&hint.to).is_some()
580            && prev.schemas.one(&hint.to).is_none();
581
582        if !can_apply {
583            if options.strict_renames {
584                return Err(MigrationError::ConfigError(format!(
585                    "postgres schema rename hint did not match snapshots: {} -> {}",
586                    hint.from, hint.to
587                )));
588            }
589            continue;
590        }
591
592        statements.push(format!(
593            "ALTER SCHEMA \"{}\" RENAME TO \"{}\";",
594            hint.from, hint.to
595        ));
596        apply_postgres_schema_rename(prev, &hint.from, &hint.to);
597    }
598
599    for hint in &options.renames.table_renames {
600        let schema = hint.schema.as_deref().unwrap_or("public");
601        if !valid_rename_name(schema)
602            || !valid_rename_name(&hint.from)
603            || !valid_rename_name(&hint.to)
604            || hint.from == hint.to
605        {
606            if options.strict_renames {
607                return Err(MigrationError::ConfigError(format!(
608                    "invalid postgres table rename hint: {}.{} -> {}",
609                    schema, hint.from, hint.to
610                )));
611            }
612            continue;
613        }
614
615        let can_apply = prev.tables.one(schema, &hint.from).is_some()
616            && cur.tables.one(schema, &hint.to).is_some()
617            && prev.tables.one(schema, &hint.to).is_none();
618
619        if !can_apply {
620            if options.strict_renames {
621                return Err(MigrationError::ConfigError(format!(
622                    "postgres table rename hint did not match snapshots: {}.{} -> {}",
623                    schema, hint.from, hint.to
624                )));
625            }
626            continue;
627        }
628
629        statements.push(format!(
630            "ALTER TABLE \"{}\".\"{}\" RENAME TO \"{}\";",
631            schema, hint.from, hint.to
632        ));
633        apply_postgres_table_rename(prev, schema, &hint.from, &hint.to);
634    }
635
636    for hint in &options.renames.column_renames {
637        let schema = hint.schema.as_deref().unwrap_or("public");
638        if !valid_rename_name(schema)
639            || !valid_rename_name(&hint.table)
640            || !valid_rename_name(&hint.from)
641            || !valid_rename_name(&hint.to)
642            || hint.from == hint.to
643        {
644            if options.strict_renames {
645                return Err(MigrationError::ConfigError(format!(
646                    "invalid postgres column rename hint: {}.{}.{} -> {}",
647                    schema, hint.table, hint.from, hint.to
648                )));
649            }
650            continue;
651        }
652
653        let can_apply = prev.columns.one(schema, &hint.table, &hint.from).is_some()
654            && cur.columns.one(schema, &hint.table, &hint.to).is_some()
655            && prev.columns.one(schema, &hint.table, &hint.to).is_none();
656
657        if !can_apply {
658            if options.strict_renames {
659                return Err(MigrationError::ConfigError(format!(
660                    "postgres column rename hint did not match snapshots: {}.{}.{} -> {}",
661                    schema, hint.table, hint.from, hint.to
662                )));
663            }
664            continue;
665        }
666
667        statements.push(format!(
668            "ALTER TABLE \"{}\".\"{}\" RENAME COLUMN \"{}\" TO \"{}\";",
669            schema, hint.table, hint.from, hint.to
670        ));
671        apply_postgres_column_rename(prev, schema, &hint.table, &hint.from, &hint.to);
672    }
673
674    Ok(statements)
675}
676
677fn apply_sqlite_table_rename(ddl: &mut SQLiteDDL, from: &str, to: &str) {
678    let to = to.to_string();
679
680    if let Some(t) = ddl
681        .tables
682        .list_mut()
683        .iter_mut()
684        .find(|t| t.name.as_ref() == from)
685    {
686        t.name = to.clone().into();
687    }
688
689    for c in ddl
690        .columns
691        .list_mut()
692        .iter_mut()
693        .filter(|c| c.table.as_ref() == from)
694    {
695        c.table = to.clone().into();
696    }
697
698    for pk in ddl
699        .pks
700        .list_mut()
701        .iter_mut()
702        .filter(|pk| pk.table.as_ref() == from)
703    {
704        pk.table = to.clone().into();
705    }
706
707    for u in ddl
708        .uniques
709        .list_mut()
710        .iter_mut()
711        .filter(|u| u.table.as_ref() == from)
712    {
713        u.table = to.clone().into();
714    }
715
716    for fk in ddl.fks.list_mut().iter_mut() {
717        if fk.table.as_ref() == from {
718            fk.table = to.clone().into();
719        }
720        if fk.table_to.as_ref() == from {
721            fk.table_to = to.clone().into();
722        }
723    }
724
725    for idx in ddl
726        .indexes
727        .list_mut()
728        .iter_mut()
729        .filter(|i| i.table.as_ref() == from)
730    {
731        idx.table = to.clone().into();
732    }
733
734    for chk in ddl
735        .checks
736        .list_mut()
737        .iter_mut()
738        .filter(|c| c.table.as_ref() == from)
739    {
740        chk.table = to.clone().into();
741    }
742}
743
744fn apply_sqlite_column_rename(ddl: &mut SQLiteDDL, table: &str, from: &str, to: &str) {
745    let to = to.to_string();
746
747    if let Some(c) = ddl
748        .columns
749        .list_mut()
750        .iter_mut()
751        .find(|c| c.table.as_ref() == table && c.name.as_ref() == from)
752    {
753        c.name = to.clone().into();
754    }
755
756    for pk in ddl
757        .pks
758        .list_mut()
759        .iter_mut()
760        .filter(|pk| pk.table.as_ref() == table)
761    {
762        for col in pk.columns.to_mut().iter_mut() {
763            if col.as_ref() == from {
764                *col = to.clone().into();
765            }
766        }
767    }
768
769    for u in ddl
770        .uniques
771        .list_mut()
772        .iter_mut()
773        .filter(|u| u.table.as_ref() == table)
774    {
775        for col in u.columns.to_mut().iter_mut() {
776            if col.as_ref() == from {
777                *col = to.clone().into();
778            }
779        }
780    }
781
782    for fk in ddl.fks.list_mut().iter_mut() {
783        if fk.table.as_ref() == table {
784            for col in fk.columns.to_mut().iter_mut() {
785                if col.as_ref() == from {
786                    *col = to.clone().into();
787                }
788            }
789        }
790        if fk.table_to.as_ref() == table {
791            for col in fk.columns_to.to_mut().iter_mut() {
792                if col.as_ref() == from {
793                    *col = to.clone().into();
794                }
795            }
796        }
797    }
798
799    for idx in ddl
800        .indexes
801        .list_mut()
802        .iter_mut()
803        .filter(|i| i.table.as_ref() == table)
804    {
805        for col in &mut idx.columns {
806            if !col.is_expression && col.value.as_ref() == from {
807                col.value = to.clone().into();
808            }
809        }
810    }
811}
812
813fn rewrite_cow(value: &mut Cow<'static, str>, from: &str, to: &str) {
814    if value.as_ref() == from {
815        *value = to.to_string().into();
816    }
817}
818
819fn rewrite_optional_cow(value: &mut Option<Cow<'static, str>>, from: &str, to: &str) {
820    if value.as_deref() == Some(from) {
821        *value = Some(to.to_string().into());
822    }
823}
824
825fn rewrite_schema_qualified_value(value: &mut Option<Cow<'static, str>>, from: &str, to: &str) {
826    let Some(current) = value.as_deref() else {
827        return;
828    };
829    let Some(rest) = current
830        .strip_prefix(from)
831        .and_then(|rest| rest.strip_prefix('.'))
832    else {
833        return;
834    };
835    *value = Some(format!("{to}.{rest}").into());
836}
837
838fn apply_postgres_schema_rename(ddl: &mut PostgresDDL, from: &str, to: &str) {
839    for schema in ddl.schemas.list_mut() {
840        rewrite_cow(&mut schema.name, from, to);
841    }
842
843    for table in ddl.tables.list_mut() {
844        rewrite_cow(&mut table.schema, from, to);
845        rewrite_schema_qualified_value(&mut table.inherits, from, to);
846    }
847
848    for column in ddl.columns.list_mut() {
849        rewrite_cow(&mut column.schema, from, to);
850        rewrite_optional_cow(&mut column.type_schema, from, to);
851        if let Some(identity) = &mut column.identity {
852            rewrite_optional_cow(&mut identity.schema, from, to);
853        }
854    }
855
856    for index in ddl.indexes.list_mut() {
857        rewrite_cow(&mut index.schema, from, to);
858    }
859
860    for fk in ddl.fks.list_mut() {
861        rewrite_cow(&mut fk.schema, from, to);
862        rewrite_cow(&mut fk.schema_to, from, to);
863    }
864
865    for pk in ddl.pks.list_mut() {
866        rewrite_cow(&mut pk.schema, from, to);
867    }
868
869    for unique in ddl.uniques.list_mut() {
870        rewrite_cow(&mut unique.schema, from, to);
871    }
872
873    for check in ddl.checks.list_mut() {
874        rewrite_cow(&mut check.schema, from, to);
875    }
876
877    for policy in ddl.policies.list_mut() {
878        rewrite_cow(&mut policy.schema, from, to);
879    }
880
881    for enum_ in ddl.enums.list_mut() {
882        rewrite_cow(&mut enum_.schema, from, to);
883    }
884
885    for sequence in ddl.sequences.list_mut() {
886        rewrite_cow(&mut sequence.schema, from, to);
887    }
888
889    for view in ddl.views.list_mut() {
890        rewrite_cow(&mut view.schema, from, to);
891    }
892}
893
894fn apply_postgres_table_rename(ddl: &mut PostgresDDL, schema: &str, from: &str, to: &str) {
895    let to = to.to_string();
896
897    for table in ddl.tables.list_mut() {
898        if table.schema.as_ref() == schema && table.name.as_ref() == from {
899            table.name = to.clone().into();
900        }
901
902        if table.schema.as_ref() == schema
903            && let Some(inherits) = &mut table.inherits
904        {
905            if inherits.as_ref() == from {
906                *inherits = to.clone().into();
907            } else if inherits.as_ref() == format!("{schema}.{from}") {
908                *inherits = format!("{schema}.{to}").into();
909            }
910        }
911    }
912
913    for c in ddl
914        .columns
915        .list_mut()
916        .iter_mut()
917        .filter(|c| c.schema.as_ref() == schema && c.table.as_ref() == from)
918    {
919        c.table = to.clone().into();
920    }
921
922    for pk in ddl
923        .pks
924        .list_mut()
925        .iter_mut()
926        .filter(|pk| pk.schema.as_ref() == schema && pk.table.as_ref() == from)
927    {
928        pk.table = to.clone().into();
929    }
930
931    for u in ddl
932        .uniques
933        .list_mut()
934        .iter_mut()
935        .filter(|u| u.schema.as_ref() == schema && u.table.as_ref() == from)
936    {
937        u.table = to.clone().into();
938    }
939
940    for fk in ddl.fks.list_mut().iter_mut() {
941        if fk.schema.as_ref() == schema && fk.table.as_ref() == from {
942            fk.table = to.clone().into();
943        }
944        if fk.schema_to.as_ref() == schema && fk.table_to.as_ref() == from {
945            fk.table_to = to.clone().into();
946        }
947    }
948
949    for idx in ddl
950        .indexes
951        .list_mut()
952        .iter_mut()
953        .filter(|i| i.schema.as_ref() == schema && i.table.as_ref() == from)
954    {
955        idx.table = to.clone().into();
956    }
957
958    for chk in ddl
959        .checks
960        .list_mut()
961        .iter_mut()
962        .filter(|c| c.schema.as_ref() == schema && c.table.as_ref() == from)
963    {
964        chk.table = to.clone().into();
965    }
966
967    for policy in ddl
968        .policies
969        .list_mut()
970        .iter_mut()
971        .filter(|p| p.schema.as_ref() == schema && p.table.as_ref() == from)
972    {
973        policy.table = to.clone().into();
974    }
975}
976
977fn apply_postgres_column_rename(
978    ddl: &mut PostgresDDL,
979    schema: &str,
980    table: &str,
981    from: &str,
982    to: &str,
983) {
984    let to = to.to_string();
985
986    for c in ddl.columns.list_mut().iter_mut() {
987        if c.schema.as_ref() == schema && c.table.as_ref() == table && c.name.as_ref() == from {
988            c.name = to.clone().into();
989        }
990    }
991
992    for pk in ddl
993        .pks
994        .list_mut()
995        .iter_mut()
996        .filter(|p| p.schema.as_ref() == schema && p.table.as_ref() == table)
997    {
998        for col in pk.columns.to_mut().iter_mut() {
999            if col.as_ref() == from {
1000                *col = to.clone().into();
1001            }
1002        }
1003    }
1004
1005    for u in ddl
1006        .uniques
1007        .list_mut()
1008        .iter_mut()
1009        .filter(|u| u.schema.as_ref() == schema && u.table.as_ref() == table)
1010    {
1011        for col in u.columns.to_mut().iter_mut() {
1012            if col.as_ref() == from {
1013                *col = to.clone().into();
1014            }
1015        }
1016    }
1017
1018    for fk in ddl.fks.list_mut().iter_mut() {
1019        if fk.schema.as_ref() == schema && fk.table.as_ref() == table {
1020            for col in fk.columns.to_mut().iter_mut() {
1021                if col.as_ref() == from {
1022                    *col = to.clone().into();
1023                }
1024            }
1025        }
1026        if fk.schema_to.as_ref() == schema && fk.table_to.as_ref() == table {
1027            for col in fk.columns_to.to_mut().iter_mut() {
1028                if col.as_ref() == from {
1029                    *col = to.clone().into();
1030                }
1031            }
1032        }
1033    }
1034
1035    for idx in ddl
1036        .indexes
1037        .list_mut()
1038        .iter_mut()
1039        .filter(|i| i.schema.as_ref() == schema && i.table.as_ref() == table)
1040    {
1041        for col in &mut idx.columns {
1042            if !col.is_expression && col.value.as_ref() == from {
1043                col.value = to.clone().into();
1044            }
1045        }
1046    }
1047}
1048
1049fn valid_rename_name(name: &str) -> bool {
1050    !name.trim().is_empty()
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055    use super::*;
1056    use crate::postgres::PostgresSnapshot;
1057    use crate::postgres::ddl::{
1058        Column as PgColumn, PostgresEntity, Schema as PgSchema, Table as PgTable,
1059    };
1060    use crate::schema::Schema as MigrationSchema;
1061    use crate::sqlite::SQLiteSnapshot;
1062    use crate::sqlite::ddl::{Column, SqliteEntity, Table};
1063
1064    #[derive(Default)]
1065    struct EmptySqliteSchema;
1066
1067    impl MigrationSchema for EmptySqliteSchema {
1068        fn dialect(&self) -> drizzle_types::Dialect {
1069            drizzle_types::Dialect::SQLite
1070        }
1071
1072        fn to_snapshot(&self) -> Snapshot {
1073            Snapshot::empty(drizzle_types::Dialect::SQLite)
1074        }
1075    }
1076
1077    #[test]
1078    fn test_generate_empty_to_empty() {
1079        let prev = Snapshot::empty(drizzle_types::Dialect::SQLite);
1080        let cur = Snapshot::empty(drizzle_types::Dialect::SQLite);
1081        let migration = diff(&prev, &cur).unwrap();
1082        assert!(migration.statements.is_empty());
1083    }
1084
1085    #[test]
1086    fn test_generate_create_table() {
1087        let prev = Snapshot::empty(drizzle_types::Dialect::SQLite);
1088
1089        let mut cur_snap = SQLiteSnapshot::new();
1090        cur_snap.add_entity(SqliteEntity::Table(Table::new("users")));
1091        cur_snap.add_entity(SqliteEntity::Column(
1092            Column::new("users", "id", "integer").not_null(),
1093        ));
1094        cur_snap.add_entity(SqliteEntity::Column(
1095            Column::new("users", "name", "text").not_null(),
1096        ));
1097        let cur = Snapshot::Sqlite(cur_snap);
1098
1099        let migration = diff(&prev, &cur).unwrap();
1100        assert!(!migration.statements.is_empty());
1101        assert!(migration.statements[0].contains("CREATE TABLE"));
1102        assert!(migration.statements[0].contains("users"));
1103    }
1104
1105    #[test]
1106    fn test_generate_dialect_mismatch() {
1107        let prev = Snapshot::empty(drizzle_types::Dialect::SQLite);
1108        let cur = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1109        let result = diff(&prev, &cur);
1110        assert!(matches!(result, Err(MigrationError::DialectMismatch)));
1111    }
1112
1113    #[test]
1114    fn test_generate_postgres_empty() {
1115        let prev = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1116        let cur = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1117        let migration = diff(&prev, &cur).unwrap();
1118        assert!(migration.statements.is_empty());
1119    }
1120
1121    #[test]
1122    fn test_diff_schemas_empty() {
1123        let prev = EmptySqliteSchema;
1124        let cur = EmptySqliteSchema;
1125        let migration = diff_schemas(&prev, &cur).unwrap();
1126        assert!(migration.statements.is_empty());
1127    }
1128
1129    #[test]
1130    fn test_diff_with_sqlite_rename_hints() {
1131        let mut prev_snap = SQLiteSnapshot::new();
1132        prev_snap.add_entity(SqliteEntity::Table(Table::new("users")));
1133        prev_snap.add_entity(SqliteEntity::Column(
1134            Column::new("users", "full_name", "text").not_null(),
1135        ));
1136
1137        let mut cur_snap = SQLiteSnapshot::new();
1138        cur_snap.add_entity(SqliteEntity::Table(Table::new("accounts")));
1139        cur_snap.add_entity(SqliteEntity::Column(
1140            Column::new("accounts", "display_name", "text").not_null(),
1141        ));
1142
1143        let prev = Snapshot::Sqlite(prev_snap);
1144        let cur = Snapshot::Sqlite(cur_snap);
1145
1146        let options = DiffOptions::new()
1147            .rename_table("users", "accounts")
1148            .rename_column("accounts", "full_name", "display_name");
1149
1150        let migration = diff_with(&prev, &cur, &options).unwrap();
1151        assert_eq!(
1152            migration.statements,
1153            vec![
1154                "ALTER TABLE `users` RENAME TO `accounts`;".to_string(),
1155                "ALTER TABLE `accounts` RENAME COLUMN `full_name` TO `display_name`;".to_string(),
1156            ]
1157        );
1158    }
1159
1160    #[test]
1161    fn test_diff_with_sqlite_table_rename_hint_and_add_column() {
1162        let mut prev_snap = SQLiteSnapshot::new();
1163        prev_snap.add_entity(SqliteEntity::Table(Table::new("users")));
1164        prev_snap.add_entity(SqliteEntity::Column(
1165            Column::new("users", "id", "integer").not_null(),
1166        ));
1167
1168        let mut cur_snap = SQLiteSnapshot::new();
1169        cur_snap.add_entity(SqliteEntity::Table(Table::new("accounts")));
1170        cur_snap.add_entity(SqliteEntity::Column(
1171            Column::new("accounts", "id", "integer").not_null(),
1172        ));
1173        cur_snap.add_entity(SqliteEntity::Column(Column::new(
1174            "accounts", "email", "text",
1175        )));
1176
1177        let migration = diff_with(
1178            &Snapshot::Sqlite(prev_snap),
1179            &Snapshot::Sqlite(cur_snap),
1180            &DiffOptions::new().rename_table("users", "accounts"),
1181        )
1182        .unwrap();
1183
1184        assert_eq!(
1185            migration.statements,
1186            vec![
1187                "ALTER TABLE `users` RENAME TO `accounts`;".to_string(),
1188                "ALTER TABLE `accounts` ADD `email` TEXT;".to_string(),
1189            ]
1190        );
1191    }
1192
1193    #[test]
1194    fn test_diff_with_postgres_table_rename_hint_and_add_column() {
1195        let mut prev_snap = PostgresSnapshot::new();
1196        prev_snap.add_entity(PostgresEntity::Schema(PgSchema::new("public")));
1197        prev_snap.add_entity(PostgresEntity::Table(PgTable::new("public", "users")));
1198        prev_snap.add_entity(PostgresEntity::Column(
1199            PgColumn::new("public", "users", "id", "integer").not_null(),
1200        ));
1201
1202        let mut cur_snap = PostgresSnapshot::new();
1203        cur_snap.add_entity(PostgresEntity::Schema(PgSchema::new("public")));
1204        cur_snap.add_entity(PostgresEntity::Table(PgTable::new("public", "accounts")));
1205        cur_snap.add_entity(PostgresEntity::Column(
1206            PgColumn::new("public", "accounts", "id", "integer").not_null(),
1207        ));
1208        cur_snap.add_entity(PostgresEntity::Column(PgColumn::new(
1209            "public", "accounts", "email", "text",
1210        )));
1211
1212        let migration = diff_with(
1213            &Snapshot::Postgres(prev_snap),
1214            &Snapshot::Postgres(cur_snap),
1215            &DiffOptions::new().rename_table("users", "accounts"),
1216        )
1217        .unwrap();
1218
1219        assert_eq!(
1220            migration.statements,
1221            vec![
1222                "ALTER TABLE \"public\".\"users\" RENAME TO \"accounts\";".to_string(),
1223                "ALTER TABLE \"accounts\" ADD COLUMN \"email\" text;".to_string(),
1224            ]
1225        );
1226    }
1227
1228    #[test]
1229    fn test_diff_with_strict_rename_hints_errors() {
1230        let prev = Snapshot::empty(drizzle_types::Dialect::SQLite);
1231        let cur = Snapshot::empty(drizzle_types::Dialect::SQLite);
1232        let options = DiffOptions::new()
1233            .strict_renames(true)
1234            .rename_table("missing_table", "users");
1235
1236        let result = diff_with(&prev, &cur, &options);
1237        assert!(matches!(result, Err(MigrationError::ConfigError(_))));
1238
1239        let prev = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1240        let cur = Snapshot::empty(drizzle_types::Dialect::PostgreSQL);
1241        let options = DiffOptions::new()
1242            .strict_renames(true)
1243            .rename_schema("missing_schema", "app");
1244
1245        let result = diff_with(&prev, &cur, &options);
1246        assert!(matches!(result, Err(MigrationError::ConfigError(_))));
1247    }
1248}