Skip to main content

drizzle_migrations/sqlite/
statements.rs

1//! `SQLite` SQL statement types and generation (v7 DDL format)
2//!
3//! This implements the full statement generation from drizzle-kit beta.
4//! - `JsonStatement` enum represents migration operations
5//! - Convertor functions convert statements to SQL strings
6
7use crate::sqlite::ddl::{
8    CheckConstraint, Column, ForeignKey, Index, PrimaryKey, Table, TableSql, UniqueConstraint, View,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13/// SQL statement breakpoint marker (used by drizzle-kit)
14pub const BREAKPOINT: &str = "--> statement-breakpoint";
15
16fn quote_ident(ident: &str) -> String {
17    format!("`{}`", ident.replace('`', "``"))
18}
19
20// =============================================================================
21// JSON Statement Types (matching statements.ts)
22// =============================================================================
23
24/// Full table information for create/recreate operations
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct TableFull {
28    pub name: String,
29    pub columns: Vec<Column>,
30    pub pk: Option<PrimaryKey>,
31    pub fks: Vec<ForeignKey>,
32    pub uniques: Vec<UniqueConstraint>,
33    pub checks: Vec<CheckConstraint>,
34    /// Whether the table has STRICT mode enabled
35    #[serde(default)]
36    pub strict: bool,
37    /// Whether the table is WITHOUT ROWID
38    #[serde(default)]
39    pub without_rowid: bool,
40}
41
42impl TableFull {
43    #[must_use]
44    pub fn new(name: &str) -> Self {
45        Self {
46            name: name.to_string(),
47            columns: Vec::new(),
48            pk: None,
49            fks: Vec::new(),
50            uniques: Vec::new(),
51            checks: Vec::new(),
52            strict: false,
53            without_rowid: false,
54        }
55    }
56}
57
58fn table_full_to_table(table: &TableFull) -> Table {
59    Table {
60        name: table.name.clone().into(),
61        strict: table.strict,
62        without_rowid: table.without_rowid,
63    }
64}
65
66fn create_table_sql(table: &TableFull) -> String {
67    let ddl_table = table_full_to_table(table);
68    TableSql::new(&ddl_table)
69        .columns(&table.columns)
70        .primary_key(table.pk.as_ref())
71        .foreign_keys(&table.fks)
72        .unique_constraints(&table.uniques)
73        .check_constraints(&table.checks)
74        .create_table_sql()
75}
76
77/// All possible JSON statement types
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(tag = "type", rename_all = "snake_case")]
80pub enum JsonStatement {
81    CreateTable(CreateTableStatement),
82    DropTable(DropTableStatement),
83    RenameTable(RenameTableStatement),
84    AddColumn(AddColumnStatement),
85    DropColumn(DropColumnStatement),
86    RenameColumn(RenameColumnStatement),
87    RecreateColumn(RecreateColumnStatement),
88    RecreateTable(RecreateTableStatement),
89    CreateIndex(CreateIndexStatement),
90    DropIndex(DropIndexStatement),
91    CreateView(CreateViewStatement),
92    DropView(DropViewStatement),
93    RenameView(RenameViewStatement),
94}
95
96impl JsonStatement {
97    /// Get the type name of this statement
98    #[must_use]
99    pub const fn type_name(&self) -> &'static str {
100        match self {
101            Self::CreateTable(_) => "create_table",
102            Self::DropTable(_) => "drop_table",
103            Self::RenameTable(_) => "rename_table",
104            Self::AddColumn(_) => "add_column",
105            Self::DropColumn(_) => "drop_column",
106            Self::RenameColumn(_) => "rename_column",
107            Self::RecreateColumn(_) => "recreate_column",
108            Self::RecreateTable(_) => "recreate_table",
109            Self::CreateIndex(_) => "create_index",
110            Self::DropIndex(_) => "drop_index",
111            Self::CreateView(_) => "create_view",
112            Self::DropView(_) => "drop_view",
113            Self::RenameView(_) => "rename_view",
114        }
115    }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct CreateTableStatement {
121    pub table: TableFull,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct DropTableStatement {
127    pub table_name: String,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct RenameTableStatement {
133    pub from: String,
134    pub to: String,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct AddColumnStatement {
140    pub column: Column,
141    pub fk: Option<ForeignKey>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct DropColumnStatement {
147    pub column: Column,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct RenameColumnStatement {
153    pub table: String,
154    pub from: String,
155    pub to: String,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct RecreateColumnStatement {
161    pub column: Column,
162    pub fk: Option<ForeignKey>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct RecreateTableStatement {
168    pub from: TableFull,
169    pub to: TableFull,
170    #[serde(default)]
171    pub data: Option<RebuildTableData>,
172}
173
174/// Validated data movement rendered as part of one table rebuild.
175#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "camelCase", deny_unknown_fields)]
177pub struct RebuildTableData {
178    pub copies: BTreeMap<String, RebuildCopyExpression>,
179    pub validations: Vec<RebuildDataValidation>,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
184pub enum RebuildCopyExpression {
185    HexTextToBlob {
186        source: String,
187    },
188    IntegerMap {
189        source: String,
190        cases: Vec<(i64, i64)>,
191    },
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
195#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
196pub enum RebuildDataValidation {
197    HexText { column: String, bytes: usize },
198    JsonValid { column: String },
199    IntegerSet { column: String, allowed: Vec<i64> },
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct CreateIndexStatement {
205    pub index: Index,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct DropIndexStatement {
211    pub index: Index,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct CreateViewStatement {
217    pub view: View,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct DropViewStatement {
223    pub view: View,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct RenameViewStatement {
229    pub from: View,
230    pub to: View,
231}
232
233// =============================================================================
234// Convertor - Statement to SQL (matching convertor.ts)
235// =============================================================================
236
237/// Convert a JSON statement to SQL string(s)
238#[must_use]
239pub fn convert_statement(statement: &JsonStatement) -> Vec<String> {
240    match statement {
241        JsonStatement::CreateTable(st) => vec![convert_create_table(st)],
242        JsonStatement::DropTable(st) => vec![convert_drop_table(st)],
243        JsonStatement::RenameTable(st) => vec![convert_rename_table(st)],
244        JsonStatement::AddColumn(st) => vec![convert_add_column(st)],
245        JsonStatement::DropColumn(st) => vec![convert_drop_column(st)],
246        JsonStatement::RenameColumn(st) => vec![convert_rename_column(st)],
247        JsonStatement::RecreateColumn(st) => convert_recreate_column(st),
248        JsonStatement::RecreateTable(st) => convert_recreate_table(st),
249        JsonStatement::CreateIndex(st) => vec![convert_create_index(st)],
250        JsonStatement::DropIndex(st) => vec![convert_drop_index(st)],
251        JsonStatement::CreateView(st) => vec![convert_create_view(st)],
252        JsonStatement::DropView(st) => vec![convert_drop_view(st)],
253        JsonStatement::RenameView(st) => convert_rename_view(st),
254    }
255}
256
257/// Convert multiple statements to SQL with optional breakpoints
258pub fn statements_to_sql(statements: &[JsonStatement], breakpoints: bool) -> String {
259    let sql_statements: Vec<String> = statements.iter().flat_map(convert_statement).collect();
260
261    if breakpoints {
262        sql_statements.join(&format!("\n{BREAKPOINT}\n"))
263    } else {
264        sql_statements.join("\n")
265    }
266}
267
268/// A grouped statement with its corresponding SQL
269#[derive(Debug, Clone)]
270pub struct GroupedStatement {
271    /// The JSON statement
272    pub json_statement: JsonStatement,
273    /// The generated SQL statements
274    pub sql_statements: Vec<String>,
275}
276
277/// Result of converting JSON statements to SQL
278#[derive(Debug, Clone)]
279pub struct ConversionResult {
280    /// All SQL statements flattened
281    pub sql_statements: Vec<String>,
282    /// Statements grouped with their JSON source
283    pub grouped_statements: Vec<GroupedStatement>,
284}
285
286/// Convert JSON statements to SQL with grouping information
287#[must_use]
288pub fn from_json(statements: Vec<JsonStatement>) -> ConversionResult {
289    let grouped: Vec<GroupedStatement> = statements
290        .into_iter()
291        .map(|statement| {
292            let sql_statements = convert_statement(&statement);
293            GroupedStatement {
294                json_statement: statement,
295                sql_statements,
296            }
297        })
298        .collect();
299
300    let sql_statements: Vec<String> = grouped
301        .iter()
302        .flat_map(|g| g.sql_statements.clone())
303        .collect();
304
305    ConversionResult {
306        sql_statements,
307        grouped_statements: grouped,
308    }
309}
310
311// =============================================================================
312// Individual Convertors
313// =============================================================================
314
315fn convert_create_table(st: &CreateTableStatement) -> String {
316    create_table_sql(&st.table)
317}
318
319fn convert_drop_table(st: &DropTableStatement) -> String {
320    format!("DROP TABLE {};", quote_ident(&st.table_name))
321}
322
323fn convert_rename_table(st: &RenameTableStatement) -> String {
324    format!(
325        "ALTER TABLE {} RENAME TO {};",
326        quote_ident(&st.from),
327        quote_ident(&st.to)
328    )
329}
330
331fn convert_add_column(st: &AddColumnStatement) -> String {
332    let column = &st.column;
333    let column_def = column.to_column_sql(false, false);
334
335    let reference = st
336        .fk
337        .as_ref()
338        .map(|fk| {
339            let to_cols = fk
340                .columns_to
341                .iter()
342                .map(|c| quote_ident(c))
343                .collect::<Vec<_>>()
344                .join(",");
345            if fk.name_explicit {
346                format!(
347                    " CONSTRAINT {} REFERENCES {}({})",
348                    quote_ident(&fk.name),
349                    quote_ident(&fk.table_to),
350                    to_cols
351                )
352            } else {
353                format!(" REFERENCES {}({})", quote_ident(&fk.table_to), to_cols)
354            }
355        })
356        .unwrap_or_default();
357
358    format!(
359        "ALTER TABLE {} ADD {}{};",
360        quote_ident(&column.table),
361        column_def,
362        reference
363    )
364}
365
366fn convert_drop_column(st: &DropColumnStatement) -> String {
367    format!(
368        "ALTER TABLE {} DROP COLUMN {};",
369        quote_ident(&st.column.table),
370        quote_ident(&st.column.name)
371    )
372}
373
374fn convert_rename_column(st: &RenameColumnStatement) -> String {
375    format!(
376        "ALTER TABLE {} RENAME COLUMN {} TO {};",
377        quote_ident(&st.table),
378        quote_ident(&st.from),
379        quote_ident(&st.to)
380    )
381}
382
383fn convert_recreate_column(st: &RecreateColumnStatement) -> Vec<String> {
384    // Drop and re-add the column
385    let drop = format!(
386        "ALTER TABLE {} DROP COLUMN {};",
387        quote_ident(&st.column.table),
388        quote_ident(&st.column.name)
389    );
390    let add = convert_add_column(&AddColumnStatement {
391        column: st.column.clone(),
392        fk: st.fk.clone(),
393    });
394    vec![drop, add]
395}
396
397fn convert_recreate_table(st: &RecreateTableStatement) -> Vec<String> {
398    let name = &st.to.name;
399    let new_table_name = format!("__new_{name}");
400
401    let data = st.data.as_ref();
402    let copied_columns = st
403        .to
404        .columns
405        .iter()
406        .filter(|target| {
407            target.generated.is_none()
408                && (data.is_some_and(|data| data.copies.contains_key(target.name.as_ref()))
409                    || st
410                        .from
411                        .columns
412                        .iter()
413                        .any(|source| source.name == target.name && source.generated.is_none()))
414        })
415        .collect::<Vec<_>>();
416    let cols_str = copied_columns
417        .iter()
418        .map(|column| quote_ident(&column.name))
419        .collect::<Vec<_>>()
420        .join(", ");
421    let select_str = copied_columns
422        .iter()
423        .map(|column| {
424            data.and_then(|data| data.copies.get(column.name.as_ref()))
425                .map_or_else(|| quote_ident(&column.name), render_copy_expression)
426        })
427        .collect::<Vec<_>>()
428        .join(", ");
429
430    let mut statements = Vec::new();
431
432    if let Some(data) = data
433        && !data.validations.is_empty()
434    {
435        let guard = format!("__drizzle_rebuild_guard_{name}");
436        statements.push(format!(
437            "CREATE TEMP TABLE {} (`valid` INTEGER NOT NULL CHECK (`valid` = 1)) STRICT;",
438            quote_ident(&guard)
439        ));
440        for validation in &data.validations {
441            statements.push(format!(
442                "INSERT INTO {}(`valid`) SELECT 0 FROM {} WHERE {} LIMIT 1;",
443                quote_ident(&guard),
444                quote_ident(name),
445                render_invalid_predicate(validation)
446            ));
447        }
448        statements.push(format!("DROP TABLE {};", quote_ident(&guard)));
449    }
450
451    // 1. Disable foreign keys
452    statements.push("PRAGMA foreign_keys=OFF;".to_string());
453
454    // 2. Create new table with temp name
455    let mut tmp_table = st.to.clone();
456    tmp_table.name.clone_from(&new_table_name);
457    // Update check constraint table references
458    for check in &mut tmp_table.checks {
459        check.table = new_table_name.clone().into();
460    }
461    statements.push(convert_create_table(&CreateTableStatement {
462        table: tmp_table,
463    }));
464
465    // 3. Copy data. If the new table has NOT NULL columns without a default
466    // that the old table can't provide, the copy will fail on non-empty
467    // tables — keep generating (matching drizzle-kit) but flag it.
468    let missing_not_null: Vec<&str> = st
469        .to
470        .columns
471        .iter()
472        .filter(|col| {
473            col.not_null
474                && col.default.is_none()
475                && col.generated.is_none()
476                && !st.from.columns.iter().any(|from| from.name == col.name)
477        })
478        .map(|col| col.name.as_ref())
479        .collect();
480    let warning = if missing_not_null.is_empty() {
481        String::new()
482    } else {
483        format!(
484            "-- WARNING: new NOT NULL column(s) without a default: {} — the copy below fails if `{name}` has rows\n",
485            missing_not_null.join(", ")
486        )
487    };
488    statements.push(format!(
489        "{warning}INSERT INTO {}({cols_str}) SELECT {select_str} FROM {};",
490        quote_ident(&new_table_name),
491        quote_ident(name)
492    ));
493
494    // 4. Drop old table
495    statements.push(format!("DROP TABLE {};", quote_ident(name)));
496
497    // 5. Rename new table
498    statements.push(format!(
499        "ALTER TABLE {} RENAME TO {};",
500        quote_ident(&new_table_name),
501        quote_ident(name)
502    ));
503
504    // 6. Re-enable foreign keys
505    statements.push("PRAGMA foreign_keys=ON;".to_string());
506
507    statements
508}
509
510fn render_copy_expression(expression: &RebuildCopyExpression) -> String {
511    match expression {
512        RebuildCopyExpression::HexTextToBlob { source } => {
513            format!("unhex({})", quote_ident(source))
514        }
515        RebuildCopyExpression::IntegerMap { source, cases } => {
516            let cases = cases
517                .iter()
518                .map(|(from, to)| format!(" WHEN {from} THEN {to}"))
519                .collect::<String>();
520            format!("CASE {}{cases} ELSE NULL END", quote_ident(source))
521        }
522    }
523}
524
525fn render_invalid_predicate(validation: &RebuildDataValidation) -> String {
526    match validation {
527        RebuildDataValidation::HexText { column, bytes } => {
528            let column = quote_ident(column);
529            let chars = bytes.saturating_mul(2);
530            format!(
531                "{column} IS NOT NULL AND (typeof({column}) <> 'text' OR length({column}) <> {chars} OR coalesce(length(unhex({column})), -1) <> {bytes})"
532            )
533        }
534        RebuildDataValidation::JsonValid { column } => {
535            let column = quote_ident(column);
536            format!(
537                "{column} IS NOT NULL AND (typeof({column}) <> 'text' OR json_valid({column}) <> 1)"
538            )
539        }
540        RebuildDataValidation::IntegerSet { column, allowed } => {
541            let column = quote_ident(column);
542            let allowed = allowed
543                .iter()
544                .map(i64::to_string)
545                .collect::<Vec<_>>()
546                .join(", ");
547            format!(
548                "{column} IS NOT NULL AND (typeof({column}) <> 'integer' OR {column} NOT IN ({allowed}))"
549            )
550        }
551    }
552}
553
554fn convert_create_index(st: &CreateIndexStatement) -> String {
555    st.index.create_index_sql()
556}
557
558fn convert_drop_index(st: &DropIndexStatement) -> String {
559    format!("DROP INDEX IF EXISTS {};", quote_ident(&st.index.name))
560}
561
562fn convert_create_view(st: &CreateViewStatement) -> String {
563    st.view.create_view_sql()
564}
565
566fn convert_drop_view(st: &DropViewStatement) -> String {
567    format!("DROP VIEW {};", quote_ident(&st.view.name))
568}
569
570fn convert_rename_view(st: &RenameViewStatement) -> Vec<String> {
571    // SQLite doesn't support RENAME VIEW, so we drop and recreate.
572    // Two separate statements: executors run one statement per string.
573    vec![
574        format!("DROP VIEW IF EXISTS {};", quote_ident(&st.from.name)),
575        st.to.create_view_sql(),
576    ]
577}
578
579// =============================================================================
580// Statement Preparation Helpers
581// =============================================================================
582
583/// Prepare add column statements with FK associations
584#[must_use]
585pub fn prepare_add_columns(columns: &[Column], fks: &[ForeignKey]) -> Vec<AddColumnStatement> {
586    columns
587        .iter()
588        .map(|col| {
589            let fk = fks
590                .iter()
591                .find(|fk| {
592                    fk.columns.len() == 1 && fk.columns[0] == col.name && fk.table == col.table
593                })
594                .cloned();
595            AddColumnStatement {
596                column: col.clone(),
597                fk,
598            }
599        })
600        .collect()
601}
602
603// =============================================================================
604// Topological Sorting for Table Dependencies
605// =============================================================================
606
607use crate::sqlite::SchemaDiff;
608use crate::sqlite::collection::{DiffType, EntityDiff};
609use crate::traits::EntityKind;
610use std::collections::{HashMap, HashSet};
611
612/// Result of topological sorting with circular dependency detection
613pub struct TopologicalSortResult<'a> {
614    /// Sorted tables
615    pub tables: Vec<&'a EntityDiff>,
616    /// Whether circular dependencies were detected
617    pub has_circular_deps: bool,
618}
619
620/// Topological sort tables for CREATE: referenced tables come first
621fn topological_sort_tables_for_create<'a>(
622    tables: &[&'a EntityDiff],
623    diff: &SchemaDiff,
624) -> TopologicalSortResult<'a> {
625    if tables.len() <= 1 {
626        return TopologicalSortResult {
627            tables: tables.to_vec(),
628            has_circular_deps: false,
629        };
630    }
631
632    // Build a map of table name -> entity diff
633    let mut table_map: HashMap<String, &EntityDiff> = HashMap::new();
634    for t in tables {
635        if let Some(name) = t.name.split(':').next_back() {
636            table_map.insert(name.to_string(), *t);
637        }
638    }
639
640    // Build dependency graph: table -> tables it depends on (via FKs)
641    let mut dependencies: HashMap<String, HashSet<String>> = HashMap::new();
642    for table_name in table_map.keys() {
643        dependencies.insert(table_name.clone(), HashSet::new());
644    }
645
646    // Find FK dependencies
647    for fk_diff in diff.by_kind(EntityKind::ForeignKey) {
648        if fk_diff.diff_type == DiffType::Create
649            && let Some(crate::sqlite::ddl::SqliteEntity::ForeignKey(fk)) = fk_diff.right.as_ref()
650        {
651            let from_table = fk.table.to_string();
652            let to_table = fk.table_to.to_string();
653            // from_table depends on to_table (to_table must be created first)
654            if table_map.contains_key(&from_table)
655                && table_map.contains_key(&to_table)
656                && let Some(deps) = dependencies.get_mut(&from_table)
657            {
658                deps.insert(to_table);
659            }
660        }
661    }
662
663    // Tables with no dependencies come first, then tables that depend on them, etc.
664    // Ready-sets and the circular-dependency fallback are sorted so the emitted
665    // SQL order is deterministic.
666    let mut result = Vec::new();
667    let mut remaining: HashSet<String> = table_map.keys().cloned().collect();
668    let mut satisfied: HashSet<String> = HashSet::new();
669    let mut has_circular_deps = false;
670
671    while !remaining.is_empty() {
672        // Find tables whose dependencies are all satisfied
673        let mut ready: Vec<String> = remaining
674            .iter()
675            .filter(|t| {
676                dependencies
677                    .get(*t)
678                    .is_none_or(|deps| deps.iter().all(|d| satisfied.contains(d)))
679            })
680            .cloned()
681            .collect();
682        ready.sort_unstable();
683
684        if ready.is_empty() {
685            // Circular dependency detected - add remaining in sorted order
686            has_circular_deps = true;
687            let mut leftover: Vec<&String> = remaining.iter().collect();
688            leftover.sort_unstable();
689            for t in leftover {
690                if let Some(entity) = table_map.get(t) {
691                    result.push(*entity);
692                }
693            }
694            break;
695        }
696
697        for t in ready {
698            remaining.remove(&t);
699            satisfied.insert(t.clone());
700            if let Some(entity) = table_map.get(&t) {
701                result.push(*entity);
702            }
703        }
704    }
705
706    TopologicalSortResult {
707        tables: result,
708        has_circular_deps,
709    }
710}
711
712/// Topological sort tables for DROP: tables with FKs come first (reverse of create)
713fn topological_sort_tables_for_drop<'a>(
714    tables: &[&'a EntityDiff],
715    diff: &SchemaDiff,
716) -> TopologicalSortResult<'a> {
717    // For drops, reverse the create order: tables that reference others drop first
718    let create_result = topological_sort_tables_for_create(tables, diff);
719    TopologicalSortResult {
720        tables: create_result.tables.into_iter().rev().collect(),
721        has_circular_deps: create_result.has_circular_deps,
722    }
723}
724
725fn append_drop_table_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
726    let dropped_tables = diff.dropped_tables();
727    let drop_result = topological_sort_tables_for_drop(&dropped_tables, diff);
728
729    // If there are circular dependencies in drops, wrap with PRAGMA
730    if drop_result.has_circular_deps && !drop_result.tables.is_empty() {
731        statements.push("PRAGMA foreign_keys=OFF;".to_string());
732    }
733
734    for entity_diff in &drop_result.tables {
735        if let Some(name) = entity_diff.name.split(':').next_back() {
736            statements.push(convert_drop_table(&DropTableStatement {
737                table_name: name.to_string(),
738            }));
739        }
740    }
741
742    if drop_result.has_circular_deps && !drop_result.tables.is_empty() {
743        statements.push("PRAGMA foreign_keys=ON;".to_string());
744    }
745}
746
747fn build_table_full(diff: &SchemaDiff, table: &crate::sqlite::ddl::Table) -> TableFull {
748    let columns_for_table: Vec<Column> = diff
749        .by_kind(EntityKind::Column)
750        .into_iter()
751        .filter(|d| d.diff_type == DiffType::Create)
752        .filter_map(|d| d.right.as_ref())
753        .filter_map(|e| match e {
754            crate::sqlite::ddl::SqliteEntity::Column(c) if c.table == table.name => Some(c.clone()),
755            _ => None,
756        })
757        .collect();
758
759    let pk = diff
760        .by_kind(EntityKind::PrimaryKey)
761        .into_iter()
762        .filter(|d| d.diff_type == DiffType::Create)
763        .filter_map(|d| d.right.as_ref())
764        .find_map(|e| match e {
765            crate::sqlite::ddl::SqliteEntity::PrimaryKey(p) if p.table == table.name => {
766                Some(p.clone())
767            }
768            _ => None,
769        });
770
771    let fks: Vec<ForeignKey> = diff
772        .by_kind(EntityKind::ForeignKey)
773        .into_iter()
774        .filter(|d| d.diff_type == DiffType::Create)
775        .filter_map(|d| d.right.as_ref())
776        .filter_map(|e| match e {
777            crate::sqlite::ddl::SqliteEntity::ForeignKey(fk) if fk.table == table.name => {
778                Some(fk.clone())
779            }
780            _ => None,
781        })
782        .collect();
783
784    let uniques: Vec<UniqueConstraint> = diff
785        .by_kind(EntityKind::UniqueConstraint)
786        .into_iter()
787        .filter(|d| d.diff_type == DiffType::Create)
788        .filter_map(|d| d.right.as_ref())
789        .filter_map(|e| match e {
790            crate::sqlite::ddl::SqliteEntity::UniqueConstraint(u) if u.table == table.name => {
791                Some(u.clone())
792            }
793            _ => None,
794        })
795        .collect();
796
797    let checks: Vec<CheckConstraint> = diff
798        .by_kind(EntityKind::CheckConstraint)
799        .into_iter()
800        .filter(|d| d.diff_type == DiffType::Create)
801        .filter_map(|d| d.right.as_ref())
802        .filter_map(|e| match e {
803            crate::sqlite::ddl::SqliteEntity::CheckConstraint(c) if c.table == table.name => {
804                Some(c.clone())
805            }
806            _ => None,
807        })
808        .collect();
809
810    TableFull {
811        name: table.name.to_string(),
812        columns: columns_for_table,
813        pk,
814        fks,
815        uniques,
816        checks,
817        strict: table.strict,
818        without_rowid: table.without_rowid,
819    }
820}
821
822fn append_create_table_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
823    let created_tables = diff.created_tables();
824    let create_result = topological_sort_tables_for_create(&created_tables, diff);
825
826    // If there are circular dependencies, wrap creates with PRAGMA to allow out-of-order creation
827    if create_result.has_circular_deps && !create_result.tables.is_empty() {
828        statements.push("PRAGMA foreign_keys=OFF;".to_string());
829    }
830
831    for entity_diff in &create_result.tables {
832        if let Some(crate::sqlite::ddl::SqliteEntity::Table(table)) = entity_diff.right.as_ref() {
833            let table_full = build_table_full(diff, table);
834            statements.push(convert_create_table(&CreateTableStatement {
835                table: table_full,
836            }));
837        }
838    }
839
840    // Re-enable foreign keys if we disabled them for circular dependencies
841    if create_result.has_circular_deps && !create_result.tables.is_empty() {
842        statements.push("PRAGMA foreign_keys=ON;".to_string());
843    }
844}
845
846fn append_add_column_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
847    for entity_diff in diff.by_kind(EntityKind::Column) {
848        if entity_diff.diff_type == DiffType::Create
849            && let Some(crate::sqlite::ddl::SqliteEntity::Column(col)) = entity_diff.right.as_ref()
850        {
851            let table_was_created = diff.created_tables().iter().any(|t| t.name == col.table);
852
853            if !table_was_created {
854                statements.push(convert_add_column(&AddColumnStatement {
855                    column: col.clone(),
856                    fk: None,
857                }));
858            }
859        }
860    }
861}
862
863fn append_index_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
864    for entity_diff in diff.by_kind(EntityKind::Index) {
865        match entity_diff.diff_type {
866            DiffType::Drop => {
867                if let Some(crate::sqlite::ddl::SqliteEntity::Index(idx)) =
868                    entity_diff.left.as_ref()
869                {
870                    statements.push(convert_drop_index(&DropIndexStatement {
871                        index: idx.clone(),
872                    }));
873                }
874            }
875            DiffType::Create => {
876                if let Some(crate::sqlite::ddl::SqliteEntity::Index(idx)) =
877                    entity_diff.right.as_ref()
878                {
879                    statements.push(convert_create_index(&CreateIndexStatement {
880                        index: idx.clone(),
881                    }));
882                }
883            }
884            DiffType::Alter => {
885                // For index alter: drop old, create new
886                if let (
887                    Some(crate::sqlite::ddl::SqliteEntity::Index(old)),
888                    Some(crate::sqlite::ddl::SqliteEntity::Index(new)),
889                ) = (entity_diff.left.as_ref(), entity_diff.right.as_ref())
890                {
891                    statements.push(convert_drop_index(&DropIndexStatement {
892                        index: old.clone(),
893                    }));
894                    statements.push(convert_create_index(&CreateIndexStatement {
895                        index: new.clone(),
896                    }));
897                }
898            }
899        }
900    }
901}
902
903// =============================================================================
904// SQLite SQL Generator
905// =============================================================================
906
907/// `SQLite` SQL generator for migration diffs
908///
909/// Generates SQL statements from schema diffs with proper ordering:
910/// 1. Table drops (reverse dependency order)
911/// 2. Table creates (dependency order - referenced tables first)
912/// 3. Column additions for existing tables
913/// 4. Index operations
914pub struct Generator {
915    /// Whether to include statement breakpoints
916    pub breakpoints: bool,
917}
918
919impl Default for Generator {
920    fn default() -> Self {
921        Self::new()
922    }
923}
924
925impl Generator {
926    #[must_use]
927    pub const fn new() -> Self {
928        Self { breakpoints: true }
929    }
930
931    #[must_use]
932    pub const fn with_breakpoints(mut self, breakpoints: bool) -> Self {
933        self.breakpoints = breakpoints;
934        self
935    }
936
937    /// Generate SQL from a schema diff
938    #[must_use]
939    pub fn generate_migration(&self, diff: &SchemaDiff) -> Vec<String> {
940        let mut statements = Vec::new();
941
942        append_drop_table_stmts(&mut statements, diff);
943        append_create_table_stmts(&mut statements, diff);
944        append_add_column_stmts(&mut statements, diff);
945        append_index_stmts(&mut statements, diff);
946
947        statements
948    }
949
950    /// Generate SQL from migration statements
951    #[must_use]
952    pub fn statements_to_sql(&self, statements: &[String]) -> String {
953        if self.breakpoints {
954            statements.join(&format!("\n{BREAKPOINT}\n"))
955        } else {
956            statements.join("\n")
957        }
958    }
959}
960
961#[cfg(test)]
962mod tests {
963    use super::*;
964    use crate::sqlite::ddl::{Column, IndexColumn};
965
966    #[test]
967    fn test_create_table_simple() {
968        let table = TableFull {
969            name: "users".to_string(),
970            columns: vec![
971                Column::new("users", "id", "integer").not_null(),
972                Column::new("users", "name", "text").not_null(),
973            ],
974            pk: None,
975            fks: Vec::new(),
976            uniques: Vec::new(),
977            checks: Vec::new(),
978            strict: false,
979            without_rowid: false,
980        };
981
982        let sql = convert_create_table(&CreateTableStatement { table });
983        assert_eq!(
984            sql,
985            "CREATE TABLE `users` (\n\t`id` INTEGER NOT NULL,\n\t`name` TEXT NOT NULL\n);"
986        );
987    }
988
989    #[test]
990    fn test_create_table_with_pk() {
991        let table = TableFull {
992            name: "users".to_string(),
993            columns: vec![Column::new("users", "id", "integer")],
994            pk: Some(PrimaryKey::from_strings(
995                "users".to_string(),
996                "users_pk".to_string(),
997                vec!["id".to_string()],
998            )),
999            fks: Vec::new(),
1000            uniques: Vec::new(),
1001            checks: Vec::new(),
1002            strict: false,
1003            without_rowid: false,
1004        };
1005
1006        let sql = convert_create_table(&CreateTableStatement { table });
1007        assert_eq!(
1008            sql,
1009            "CREATE TABLE `users` (\n\t`id` INTEGER PRIMARY KEY\n);"
1010        );
1011    }
1012
1013    #[test]
1014    fn test_drop_table() {
1015        let sql = convert_drop_table(&DropTableStatement {
1016            table_name: "users".to_string(),
1017        });
1018        assert_eq!(sql, "DROP TABLE `users`;");
1019    }
1020
1021    #[test]
1022    fn test_add_column() {
1023        let sql = convert_add_column(&AddColumnStatement {
1024            column: Column::new("users", "email", "text").not_null(),
1025            fk: None,
1026        });
1027        assert_eq!(sql, "ALTER TABLE `users` ADD `email` TEXT NOT NULL;");
1028    }
1029
1030    #[test]
1031    fn test_create_index() {
1032        let index = Index::new(
1033            "users",
1034            "idx_users_email",
1035            vec![IndexColumn {
1036                value: "email".into(),
1037                is_expression: false,
1038            }],
1039        )
1040        .unique();
1041
1042        let sql = convert_create_index(&CreateIndexStatement { index });
1043        assert_eq!(
1044            sql,
1045            "CREATE UNIQUE INDEX `idx_users_email` ON `users`(`email`);"
1046        );
1047    }
1048
1049    #[test]
1050    fn test_rename_view_returns_two_statements() {
1051        let mut from = View::new("old_view");
1052        from.definition = Some("SELECT 1".into());
1053        let mut to = View::new("new_view");
1054        to.definition = Some("SELECT 1".into());
1055
1056        let statements =
1057            convert_statement(&JsonStatement::RenameView(RenameViewStatement { from, to }));
1058        assert_eq!(
1059            statements,
1060            vec![
1061                "DROP VIEW IF EXISTS `old_view`;".to_string(),
1062                "CREATE VIEW `new_view` AS SELECT 1;".to_string(),
1063            ]
1064        );
1065    }
1066
1067    #[test]
1068    fn test_recreate_warns_on_new_not_null_column_without_default() {
1069        let from = TableFull {
1070            name: "users".to_string(),
1071            columns: vec![Column::new("users", "id", "integer").not_null()],
1072            pk: None,
1073            fks: Vec::new(),
1074            uniques: Vec::new(),
1075            checks: Vec::new(),
1076            strict: false,
1077            without_rowid: false,
1078        };
1079        let to = TableFull {
1080            name: "users".to_string(),
1081            columns: vec![
1082                Column::new("users", "id", "integer").not_null(),
1083                Column::new("users", "email", "text").not_null(),
1084            ],
1085            pk: None,
1086            fks: Vec::new(),
1087            uniques: Vec::new(),
1088            checks: Vec::new(),
1089            strict: false,
1090            without_rowid: false,
1091        };
1092
1093        let statements = convert_recreate_table(&RecreateTableStatement {
1094            from,
1095            to,
1096            data: None,
1097        });
1098        let insert = statements
1099            .iter()
1100            .find(|s| s.contains("INSERT INTO"))
1101            .expect("insert statement");
1102        assert!(
1103            insert.starts_with("-- WARNING:") && insert.contains("email"),
1104            "expected warning comment ahead of the INSERT, got: {insert}"
1105        );
1106    }
1107
1108    #[test]
1109    fn test_create_table_strict() {
1110        let table = TableFull {
1111            name: "data".to_string(),
1112            columns: vec![
1113                Column::new("data", "id", "integer").not_null(),
1114                Column::new("data", "value", "text").not_null(),
1115            ],
1116            pk: None,
1117            fks: Vec::new(),
1118            uniques: Vec::new(),
1119            checks: Vec::new(),
1120            strict: true,
1121            without_rowid: false,
1122        };
1123
1124        let sql = convert_create_table(&CreateTableStatement { table });
1125        assert_eq!(
1126            sql,
1127            "CREATE TABLE `data` (\n\t`id` INTEGER NOT NULL,\n\t`value` TEXT NOT NULL\n) STRICT;"
1128        );
1129    }
1130
1131    #[test]
1132    fn test_create_table_without_rowid() {
1133        let table = TableFull {
1134            name: "kv".to_string(),
1135            columns: vec![
1136                Column::new("kv", "key", "text").not_null(),
1137                Column::new("kv", "value", "blob"),
1138            ],
1139            pk: Some(PrimaryKey::from_strings(
1140                "kv".to_string(),
1141                "kv_pk".to_string(),
1142                vec!["key".to_string()],
1143            )),
1144            fks: Vec::new(),
1145            uniques: Vec::new(),
1146            checks: Vec::new(),
1147            strict: false,
1148            without_rowid: true,
1149        };
1150
1151        let sql = convert_create_table(&CreateTableStatement { table });
1152        assert_eq!(
1153            sql,
1154            "CREATE TABLE `kv` (\n\t`key` TEXT PRIMARY KEY NOT NULL,\n\t`value` BLOB\n) WITHOUT ROWID;"
1155        );
1156    }
1157
1158    #[test]
1159    fn test_create_table_strict_without_rowid() {
1160        let table = TableFull {
1161            name: "cache".to_string(),
1162            columns: vec![
1163                Column::new("cache", "key", "text").not_null(),
1164                Column::new("cache", "data", "blob"),
1165            ],
1166            pk: Some(PrimaryKey::from_strings(
1167                "cache".to_string(),
1168                "cache_pk".to_string(),
1169                vec!["key".to_string()],
1170            )),
1171            fks: Vec::new(),
1172            uniques: Vec::new(),
1173            checks: Vec::new(),
1174            strict: true,
1175            without_rowid: true,
1176        };
1177
1178        let sql = convert_create_table(&CreateTableStatement { table });
1179        assert_eq!(
1180            sql,
1181            "CREATE TABLE `cache` (\n\t`key` TEXT PRIMARY KEY NOT NULL,\n\t`data` BLOB\n) WITHOUT ROWID, STRICT;"
1182        );
1183    }
1184}