Skip to main content

drizzle_migrations/sqlite/
diff.rs

1//! Schema diff types and logic for `SQLite` v7 DDL format
2//!
3//! This module provides diffing between DDL collections and
4//! generates migration statements from schema changes.
5
6use super::SQLiteSnapshot;
7use super::collection::{DiffType, EntityDiff, SQLiteDDL, diff_ddl};
8use super::ddl::SqliteEntity;
9use super::statements::{
10    AddColumnStatement, CreateIndexStatement, CreateTableStatement, CreateViewStatement,
11    DropColumnStatement, DropIndexStatement, DropTableStatement, DropViewStatement, JsonStatement,
12    RecreateTableStatement, RenameColumnStatement, RenameTableStatement, TableFull, from_json,
13};
14use crate::traits::EntityKind;
15use std::collections::{BTreeMap, BTreeSet, HashSet};
16
17// Re-export diff types from collection
18pub use super::collection::{DiffType as SchemaDiffType, EntityDiff as SchemaEntityDiff};
19
20/// Complete schema diff between two snapshots
21#[derive(Debug, Clone, Default)]
22pub struct SchemaDiff {
23    /// All entity diffs
24    pub diffs: Vec<EntityDiff>,
25}
26
27impl SchemaDiff {
28    /// Check if there are any changes
29    #[must_use]
30    pub const fn has_changes(&self) -> bool {
31        !self.diffs.is_empty()
32    }
33
34    /// Check if this diff is empty (no changes)
35    #[must_use]
36    pub const fn is_empty(&self) -> bool {
37        self.diffs.is_empty()
38    }
39
40    /// Get created entities
41    #[must_use]
42    pub fn created(&self) -> Vec<&EntityDiff> {
43        self.diffs
44            .iter()
45            .filter(|d| d.diff_type == DiffType::Create)
46            .collect()
47    }
48
49    /// Get dropped entities
50    #[must_use]
51    pub fn dropped(&self) -> Vec<&EntityDiff> {
52        self.diffs
53            .iter()
54            .filter(|d| d.diff_type == DiffType::Drop)
55            .collect()
56    }
57
58    /// Get altered entities
59    #[must_use]
60    pub fn altered(&self) -> Vec<&EntityDiff> {
61        self.diffs
62            .iter()
63            .filter(|d| d.diff_type == DiffType::Alter)
64            .collect()
65    }
66
67    /// Get diffs filtered by entity kind
68    #[must_use]
69    pub fn by_kind(&self, kind: EntityKind) -> Vec<&EntityDiff> {
70        self.diffs.iter().filter(|d| d.kind == kind).collect()
71    }
72
73    /// Get created tables
74    #[must_use]
75    pub fn created_tables(&self) -> Vec<&EntityDiff> {
76        self.diffs
77            .iter()
78            .filter(|d| d.diff_type == DiffType::Create && d.kind == EntityKind::Table)
79            .collect()
80    }
81
82    /// Get dropped tables
83    #[must_use]
84    pub fn dropped_tables(&self) -> Vec<&EntityDiff> {
85        self.diffs
86            .iter()
87            .filter(|d| d.diff_type == DiffType::Drop && d.kind == EntityKind::Table)
88            .collect()
89    }
90}
91
92/// Compare two `SQLite` snapshots and return the diff
93#[must_use]
94pub fn diff_snapshots(prev: &SQLiteSnapshot, cur: &SQLiteSnapshot) -> SchemaDiff {
95    let prev_ddl = SQLiteDDL::from_entities(prev.ddl.clone());
96    let cur_ddl = SQLiteDDL::from_entities(cur.ddl.clone());
97
98    SchemaDiff {
99        diffs: diff_ddl(&prev_ddl, &cur_ddl),
100    }
101}
102
103/// Compare two DDL collections directly
104#[must_use]
105pub fn diff_collections(prev: &SQLiteDDL, cur: &SQLiteDDL) -> SchemaDiff {
106    SchemaDiff {
107        diffs: diff_ddl(prev, cur),
108    }
109}
110
111// =============================================================================
112// Migration Diff Result
113// =============================================================================
114
115/// A table rename operation
116#[derive(Debug, Clone)]
117pub struct TableRename {
118    pub from: String,
119    pub to: String,
120}
121
122/// A column rename operation
123#[derive(Debug, Clone)]
124pub struct ColumnRename {
125    pub table: String,
126    pub from: String,
127    pub to: String,
128}
129
130/// Result of computing a migration diff
131#[derive(Debug, Clone, Default)]
132pub struct MigrationDiff {
133    /// JSON statements for the migration
134    pub statements: Vec<JsonStatement>,
135    /// Generated SQL statements
136    pub sql_statements: Vec<String>,
137    /// Renames that occurred (for tracking in snapshot)
138    pub renames: Vec<String>,
139    /// Warning messages
140    pub warnings: Vec<String>,
141}
142
143/// Build a `TableFull` from DDL for a given table name
144#[must_use]
145pub fn table_from_ddl(table_name: &str, ddl: &SQLiteDDL) -> TableFull {
146    let entities = ddl.table_entities(table_name);
147
148    // Get table-level options (strict, without_rowid)
149    let (strict, without_rowid) = ddl
150        .tables
151        .one(table_name)
152        .map_or((false, false), |t| (t.strict, t.without_rowid));
153
154    TableFull {
155        name: table_name.to_string(),
156        columns: entities.columns.into_iter().cloned().collect(),
157        pk: entities.pk.cloned(),
158        fks: entities.fks.into_iter().cloned().collect(),
159        uniques: entities.uniques.into_iter().cloned().collect(),
160        checks: entities.checks.into_iter().cloned().collect(),
161        strict,
162        without_rowid,
163    }
164}
165
166fn entity_table_name(entity: &SqliteEntity) -> Option<String> {
167    match entity {
168        SqliteEntity::Column(c) => Some(c.table.to_string()),
169        SqliteEntity::ForeignKey(fk) => Some(fk.table.to_string()),
170        SqliteEntity::PrimaryKey(pk) => Some(pk.table.to_string()),
171        SqliteEntity::UniqueConstraint(uc) => Some(uc.table.to_string()),
172        SqliteEntity::CheckConstraint(cc) => Some(cc.table.to_string()),
173        _ => None,
174    }
175}
176
177fn collect_tables_to_recreate(
178    schema_diff: &SchemaDiff,
179    created: &HashSet<String>,
180    dropped: &HashSet<String>,
181) -> BTreeSet<String> {
182    // BTreeSet: iteration order reaches the emitted SQL, so it must be
183    // deterministic.
184    let mut out: BTreeSet<String> = BTreeSet::new();
185
186    // Table-level option changes (STRICT / WITHOUT ROWID) can only be applied
187    // by recreating the table.
188    for table_diff in schema_diff.by_kind(EntityKind::Table) {
189        if table_diff.diff_type == DiffType::Alter
190            && let Some(SqliteEntity::Table(table)) = &table_diff.right
191            && !created.contains(table.name.as_ref())
192            && !dropped.contains(table.name.as_ref())
193        {
194            out.insert(table.name.to_string());
195        }
196    }
197
198    // Column alterations trigger recreation (SQLite has no ALTER COLUMN).
199    for col_diff in schema_diff.by_kind(EntityKind::Column) {
200        if col_diff.diff_type == DiffType::Alter
201            && let Some(SqliteEntity::Column(col)) = &col_diff.right
202            && !created.contains(col.table.as_ref())
203            && !dropped.contains(col.table.as_ref())
204        {
205            out.insert(col.table.to_string());
206        }
207    }
208
209    // New STORED generated columns - SQLite doesn't allow ALTER TABLE ADD COLUMN for STORED
210    // See: https://www.sqlite.org/gencol.html
211    for col_diff in schema_diff.by_kind(EntityKind::Column) {
212        if col_diff.diff_type == DiffType::Create
213            && let Some(SqliteEntity::Column(col)) = &col_diff.right
214            && col
215                .generated
216                .as_ref()
217                .is_some_and(|g| g.gen_type == super::ddl::GeneratedType::Stored)
218            && !created.contains(col.table.as_ref())
219            && !dropped.contains(col.table.as_ref())
220        {
221            out.insert(col.table.to_string());
222        }
223    }
224
225    // FK, PK, unique, check constraint changes all require recreation.
226    for kind in [
227        EntityKind::ForeignKey,
228        EntityKind::PrimaryKey,
229        EntityKind::UniqueConstraint,
230        EntityKind::CheckConstraint,
231    ] {
232        for diff in schema_diff.by_kind(kind) {
233            if !matches!(
234                diff.diff_type,
235                DiffType::Create | DiffType::Drop | DiffType::Alter
236            ) {
237                continue;
238            }
239            let table = diff
240                .right
241                .as_ref()
242                .and_then(entity_table_name)
243                .or_else(|| diff.left.as_ref().and_then(entity_table_name));
244            if let Some(table) = table
245                && !created.contains(&table)
246                && !dropped.contains(&table)
247            {
248                out.insert(table);
249            }
250        }
251    }
252
253    out
254}
255
256/// Compute a full migration diff between two DDL states
257///
258/// This is a simplified version of the TypeScript ddlDiff function.
259/// For a fully interactive migration with rename detection, you would
260/// need to provide resolver callbacks.
261#[must_use]
262pub fn compute_migration(prev: &SQLiteDDL, cur: &SQLiteDDL) -> MigrationDiff {
263    // Heuristic rename detection (non-interactive):
264    // - detect exact table renames (same schema, identical entities)
265    // - detect exact column renames (same table, identical column properties)
266    let mut prev_normalized = prev.clone();
267    let mut rename_statements: Vec<JsonStatement> = Vec::new();
268    let mut table_renames: Vec<TableRename> = Vec::new();
269    let mut column_renames: Vec<ColumnRename> = Vec::new();
270    let mut warnings = Vec::new();
271
272    detect_and_apply_renames(
273        &mut prev_normalized,
274        cur,
275        &mut rename_statements,
276        &mut table_renames,
277        &mut column_renames,
278        &mut warnings,
279    );
280
281    let schema_diff = diff_collections(&prev_normalized, cur);
282    let mut statements = Vec::new();
283    let renames = prepare_migration_renames(&table_renames, &column_renames);
284
285    // Emit rename statements first so subsequent diffs apply to the renamed schema.
286    statements.extend(rename_statements);
287
288    // Track created/dropped table names
289    let created_table_names: HashSet<String> = schema_diff
290        .created_tables()
291        .iter()
292        .map(|d| d.name.clone())
293        .collect();
294
295    let dropped_table_names: HashSet<String> = schema_diff
296        .dropped_tables()
297        .iter()
298        .map(|d| d.name.clone())
299        .collect();
300
301    // Collect tables that need recreation due to column alterations
302    // SQLite doesn't support ALTER COLUMN, so we need to recreate the table
303    let tables_to_recreate =
304        collect_tables_to_recreate(&schema_diff, &created_table_names, &dropped_table_names);
305
306    append_table_create_recreate_stmts(
307        &mut statements,
308        &schema_diff,
309        prev,
310        cur,
311        &tables_to_recreate,
312    );
313    append_add_column_stmts(
314        &mut statements,
315        &schema_diff,
316        cur,
317        &created_table_names,
318        &tables_to_recreate,
319    );
320    append_index_stmts(&mut statements, &schema_diff, cur, &tables_to_recreate);
321    append_drop_column_and_view_stmts(
322        &mut statements,
323        &schema_diff,
324        &dropped_table_names,
325        &tables_to_recreate,
326    );
327    append_drop_table_stmts(&mut statements, &schema_diff);
328    collect_stored_generated_warnings(&mut warnings, &schema_diff);
329
330    // Convert to SQL
331    let result = from_json(statements.clone());
332
333    MigrationDiff {
334        statements,
335        sql_statements: result.sql_statements,
336        renames,
337        warnings,
338    }
339}
340
341fn append_table_create_recreate_stmts(
342    statements: &mut Vec<JsonStatement>,
343    schema_diff: &SchemaDiff,
344    prev: &SQLiteDDL,
345    cur: &SQLiteDDL,
346    tables_to_recreate: &BTreeSet<String>,
347) {
348    // 1. Create tables
349    for table_diff in schema_diff.created_tables() {
350        if let Some(SqliteEntity::Table(table)) = &table_diff.right {
351            let table_full = table_from_ddl(&table.name, cur);
352            statements.push(JsonStatement::CreateTable(CreateTableStatement {
353                table: table_full,
354            }));
355        }
356    }
357
358    // 2. Recreate tables that have column alterations
359    for table_name in tables_to_recreate {
360        let from_table = table_from_ddl(table_name, prev);
361        let to_table = table_from_ddl(table_name, cur);
362        statements.push(JsonStatement::RecreateTable(RecreateTableStatement {
363            from: from_table,
364            to: to_table,
365            data: None,
366        }));
367    }
368}
369
370fn append_add_column_stmts(
371    statements: &mut Vec<JsonStatement>,
372    schema_diff: &SchemaDiff,
373    cur: &SQLiteDDL,
374    created_table_names: &HashSet<String>,
375    tables_to_recreate: &BTreeSet<String>,
376) {
377    // 3. Add columns (for existing tables only, skip tables being recreated)
378    for col_diff in schema_diff.by_kind(EntityKind::Column) {
379        if col_diff.diff_type == DiffType::Create
380            && let Some(SqliteEntity::Column(col)) = &col_diff.right
381            // Skip columns for newly created tables
382            && !created_table_names.contains(col.table.as_ref())
383            // Skip columns for tables being recreated
384            && !tables_to_recreate.contains(col.table.as_ref())
385        {
386            // Find associated FK if any
387            let fk = cur
388                .fks
389                .for_table(&col.table)
390                .into_iter()
391                .find(|fk| fk.columns.len() == 1 && fk.columns[0] == col.name)
392                .cloned();
393
394            statements.push(JsonStatement::AddColumn(AddColumnStatement {
395                column: col.clone(),
396                fk,
397            }));
398        }
399    }
400}
401
402fn append_index_stmts(
403    statements: &mut Vec<JsonStatement>,
404    schema_diff: &SchemaDiff,
405    cur: &SQLiteDDL,
406    tables_to_recreate: &BTreeSet<String>,
407) {
408    // 4. Drop indexes (skip tables being recreated - indexes will be recreated with table)
409    for idx_diff in schema_diff.by_kind(EntityKind::Index) {
410        if idx_diff.diff_type == DiffType::Drop
411            && let Some(SqliteEntity::Index(idx)) = &idx_diff.left
412            && !tables_to_recreate.contains(idx.table.as_ref())
413        {
414            statements.push(JsonStatement::DropIndex(DropIndexStatement {
415                index: idx.clone(),
416            }));
417        }
418    }
419
420    // 5. Create indexes (including for newly created tables, skip tables being recreated)
421    for idx_diff in schema_diff.by_kind(EntityKind::Index) {
422        if idx_diff.diff_type == DiffType::Create
423            && let Some(SqliteEntity::Index(idx)) = &idx_diff.right
424            && !tables_to_recreate.contains(idx.table.as_ref())
425        {
426            statements.push(JsonStatement::CreateIndex(CreateIndexStatement {
427                index: idx.clone(),
428            }));
429        }
430    }
431
432    // 5b. Recreate indexes for tables that were recreated
433    // When a table is recreated, all its indexes are dropped, so we need to recreate them
434    for table_name in tables_to_recreate {
435        for idx in cur.indexes.for_table(table_name) {
436            statements.push(JsonStatement::CreateIndex(CreateIndexStatement {
437                index: idx.clone(),
438            }));
439        }
440    }
441
442    // 6. Alter indexes (drop old, create new, skip tables being recreated)
443    for idx_diff in schema_diff.by_kind(EntityKind::Index) {
444        if idx_diff.diff_type == DiffType::Alter {
445            if let Some(SqliteEntity::Index(old_idx)) = &idx_diff.left
446                && !tables_to_recreate.contains(old_idx.table.as_ref())
447            {
448                statements.push(JsonStatement::DropIndex(DropIndexStatement {
449                    index: old_idx.clone(),
450                }));
451            }
452            if let Some(SqliteEntity::Index(new_idx)) = &idx_diff.right
453                && !tables_to_recreate.contains(new_idx.table.as_ref())
454            {
455                statements.push(JsonStatement::CreateIndex(CreateIndexStatement {
456                    index: new_idx.clone(),
457                }));
458            }
459        }
460    }
461}
462
463fn append_drop_column_and_view_stmts(
464    statements: &mut Vec<JsonStatement>,
465    schema_diff: &SchemaDiff,
466    dropped_table_names: &HashSet<String>,
467    tables_to_recreate: &BTreeSet<String>,
468) {
469    // 7. Drop columns (for non-dropped tables, skip tables being recreated)
470    for col_diff in schema_diff.by_kind(EntityKind::Column) {
471        if col_diff.diff_type == DiffType::Drop
472            && let Some(SqliteEntity::Column(col)) = &col_diff.left
473            // Skip columns for dropped tables
474            && !dropped_table_names.contains(col.table.as_ref())
475            // Skip columns for tables being recreated
476            && !tables_to_recreate.contains(col.table.as_ref())
477        {
478            statements.push(JsonStatement::DropColumn(DropColumnStatement {
479                column: col.clone(),
480            }));
481        }
482    }
483
484    // 8. Drop views
485    for view_diff in schema_diff.by_kind(EntityKind::View) {
486        if view_diff.diff_type == DiffType::Drop
487            && let Some(SqliteEntity::View(view)) = &view_diff.left
488            && !view.is_existing
489        {
490            statements.push(JsonStatement::DropView(DropViewStatement {
491                view: view.clone(),
492            }));
493        }
494    }
495
496    // 9. Create views
497    for view_diff in schema_diff.by_kind(EntityKind::View) {
498        if view_diff.diff_type == DiffType::Create
499            && let Some(SqliteEntity::View(view)) = &view_diff.right
500            && !view.is_existing
501        {
502            statements.push(JsonStatement::CreateView(CreateViewStatement {
503                view: view.clone(),
504            }));
505        }
506    }
507
508    // 10. Alter views (drop and recreate)
509    for view_diff in schema_diff.by_kind(EntityKind::View) {
510        if view_diff.diff_type == DiffType::Alter {
511            if let Some(SqliteEntity::View(old_view)) = &view_diff.left {
512                statements.push(JsonStatement::DropView(DropViewStatement {
513                    view: old_view.clone(),
514                }));
515            }
516            if let Some(SqliteEntity::View(new_view)) = &view_diff.right {
517                statements.push(JsonStatement::CreateView(CreateViewStatement {
518                    view: new_view.clone(),
519                }));
520            }
521        }
522    }
523}
524
525fn append_drop_table_stmts(statements: &mut Vec<JsonStatement>, schema_diff: &SchemaDiff) {
526    // 11. Drop tables
527    for table_diff in schema_diff.dropped_tables() {
528        statements.push(JsonStatement::DropTable(DropTableStatement {
529            table_name: table_diff.name.clone(),
530        }));
531    }
532}
533
534fn collect_stored_generated_warnings(warnings: &mut Vec<String>, schema_diff: &SchemaDiff) {
535    // Add warnings for STORED generated columns
536    for col_diff in schema_diff.by_kind(EntityKind::Column) {
537        if col_diff.diff_type == DiffType::Alter
538            && let Some(SqliteEntity::Column(col)) = &col_diff.right
539            && col
540                .generated
541                .as_ref()
542                .is_some_and(|g| g.gen_type == super::ddl::GeneratedType::Stored)
543        {
544            warnings.push(format!(
545                "Column '{}' in table '{}' has STORED generated column which requires table recreation",
546                col.name, col.table
547            ));
548        }
549    }
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
553struct TableColumnFingerprint {
554    name: String,
555    sql_type: String,
556    not_null: bool,
557}
558
559#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
560struct TableFingerprint {
561    columns: Vec<TableColumnFingerprint>,
562    pk_columns: Vec<String>,
563}
564
565fn table_fingerprint(table_name: &str, ddl: &SQLiteDDL) -> TableFingerprint {
566    let mut columns: Vec<_> = ddl
567        .columns
568        .for_table(table_name)
569        .into_iter()
570        .map(|c| TableColumnFingerprint {
571            name: c.name.to_string(),
572            sql_type: c.sql_type.to_string(),
573            not_null: c.not_null,
574        })
575        .collect();
576    columns.sort();
577
578    let pk_columns = if let Some(pk) = ddl.pks.for_table(table_name) {
579        pk.columns.iter().map(ToString::to_string).collect()
580    } else {
581        let mut inline_pk_columns: Vec<_> = ddl
582            .columns
583            .for_table(table_name)
584            .into_iter()
585            .filter(|c| c.primary_key.unwrap_or(false))
586            .map(|c| (c.ordinal_position.unwrap_or(i32::MAX), c.name.to_string()))
587            .collect();
588        inline_pk_columns.sort();
589        inline_pk_columns
590            .into_iter()
591            .map(|(_, name)| name)
592            .collect()
593    };
594
595    TableFingerprint {
596        columns,
597        pk_columns,
598    }
599}
600
601fn detect_and_apply_renames(
602    prev: &mut SQLiteDDL,
603    cur: &SQLiteDDL,
604    rename_statements: &mut Vec<JsonStatement>,
605    table_renames: &mut Vec<TableRename>,
606    column_renames: &mut Vec<ColumnRename>,
607    warnings: &mut Vec<String>,
608) {
609    // Table renames: exact match of columns (name/type/nullability) and PK shape.
610    let prev_tables: Vec<String> = prev
611        .tables
612        .list()
613        .iter()
614        .map(|t| t.name.to_string())
615        .collect();
616    let cur_tables: Vec<String> = cur
617        .tables
618        .list()
619        .iter()
620        .map(|t| t.name.to_string())
621        .collect();
622
623    let dropped: Vec<String> = prev_tables
624        .iter()
625        .filter(|t| !cur_tables.contains(t))
626        .cloned()
627        .collect();
628    let created: Vec<String> = cur_tables
629        .iter()
630        .filter(|t| !prev_tables.contains(t))
631        .cloned()
632        .collect();
633
634    let mut candidates: BTreeMap<TableFingerprint, (Vec<String>, Vec<String>)> = BTreeMap::new();
635    for from in dropped {
636        candidates
637            .entry(table_fingerprint(&from, prev))
638            .or_default()
639            .0
640            .push(from);
641    }
642    for to in created {
643        candidates
644            .entry(table_fingerprint(&to, cur))
645            .or_default()
646            .1
647            .push(to);
648    }
649
650    for (_, (mut dropped, mut created)) in candidates {
651        if dropped.is_empty() || created.is_empty() {
652            continue;
653        }
654
655        dropped.sort();
656        created.sort();
657
658        if dropped.len() == 1 && created.len() == 1 {
659            let from = &dropped[0];
660            let to = &created[0];
661            table_renames.push(TableRename {
662                from: from.clone(),
663                to: to.clone(),
664            });
665            rename_statements.push(JsonStatement::RenameTable(RenameTableStatement {
666                from: from.clone(),
667                to: to.clone(),
668            }));
669            apply_table_rename(prev, from, to);
670        } else {
671            warnings.push(format!(
672                "Ambiguous SQLite table rename candidates between dropped tables [{}] and created tables [{}]; no rename was inferred. Use DiffOptions::rename_table(...) with diff_with or diff_schemas_with to provide an explicit rename hint.",
673                dropped.join(", "),
674                created.join(", ")
675            ));
676        }
677    }
678
679    // Column renames (within tables that exist in both): exact property match, different name.
680    let common_tables: Vec<String> = prev
681        .tables
682        .list()
683        .iter()
684        .map(|t| t.name.to_string())
685        .filter(|t| cur.tables.one(t).is_some())
686        .collect();
687
688    for table in common_tables {
689        let prev_cols: Vec<_> = prev.columns.for_table(&table);
690        let cur_cols: Vec<_> = cur.columns.for_table(&table);
691
692        let prev_names: Vec<String> = prev_cols.iter().map(|c| c.name.to_string()).collect();
693        let cur_names: Vec<String> = cur_cols.iter().map(|c| c.name.to_string()).collect();
694
695        let dropped_cols: Vec<String> = prev_names
696            .iter()
697            .filter(|c| !cur_names.contains(c))
698            .cloned()
699            .collect();
700        let created_cols: Vec<String> = cur_names
701            .iter()
702            .filter(|c| !prev_names.contains(c))
703            .cloned()
704            .collect();
705
706        if dropped_cols.len() != 1 || created_cols.len() != 1 {
707            continue;
708        }
709
710        let from = &dropped_cols[0];
711        let to = &created_cols[0];
712
713        let prev_col = prev.columns.one(&table, from);
714        let cur_col = cur.columns.one(&table, to);
715        if let (Some(prev_col), Some(cur_col)) = (prev_col, cur_col) {
716            let mut prev_cmp = prev_col.clone();
717            prev_cmp.name.clone_from(&cur_col.name);
718            if prev_cmp == *cur_col {
719                column_renames.push(ColumnRename {
720                    table: table.clone(),
721                    from: from.clone(),
722                    to: to.clone(),
723                });
724                rename_statements.push(JsonStatement::RenameColumn(RenameColumnStatement {
725                    table: table.clone(),
726                    from: from.clone(),
727                    to: to.clone(),
728                }));
729                apply_column_rename(prev, &table, from, to);
730            }
731        }
732    }
733}
734
735fn apply_table_rename(ddl: &mut SQLiteDDL, from: &str, to: &str) {
736    let to = to.to_string();
737    // Tables
738    if let Some(t) = ddl
739        .tables
740        .list_mut()
741        .iter_mut()
742        .find(|t| t.name.as_ref() == from)
743    {
744        t.name = to.clone().into();
745    }
746    // Columns
747    for c in ddl
748        .columns
749        .list_mut()
750        .iter_mut()
751        .filter(|c| c.table.as_ref() == from)
752    {
753        c.table = to.clone().into();
754    }
755    // PKs
756    for pk in ddl
757        .pks
758        .list_mut()
759        .iter_mut()
760        .filter(|pk| pk.table.as_ref() == from)
761    {
762        pk.table = to.clone().into();
763    }
764    // Uniques
765    for u in ddl
766        .uniques
767        .list_mut()
768        .iter_mut()
769        .filter(|u| u.table.as_ref() == from)
770    {
771        u.table = to.clone().into();
772    }
773    // FKs (table side and referenced side)
774    for fk in ddl.fks.list_mut().iter_mut() {
775        if fk.table.as_ref() == from {
776            fk.table = to.clone().into();
777        }
778        if fk.table_to.as_ref() == from {
779            fk.table_to = to.clone().into();
780        }
781    }
782    // Indexes
783    for idx in ddl
784        .indexes
785        .list_mut()
786        .iter_mut()
787        .filter(|i| i.table.as_ref() == from)
788    {
789        idx.table = to.clone().into();
790    }
791    // Checks
792    for chk in ddl
793        .checks
794        .list_mut()
795        .iter_mut()
796        .filter(|c| c.table.as_ref() == from)
797    {
798        chk.table = to.clone().into();
799    }
800}
801
802fn apply_column_rename(ddl: &mut SQLiteDDL, table: &str, from: &str, to: &str) {
803    let to = to.to_string();
804    // Columns
805    if let Some(c) = ddl
806        .columns
807        .list_mut()
808        .iter_mut()
809        .find(|c| c.table.as_ref() == table && c.name.as_ref() == from)
810    {
811        c.name = to.clone().into();
812    }
813    // PK columns
814    for pk in ddl
815        .pks
816        .list_mut()
817        .iter_mut()
818        .filter(|pk| pk.table.as_ref() == table)
819    {
820        for col in pk.columns.to_mut().iter_mut() {
821            if col.as_ref() == from {
822                *col = to.clone().into();
823            }
824        }
825    }
826    // Unique columns
827    for u in ddl
828        .uniques
829        .list_mut()
830        .iter_mut()
831        .filter(|u| u.table.as_ref() == table)
832    {
833        for col in u.columns.to_mut().iter_mut() {
834            if col.as_ref() == from {
835                *col = to.clone().into();
836            }
837        }
838    }
839    // FK columns
840    for fk in ddl.fks.list_mut().iter_mut() {
841        if fk.table.as_ref() == table {
842            for col in fk.columns.to_mut().iter_mut() {
843                if col.as_ref() == from {
844                    *col = to.clone().into();
845                }
846            }
847        }
848        if fk.table_to.as_ref() == table {
849            for col in fk.columns_to.to_mut().iter_mut() {
850                if col.as_ref() == from {
851                    *col = to.clone().into();
852                }
853            }
854        }
855    }
856    // Index columns (only non-expression)
857    for idx in ddl
858        .indexes
859        .list_mut()
860        .iter_mut()
861        .filter(|i| i.table.as_ref() == table)
862    {
863        for col in &mut idx.columns {
864            if !col.is_expression && col.value.as_ref() == from {
865                col.value = to.clone().into();
866            }
867        }
868    }
869}
870
871/// Prepare rename tracking strings for snapshot storage
872#[must_use]
873pub fn prepare_migration_renames(
874    table_renames: &[TableRename],
875    column_renames: &[ColumnRename],
876) -> Vec<String> {
877    let mut renames = Vec::new();
878
879    for tr in table_renames {
880        renames.push(format!("table:{}:{}", tr.from, tr.to));
881    }
882
883    for cr in column_renames {
884        renames.push(format!("column:{}:{}:{}", cr.table, cr.from, cr.to));
885    }
886
887    renames
888}
889
890#[cfg(test)]
891mod tests {
892    use super::*;
893    use crate::sqlite::ddl::{Column, ForeignKey, Index, IndexColumn, SqliteEntity, Table};
894    use std::borrow::Cow;
895
896    #[test]
897    fn test_empty_diff() {
898        let prev = SQLiteSnapshot::new();
899        let cur = SQLiteSnapshot::new();
900
901        let diff = diff_snapshots(&prev, &cur);
902        assert!(!diff.has_changes());
903    }
904
905    #[test]
906    fn test_table_creation() {
907        let prev = SQLiteSnapshot::new();
908        let mut cur = SQLiteSnapshot::new();
909
910        cur.add_entity(SqliteEntity::Table(Table::new("users")));
911        cur.add_entity(SqliteEntity::Column(
912            Column::new("users", "id", "integer").not_null(),
913        ));
914
915        let diff = diff_snapshots(&prev, &cur);
916        assert!(diff.has_changes());
917        assert_eq!(diff.created_tables().len(), 1);
918    }
919
920    #[test]
921    fn test_table_deletion() {
922        let mut prev = SQLiteSnapshot::new();
923        let cur = SQLiteSnapshot::new();
924
925        prev.add_entity(SqliteEntity::Table(Table::new("users")));
926
927        let diff = diff_snapshots(&prev, &cur);
928        assert!(diff.has_changes());
929        assert_eq!(diff.dropped_tables().len(), 1);
930    }
931
932    fn sqlite_table_with_id(table: &str) -> SQLiteDDL {
933        let mut ddl = SQLiteDDL::new();
934        ddl.tables.push(Table::new(table.to_string()));
935        ddl.columns
936            .push(Column::new(table.to_string(), "id", "integer").not_null());
937        ddl
938    }
939
940    #[test]
941    fn pure_table_rename_emits_single_rename_statement() {
942        let prev = sqlite_table_with_id("users");
943        let cur = sqlite_table_with_id("accounts");
944
945        let migration = compute_migration(&prev, &cur);
946
947        assert_eq!(migration.statements.len(), 1);
948        assert!(matches!(
949            migration.statements[0],
950            JsonStatement::RenameTable(_)
951        ));
952        assert_eq!(
953            migration.sql_statements,
954            vec!["ALTER TABLE `users` RENAME TO `accounts`;"]
955        );
956        assert!(
957            !migration
958                .statements
959                .iter()
960                .any(|statement| matches!(statement, JsonStatement::DropTable(_)))
961        );
962    }
963
964    #[test]
965    fn table_rename_rewrites_indexes_and_foreign_keys() {
966        let mut prev = sqlite_table_with_id("users");
967        prev.tables.push(Table::new("posts"));
968        prev.columns
969            .push(Column::new("posts", "id", "integer").not_null());
970        prev.columns
971            .push(Column::new("posts", "user_id", "integer").not_null());
972        prev.indexes.push(Index::new(
973            "users",
974            "idx_users_id",
975            vec![IndexColumn::new("id")],
976        ));
977        prev.fks.push(ForeignKey::new(
978            "posts",
979            "fk_posts_user",
980            vec![Cow::Borrowed("user_id")],
981            "users",
982            vec![Cow::Borrowed("id")],
983        ));
984
985        let mut cur = sqlite_table_with_id("accounts");
986        cur.tables.push(Table::new("posts"));
987        cur.columns
988            .push(Column::new("posts", "id", "integer").not_null());
989        cur.columns
990            .push(Column::new("posts", "user_id", "integer").not_null());
991        cur.indexes.push(Index::new(
992            "accounts",
993            "idx_users_id",
994            vec![IndexColumn::new("id")],
995        ));
996        cur.fks.push(ForeignKey::new(
997            "posts",
998            "fk_posts_user",
999            vec![Cow::Borrowed("user_id")],
1000            "accounts",
1001            vec![Cow::Borrowed("id")],
1002        ));
1003
1004        let migration = compute_migration(&prev, &cur);
1005
1006        assert_eq!(
1007            migration.sql_statements,
1008            vec!["ALTER TABLE `users` RENAME TO `accounts`;"]
1009        );
1010        assert!(
1011            !migration.sql_statements.iter().any(|statement| {
1012                statement.starts_with("DROP")
1013                    || statement.starts_with("CREATE INDEX")
1014                    || statement.contains("fk_posts_user")
1015            }),
1016            "unexpected dependent churn: {:?}",
1017            migration.sql_statements
1018        );
1019    }
1020
1021    #[test]
1022    fn ambiguous_table_rename_does_not_guess_and_warns() {
1023        let mut prev = sqlite_table_with_id("users");
1024        let mut admins = sqlite_table_with_id("admins");
1025        prev.tables.list_mut().append(admins.tables.list_mut());
1026        prev.columns.list_mut().append(admins.columns.list_mut());
1027        let cur = sqlite_table_with_id("accounts");
1028
1029        let migration = compute_migration(&prev, &cur);
1030
1031        assert!(
1032            migration.warnings.iter().any(|warning| warning
1033                .contains("Ambiguous SQLite table rename candidates")
1034                && warning.contains("rename_table")),
1035            "expected ambiguous rename warning, got {:?}",
1036            migration.warnings
1037        );
1038        assert!(
1039            !migration
1040                .statements
1041                .iter()
1042                .any(|statement| matches!(statement, JsonStatement::RenameTable(_)))
1043        );
1044    }
1045
1046    #[test]
1047    fn test_column_nullable_change() {
1048        // Test that changing Option<String> to String (nullable to not null) is detected
1049        let mut prev = SQLiteSnapshot::new();
1050        prev.add_entity(SqliteEntity::Table(Table::new("users")));
1051        prev.add_entity(SqliteEntity::Column(Column::new("users", "email", "text"))); // nullable
1052
1053        let mut cur = SQLiteSnapshot::new();
1054        cur.add_entity(SqliteEntity::Table(Table::new("users")));
1055        cur.add_entity(SqliteEntity::Column(
1056            Column::new("users", "email", "text").not_null(),
1057        )); // not null
1058
1059        let diff = diff_snapshots(&prev, &cur);
1060        assert!(diff.has_changes(), "Should detect nullable change");
1061
1062        // Should be an Alter diff for the column
1063        let altered = diff.altered();
1064        assert_eq!(altered.len(), 1, "Should have one altered entity");
1065        assert_eq!(altered[0].kind, crate::traits::EntityKind::Column);
1066        assert_eq!(altered[0].name, "users:email");
1067    }
1068
1069    #[test]
1070    fn test_column_not_null_to_nullable() {
1071        // Test that changing String to Option<String> (not null to nullable) is detected
1072        let mut prev = SQLiteSnapshot::new();
1073        prev.add_entity(SqliteEntity::Table(Table::new("users")));
1074        prev.add_entity(SqliteEntity::Column(
1075            Column::new("users", "email", "text").not_null(),
1076        )); // not null
1077
1078        let mut cur = SQLiteSnapshot::new();
1079        cur.add_entity(SqliteEntity::Table(Table::new("users")));
1080        cur.add_entity(SqliteEntity::Column(Column::new("users", "email", "text"))); // nullable
1081
1082        let diff = diff_snapshots(&prev, &cur);
1083        assert!(diff.has_changes(), "Should detect nullable change");
1084
1085        // Should be an Alter diff for the column
1086        let altered = diff.altered();
1087        assert_eq!(altered.len(), 1, "Should have one altered entity");
1088        assert_eq!(altered[0].kind, crate::traits::EntityKind::Column);
1089    }
1090
1091    #[test]
1092    fn test_column_nullable_change_generates_sql() {
1093        // Test that changing nullable to not null generates RecreateTable SQL
1094        let mut prev_ddl = SQLiteDDL::new();
1095        prev_ddl.tables.push(Table::new("users"));
1096        prev_ddl
1097            .columns
1098            .push(Column::new("users", "id", "integer").not_null());
1099        prev_ddl.columns.push(Column::new("users", "email", "text")); // nullable
1100
1101        let mut cur_ddl = SQLiteDDL::new();
1102        cur_ddl.tables.push(Table::new("users"));
1103        cur_ddl
1104            .columns
1105            .push(Column::new("users", "id", "integer").not_null());
1106        cur_ddl
1107            .columns
1108            .push(Column::new("users", "email", "text").not_null()); // not null
1109
1110        let migration = compute_migration(&prev_ddl, &cur_ddl);
1111
1112        // Should have generated SQL statements
1113        assert!(
1114            !migration.sql_statements.is_empty(),
1115            "Should generate SQL statements"
1116        );
1117
1118        // Should have a RecreateTable statement
1119        let has_recreate = migration
1120            .statements
1121            .iter()
1122            .any(|s| matches!(s, JsonStatement::RecreateTable(_)));
1123        assert!(
1124            has_recreate,
1125            "Should have RecreateTable statement for column alteration"
1126        );
1127
1128        // Verify individual SQL statements for table recreation pattern
1129        assert_eq!(migration.sql_statements[0], "PRAGMA foreign_keys=OFF;");
1130        assert!(
1131            migration.sql_statements[1].starts_with("CREATE TABLE `__new_users`"),
1132            "Expected CREATE TABLE `__new_users`, got: {}",
1133            migration.sql_statements[1]
1134        );
1135        assert!(
1136            migration.sql_statements[1].contains("`email` TEXT NOT NULL"),
1137            "New table should have NOT NULL on email: {}",
1138            migration.sql_statements[1]
1139        );
1140        assert_eq!(
1141            migration.sql_statements[2],
1142            "INSERT INTO `__new_users`(`id`, `email`) SELECT `id`, `email` FROM `users`;"
1143        );
1144        assert_eq!(migration.sql_statements[3], "DROP TABLE `users`;");
1145        assert_eq!(
1146            migration.sql_statements[4],
1147            "ALTER TABLE `__new_users` RENAME TO `users`;"
1148        );
1149        assert_eq!(migration.sql_statements[5], "PRAGMA foreign_keys=ON;");
1150    }
1151
1152    #[test]
1153    fn strict_toggle_generates_table_recreate() {
1154        let mut prev = SQLiteDDL::new();
1155        prev.tables.push(Table::new("users"));
1156        prev.columns
1157            .push(Column::new("users", "id", "integer").not_null());
1158
1159        let mut cur = SQLiteDDL::new();
1160        cur.tables.push(Table::new("users").strict());
1161        cur.columns
1162            .push(Column::new("users", "id", "integer").not_null());
1163
1164        let migration = compute_migration(&prev, &cur);
1165
1166        let has_recreate = migration
1167            .statements
1168            .iter()
1169            .any(|s| matches!(s, JsonStatement::RecreateTable(_)));
1170        assert!(
1171            has_recreate,
1172            "toggling STRICT must recreate the table, got: {:?}",
1173            migration.statements
1174        );
1175        assert!(
1176            migration.sql_statements.iter().any(|sql| sql
1177                .starts_with("CREATE TABLE `__new_users`")
1178                && sql.ends_with("STRICT;")),
1179            "recreated table must carry STRICT: {:?}",
1180            migration.sql_statements
1181        );
1182    }
1183
1184    #[test]
1185    fn partial_index_predicate_change_recreates_index() {
1186        let mut prev = sqlite_table_with_id("jobs");
1187        let mut previous_index =
1188            Index::new("jobs", "idx_jobs_unclaimed", vec![IndexColumn::new("id")]);
1189        previous_index.where_clause = Some(Cow::Borrowed("builder IS NULL"));
1190        prev.indexes.push(previous_index);
1191
1192        let mut cur = sqlite_table_with_id("jobs");
1193        let mut current_index =
1194            Index::new("jobs", "idx_jobs_unclaimed", vec![IndexColumn::new("id")]);
1195        current_index.where_clause = Some(Cow::Borrowed("builder IS NOT NULL"));
1196        cur.indexes.push(current_index);
1197
1198        let migration = compute_migration(&prev, &cur);
1199        assert_eq!(
1200            migration.sql_statements,
1201            vec![
1202                "DROP INDEX IF EXISTS `idx_jobs_unclaimed`;",
1203                "CREATE INDEX `idx_jobs_unclaimed` ON `jobs`(`id`) WHERE builder IS NOT NULL;",
1204            ]
1205        );
1206    }
1207
1208    #[test]
1209    fn without_rowid_toggle_generates_table_recreate() {
1210        let mut prev = SQLiteDDL::new();
1211        prev.tables.push(Table::new("kv"));
1212        prev.columns
1213            .push(Column::new("kv", "key", "text").not_null());
1214
1215        let mut cur = SQLiteDDL::new();
1216        cur.tables.push(Table::new("kv").without_rowid());
1217        cur.columns
1218            .push(Column::new("kv", "key", "text").not_null());
1219
1220        let migration = compute_migration(&prev, &cur);
1221
1222        assert!(
1223            migration
1224                .statements
1225                .iter()
1226                .any(|s| matches!(s, JsonStatement::RecreateTable(_))),
1227            "toggling WITHOUT ROWID must recreate the table, got: {:?}",
1228            migration.statements
1229        );
1230    }
1231
1232    #[test]
1233    fn multi_table_recreation_order_is_deterministic() {
1234        let make = |not_null: bool| {
1235            let mut ddl = SQLiteDDL::new();
1236            for table in ["zeta", "alpha", "midway"] {
1237                ddl.tables.push(Table::new(table.to_string()));
1238                let col = Column::new(table.to_string(), "name", "text");
1239                ddl.columns
1240                    .push(if not_null { col.not_null() } else { col });
1241            }
1242            ddl
1243        };
1244
1245        let migration = compute_migration(&make(false), &make(true));
1246        let recreate_order: Vec<String> = migration
1247            .statements
1248            .iter()
1249            .filter_map(|s| match s {
1250                JsonStatement::RecreateTable(st) => Some(st.to.name.clone()),
1251                _ => None,
1252            })
1253            .collect();
1254        assert_eq!(
1255            recreate_order,
1256            vec!["alpha", "midway", "zeta"],
1257            "table recreation must be emitted in sorted order"
1258        );
1259    }
1260
1261    #[test]
1262    fn test_column_type_change_generates_recreate() {
1263        // Test that changing column type generates RecreateTable
1264        let mut prev_ddl = SQLiteDDL::new();
1265        prev_ddl.tables.push(Table::new("users"));
1266        prev_ddl.columns.push(Column::new("users", "age", "text")); // text
1267
1268        let mut cur_ddl = SQLiteDDL::new();
1269        cur_ddl.tables.push(Table::new("users"));
1270        cur_ddl.columns.push(Column::new("users", "age", "integer")); // integer
1271
1272        let migration = compute_migration(&prev_ddl, &cur_ddl);
1273
1274        // Should have a RecreateTable statement
1275        let has_recreate = migration
1276            .statements
1277            .iter()
1278            .any(|s| matches!(s, JsonStatement::RecreateTable(_)));
1279        assert!(
1280            has_recreate,
1281            "Should have RecreateTable statement for type change"
1282        );
1283    }
1284}