Skip to main content

sqlmodel_schema/ddl/
sqlite.rs

1//! SQLite DDL generator.
2//!
3//! SQLite has limited ALTER TABLE support, requiring table recreation for some operations.
4
5use super::{
6    DdlGenerator, generate_add_column, generate_create_index, generate_create_table,
7    generate_drop_index, generate_drop_table, generate_rename_column, generate_rename_table,
8    quote_identifier,
9};
10use crate::diff::SchemaOperation;
11use crate::introspect::{Dialect, ForeignKeyInfo, TableInfo, UniqueConstraintInfo};
12
13/// DDL generator for SQLite.
14pub struct SqliteDdlGenerator;
15
16impl DdlGenerator for SqliteDdlGenerator {
17    fn dialect(&self) -> &'static str {
18        "sqlite"
19    }
20
21    fn generate(&self, op: &SchemaOperation) -> Vec<String> {
22        tracing::debug!(dialect = "sqlite", op = ?op, "Generating DDL");
23
24        let statements = match op {
25            // Tables
26            SchemaOperation::CreateTable(table) => {
27                // For SQLite, implement UNIQUE constraints via named UNIQUE indexes so they can
28                // be dropped later without requiring table recreation.
29                let mut base = table.clone();
30                base.unique_constraints.clear();
31
32                let mut stmts = vec![generate_create_table(&base, Dialect::Sqlite)];
33
34                for uk in &table.unique_constraints {
35                    let cols: Vec<String> = uk
36                        .columns
37                        .iter()
38                        .map(|c| quote_identifier(c, Dialect::Sqlite))
39                        .collect();
40                    let name = uk
41                        .name
42                        .clone()
43                        .unwrap_or_else(|| format!("uk_{}_{}", table.name, uk.columns.join("_")));
44                    stmts.push(format!(
45                        "CREATE UNIQUE INDEX {} ON {}({})",
46                        quote_identifier(&name, Dialect::Sqlite),
47                        quote_identifier(&table.name, Dialect::Sqlite),
48                        cols.join(", ")
49                    ));
50                }
51
52                for idx in &table.indexes {
53                    if idx.primary {
54                        continue;
55                    }
56                    stmts.push(generate_create_index(&table.name, idx, Dialect::Sqlite));
57                }
58
59                stmts
60            }
61            SchemaOperation::DropTable(name) => {
62                vec![generate_drop_table(name, Dialect::Sqlite)]
63            }
64            SchemaOperation::RenameTable { from, to } => {
65                vec![generate_rename_table(from, to, Dialect::Sqlite)]
66            }
67
68            // Columns
69            SchemaOperation::AddColumn { table, column } => {
70                vec![generate_add_column(table, column, Dialect::Sqlite)]
71            }
72            SchemaOperation::DropColumn {
73                table,
74                column,
75                table_info,
76            } => {
77                if let Some(table_info) = table_info {
78                    sqlite_drop_column_recreate(table_info, column)
79                } else {
80                    vec![
81                        "-- SQLite: DROP COLUMN without table_info; using ALTER TABLE DROP COLUMN (requires SQLite >= 3.35.0)".to_string(),
82                        format!(
83                            "ALTER TABLE {} DROP COLUMN {}",
84                            quote_identifier(table, Dialect::Sqlite),
85                            quote_identifier(column, Dialect::Sqlite)
86                        ),
87                    ]
88                }
89            }
90            SchemaOperation::AlterColumnType {
91                table,
92                column,
93                to_type,
94                table_info,
95                ..
96            } => {
97                if let Some(table_info) = table_info {
98                    sqlite_alter_column_type_recreate(table_info, column, to_type)
99                } else {
100                    vec![format!(
101                        "SELECT __sqlmodel_error__('SQLite ALTER COLUMN TYPE requires table_info: {}.{} -> {}')",
102                        sanitize_temp_ident(table),
103                        sanitize_temp_ident(column),
104                        sanitize_temp_ident(to_type)
105                    )]
106                }
107            }
108            SchemaOperation::AlterColumnNullable {
109                table,
110                column,
111                to_nullable,
112                table_info,
113                ..
114            } => {
115                if let Some(table_info) = table_info {
116                    sqlite_alter_column_nullable_recreate(table_info, &column.name, *to_nullable)
117                } else {
118                    vec![format!(
119                        "SELECT __sqlmodel_error__('SQLite ALTER COLUMN NULLABILITY requires table_info: {}.{}')",
120                        sanitize_temp_ident(table),
121                        sanitize_temp_ident(&column.name)
122                    )]
123                }
124            }
125            SchemaOperation::AlterColumnDefault {
126                table,
127                column,
128                to_default,
129                table_info,
130                ..
131            } => {
132                if let Some(table_info) = table_info {
133                    sqlite_alter_column_default_recreate(table_info, column, to_default.as_deref())
134                } else {
135                    vec![format!(
136                        "SELECT __sqlmodel_error__('SQLite ALTER COLUMN DEFAULT requires table_info: {}.{}')",
137                        sanitize_temp_ident(table),
138                        sanitize_temp_ident(column)
139                    )]
140                }
141            }
142            SchemaOperation::RenameColumn { table, from, to } => {
143                vec![generate_rename_column(table, from, to, Dialect::Sqlite)]
144            }
145
146            // Primary Keys
147            SchemaOperation::AddPrimaryKey {
148                table,
149                columns,
150                table_info,
151            } => {
152                if let Some(table_info) = table_info {
153                    sqlite_add_primary_key_recreate(table_info, columns)
154                } else {
155                    vec![format!(
156                        "SELECT __sqlmodel_error__('SQLite ADD PRIMARY KEY requires table_info: {}')",
157                        sanitize_temp_ident(table)
158                    )]
159                }
160            }
161            SchemaOperation::DropPrimaryKey { table, table_info } => {
162                if let Some(table_info) = table_info {
163                    sqlite_drop_primary_key_recreate(table_info)
164                } else {
165                    vec![format!(
166                        "SELECT __sqlmodel_error__('SQLite DROP PRIMARY KEY requires table_info: {}')",
167                        sanitize_temp_ident(table)
168                    )]
169                }
170            }
171
172            // Foreign Keys
173            SchemaOperation::AddForeignKey {
174                table,
175                fk,
176                table_info,
177            } => {
178                if let Some(table_info) = table_info {
179                    sqlite_add_foreign_key_recreate(table_info, fk)
180                } else {
181                    vec![format!(
182                        "SELECT __sqlmodel_error__('SQLite ADD FOREIGN KEY requires table_info: {}.{}')",
183                        sanitize_temp_ident(table),
184                        sanitize_temp_ident(&fk.column)
185                    )]
186                }
187            }
188            SchemaOperation::DropForeignKey {
189                table,
190                name,
191                table_info,
192            } => {
193                if let Some(table_info) = table_info {
194                    sqlite_drop_foreign_key_recreate(table_info, name)
195                } else {
196                    vec![format!(
197                        "SELECT __sqlmodel_error__('SQLite DROP FOREIGN KEY requires table_info: {}.{}')",
198                        sanitize_temp_ident(table),
199                        sanitize_temp_ident(name)
200                    )]
201                }
202            }
203
204            // Unique Constraints
205            SchemaOperation::AddUnique {
206                table, constraint, ..
207            } => {
208                // SQLite: Create a unique index instead
209                let cols: Vec<String> = constraint
210                    .columns
211                    .iter()
212                    .map(|c| quote_identifier(c, Dialect::Sqlite))
213                    .collect();
214                let name = constraint
215                    .name
216                    .clone()
217                    .unwrap_or_else(|| format!("uk_{}_{}", table, constraint.columns.join("_")));
218                vec![format!(
219                    "CREATE UNIQUE INDEX {} ON {}({})",
220                    quote_identifier(&name, Dialect::Sqlite),
221                    quote_identifier(table, Dialect::Sqlite),
222                    cols.join(", ")
223                )]
224            }
225            SchemaOperation::DropUnique {
226                table,
227                name,
228                table_info,
229            } => {
230                // If this is a SQLite autoindex (constraint-backed), DROP INDEX will fail.
231                // In that case we must recreate the table without the unique constraint.
232                if name.starts_with("sqlite_autoindex_") {
233                    if let Some(table_info) = table_info {
234                        sqlite_drop_unique_recreate(table_info, name)
235                    } else {
236                        vec![format!(
237                            "SELECT __sqlmodel_error__('SQLite DROP UNIQUE autoindex requires table_info: {}.{}')",
238                            sanitize_temp_ident(table),
239                            sanitize_temp_ident(name)
240                        )]
241                    }
242                } else {
243                    vec![generate_drop_index(table, name, Dialect::Sqlite)]
244                }
245            }
246
247            // Indexes
248            SchemaOperation::CreateIndex { table, index } => {
249                vec![generate_create_index(table, index, Dialect::Sqlite)]
250            }
251            SchemaOperation::DropIndex { table, name } => {
252                vec![generate_drop_index(table, name, Dialect::Sqlite)]
253            }
254        };
255
256        for stmt in &statements {
257            tracing::trace!(sql = %stmt, "Generated SQLite DDL statement");
258        }
259
260        statements
261    }
262}
263
264fn sanitize_temp_ident(s: &str) -> String {
265    let mut out = String::with_capacity(s.len());
266    for c in s.chars() {
267        if c.is_ascii_alphanumeric() || c == '_' {
268            out.push(c);
269        } else {
270            out.push('_');
271        }
272    }
273    if out.is_empty() {
274        out.push_str("tmp");
275    }
276    out
277}
278
279fn sqlite_recreate_table(
280    new_table: &TableInfo,
281    tmp_old: &str,
282    insert_cols: &[String],
283    select_exprs: &[String],
284) -> Vec<String> {
285    let table_name = new_table.name.as_str();
286
287    // For SQLite we intentionally implement unique constraints via named unique indexes
288    // (not table-level UNIQUE constraints) so we can DROP them later without table recreation.
289    let mut create_table = new_table.clone();
290    create_table.unique_constraints.clear();
291
292    let mut stmts = vec![
293        "PRAGMA foreign_keys=OFF".to_string(),
294        "BEGIN".to_string(),
295        generate_rename_table(table_name, tmp_old, Dialect::Sqlite),
296        super::generate_create_table_with_if_not_exists(&create_table, Dialect::Sqlite, false),
297    ];
298
299    stmts.push(format!(
300        "INSERT INTO {} ({}) SELECT {} FROM {}",
301        quote_identifier(table_name, Dialect::Sqlite),
302        insert_cols.join(", "),
303        select_exprs.join(", "),
304        quote_identifier(tmp_old, Dialect::Sqlite)
305    ));
306
307    stmts.push(generate_drop_table(tmp_old, Dialect::Sqlite));
308
309    for uk in &new_table.unique_constraints {
310        let cols: Vec<String> = uk
311            .columns
312            .iter()
313            .map(|c| quote_identifier(c, Dialect::Sqlite))
314            .collect();
315        let name = uk
316            .name
317            .clone()
318            .unwrap_or_else(|| format!("uk_{}_{}", table_name, uk.columns.join("_")));
319        stmts.push(format!(
320            "CREATE UNIQUE INDEX {} ON {}({})",
321            quote_identifier(&name, Dialect::Sqlite),
322            quote_identifier(table_name, Dialect::Sqlite),
323            cols.join(", ")
324        ));
325    }
326
327    for idx in &new_table.indexes {
328        if idx.primary {
329            continue;
330        }
331        stmts.push(generate_create_index(table_name, idx, Dialect::Sqlite));
332    }
333
334    stmts.push("COMMIT".to_string());
335    stmts.push("PRAGMA foreign_keys=ON".to_string());
336    stmts
337}
338
339fn sqlite_fk_effective_name(table: &str, fk: &ForeignKeyInfo) -> String {
340    fk.name
341        .clone()
342        .unwrap_or_else(|| format!("fk_{}_{}", table, fk.column))
343}
344
345fn sqlite_unique_effective_name(table: &str, uk: &UniqueConstraintInfo) -> String {
346    uk.name
347        .clone()
348        .unwrap_or_else(|| format!("uk_{}_{}", table, uk.columns.join("_")))
349}
350
351fn sqlite_add_primary_key_recreate(table: &TableInfo, pk_columns: &[String]) -> Vec<String> {
352    let table_name = table.name.as_str();
353    let tmp_old = format!("__sqlmodel_old_{}_add_pk", sanitize_temp_ident(table_name));
354
355    let mut new_table = table.clone();
356    new_table.primary_key = pk_columns.to_vec();
357    for col in &mut new_table.columns {
358        col.primary_key = pk_columns.iter().any(|c| c == &col.name);
359    }
360
361    let cols: Vec<String> = new_table
362        .columns
363        .iter()
364        .map(|c| quote_identifier(&c.name, Dialect::Sqlite))
365        .collect();
366
367    sqlite_recreate_table(&new_table, &tmp_old, &cols, &cols)
368}
369
370fn sqlite_drop_primary_key_recreate(table: &TableInfo) -> Vec<String> {
371    let table_name = table.name.as_str();
372    let tmp_old = format!("__sqlmodel_old_{}_drop_pk", sanitize_temp_ident(table_name));
373
374    let mut new_table = table.clone();
375    new_table.primary_key.clear();
376    for col in &mut new_table.columns {
377        col.primary_key = false;
378    }
379
380    let cols: Vec<String> = new_table
381        .columns
382        .iter()
383        .map(|c| quote_identifier(&c.name, Dialect::Sqlite))
384        .collect();
385
386    sqlite_recreate_table(&new_table, &tmp_old, &cols, &cols)
387}
388
389fn sqlite_add_foreign_key_recreate(table: &TableInfo, fk: &ForeignKeyInfo) -> Vec<String> {
390    let table_name = table.name.as_str();
391    let tmp_old = format!(
392        "__sqlmodel_old_{}_add_fk_{}",
393        sanitize_temp_ident(table_name),
394        sanitize_temp_ident(&fk.column)
395    );
396
397    let mut new_table = table.clone();
398    // Keep one FK per local column (SQLite/SQLModel model metadata assumes this).
399    new_table.foreign_keys.retain(|x| x.column != fk.column);
400    new_table.foreign_keys.push(fk.clone());
401
402    let cols: Vec<String> = new_table
403        .columns
404        .iter()
405        .map(|c| quote_identifier(&c.name, Dialect::Sqlite))
406        .collect();
407
408    sqlite_recreate_table(&new_table, &tmp_old, &cols, &cols)
409}
410
411fn sqlite_drop_foreign_key_recreate(table: &TableInfo, name: &str) -> Vec<String> {
412    let table_name = table.name.as_str();
413    let tmp_old = format!(
414        "__sqlmodel_old_{}_drop_fk_{}",
415        sanitize_temp_ident(table_name),
416        sanitize_temp_ident(name)
417    );
418
419    let mut new_table = table.clone();
420    new_table
421        .foreign_keys
422        .retain(|fk| sqlite_fk_effective_name(table_name, fk) != name);
423
424    let cols: Vec<String> = new_table
425        .columns
426        .iter()
427        .map(|c| quote_identifier(&c.name, Dialect::Sqlite))
428        .collect();
429
430    sqlite_recreate_table(&new_table, &tmp_old, &cols, &cols)
431}
432
433fn sqlite_drop_unique_recreate(table: &TableInfo, name: &str) -> Vec<String> {
434    let table_name = table.name.as_str();
435    let tmp_old = format!(
436        "__sqlmodel_old_{}_drop_uk_{}",
437        sanitize_temp_ident(table_name),
438        sanitize_temp_ident(name)
439    );
440
441    let mut new_table = table.clone();
442    new_table
443        .unique_constraints
444        .retain(|uk| sqlite_unique_effective_name(table_name, uk) != name);
445
446    let cols: Vec<String> = new_table
447        .columns
448        .iter()
449        .map(|c| quote_identifier(&c.name, Dialect::Sqlite))
450        .collect();
451
452    sqlite_recreate_table(&new_table, &tmp_old, &cols, &cols)
453}
454
455fn sqlite_drop_column_recreate(table: &TableInfo, drop_column: &str) -> Vec<String> {
456    let table_name = table.name.as_str();
457    let drop_column = drop_column.to_string();
458
459    if !table.columns.iter().any(|c| c.name == drop_column) {
460        return vec![format!(
461            "-- SQLite: column '{}' not found on table '{}' (noop)",
462            drop_column, table_name
463        )];
464    }
465
466    let mut new_table = table.clone();
467    new_table.columns.retain(|c| c.name != drop_column);
468    new_table.primary_key.retain(|c| c != &drop_column);
469    new_table.foreign_keys.retain(|fk| fk.column != drop_column);
470    new_table
471        .unique_constraints
472        .retain(|u| !u.columns.iter().any(|c| c == &drop_column));
473    new_table
474        .indexes
475        .retain(|idx| !idx.columns.iter().any(|c| c == &drop_column));
476
477    if new_table.columns.is_empty() {
478        return vec![format!(
479            "SELECT __sqlmodel_error__('cannot drop last column {}.{}')",
480            sanitize_temp_ident(table_name),
481            sanitize_temp_ident(&drop_column)
482        )];
483    }
484
485    let tmp_old = format!(
486        "__sqlmodel_old_{}_drop_{}",
487        sanitize_temp_ident(table_name),
488        sanitize_temp_ident(&drop_column)
489    );
490
491    let cols: Vec<String> = new_table
492        .columns
493        .iter()
494        .map(|c| quote_identifier(&c.name, Dialect::Sqlite))
495        .collect();
496    sqlite_recreate_table(&new_table, &tmp_old, &cols, &cols)
497}
498
499fn sqlite_alter_column_type_recreate(
500    table: &TableInfo,
501    column: &str,
502    to_type: &str,
503) -> Vec<String> {
504    let table_name = table.name.as_str();
505    let tmp_old = format!(
506        "__sqlmodel_old_{}_type_{}",
507        sanitize_temp_ident(table_name),
508        sanitize_temp_ident(column)
509    );
510
511    let mut new_table = table.clone();
512    for col in &mut new_table.columns {
513        if col.name == column {
514            col.sql_type = to_type.to_string();
515            col.parsed_type = crate::introspect::ParsedSqlType::parse(to_type);
516        }
517    }
518
519    let insert_cols: Vec<String> = new_table
520        .columns
521        .iter()
522        .map(|c| quote_identifier(&c.name, Dialect::Sqlite))
523        .collect();
524
525    let select_exprs: Vec<String> = new_table
526        .columns
527        .iter()
528        .map(|c| {
529            let q = quote_identifier(&c.name, Dialect::Sqlite);
530            if c.name == column {
531                format!("CAST({} AS {})", q, to_type)
532            } else {
533                q
534            }
535        })
536        .collect();
537
538    sqlite_recreate_table(&new_table, &tmp_old, &insert_cols, &select_exprs)
539}
540
541fn sqlite_alter_column_nullable_recreate(
542    table: &TableInfo,
543    column: &str,
544    to_nullable: bool,
545) -> Vec<String> {
546    let table_name = table.name.as_str();
547    let tmp_old = format!(
548        "__sqlmodel_old_{}_nullable_{}",
549        sanitize_temp_ident(table_name),
550        sanitize_temp_ident(column)
551    );
552
553    let mut new_table = table.clone();
554    for col in &mut new_table.columns {
555        if col.name == column {
556            col.nullable = to_nullable;
557        }
558    }
559
560    let cols: Vec<String> = new_table
561        .columns
562        .iter()
563        .map(|c| quote_identifier(&c.name, Dialect::Sqlite))
564        .collect();
565
566    sqlite_recreate_table(&new_table, &tmp_old, &cols, &cols)
567}
568
569fn sqlite_alter_column_default_recreate(
570    table: &TableInfo,
571    column: &str,
572    to_default: Option<&str>,
573) -> Vec<String> {
574    let table_name = table.name.as_str();
575    let tmp_old = format!(
576        "__sqlmodel_old_{}_default_{}",
577        sanitize_temp_ident(table_name),
578        sanitize_temp_ident(column)
579    );
580
581    let mut new_table = table.clone();
582    for col in &mut new_table.columns {
583        if col.name == column {
584            col.default = to_default.map(|s| s.to_string());
585        }
586    }
587
588    let cols: Vec<String> = new_table
589        .columns
590        .iter()
591        .map(|c| quote_identifier(&c.name, Dialect::Sqlite))
592        .collect();
593
594    sqlite_recreate_table(&new_table, &tmp_old, &cols, &cols)
595}
596
597// ============================================================================
598// Unit Tests
599// ============================================================================
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604    use crate::diff::SchemaOperation;
605    use crate::introspect::{
606        ColumnInfo, ForeignKeyInfo, IndexInfo, ParsedSqlType, TableInfo, UniqueConstraintInfo,
607    };
608
609    fn make_column(name: &str, sql_type: &str, nullable: bool) -> ColumnInfo {
610        ColumnInfo {
611            name: name.to_string(),
612            sql_type: sql_type.to_string(),
613            parsed_type: ParsedSqlType::parse(sql_type),
614            nullable,
615            default: None,
616            primary_key: false,
617            auto_increment: false,
618            comment: None,
619        }
620    }
621
622    fn make_table(name: &str, columns: Vec<ColumnInfo>, pk: Vec<&str>) -> TableInfo {
623        TableInfo {
624            name: name.to_string(),
625            columns,
626            primary_key: pk.into_iter().map(String::from).collect(),
627            foreign_keys: Vec::new(),
628            unique_constraints: Vec::new(),
629            check_constraints: Vec::new(),
630            indexes: Vec::new(),
631            comment: None,
632        }
633    }
634
635    #[test]
636    fn test_create_table() {
637        let ddl = SqliteDdlGenerator;
638        let table = make_table(
639            "heroes",
640            vec![
641                make_column("id", "INTEGER", false),
642                make_column("name", "TEXT", false),
643            ],
644            vec!["id"],
645        );
646        let op = SchemaOperation::CreateTable(table);
647        let stmts = ddl.generate(&op);
648
649        assert_eq!(stmts.len(), 1);
650        assert!(stmts[0].contains("CREATE TABLE IF NOT EXISTS"));
651        assert!(stmts[0].contains("\"heroes\""));
652    }
653
654    #[test]
655    fn test_create_table_emits_indexes() {
656        let ddl = SqliteDdlGenerator;
657        let mut table = make_table(
658            "heroes",
659            vec![
660                make_column("id", "INTEGER", false),
661                make_column("name", "TEXT", false),
662            ],
663            vec!["id"],
664        );
665        table.indexes.push(IndexInfo {
666            name: "idx_heroes_name".to_string(),
667            columns: vec!["name".to_string()],
668            unique: false,
669            index_type: None,
670            primary: false,
671        });
672        let op = SchemaOperation::CreateTable(table);
673        let stmts = ddl.generate(&op);
674
675        assert_eq!(stmts.len(), 2);
676        assert!(stmts[0].contains("CREATE TABLE IF NOT EXISTS"));
677        assert!(stmts[1].contains("CREATE INDEX"));
678        assert!(stmts[1].contains("\"idx_heroes_name\""));
679    }
680
681    #[test]
682    fn test_drop_table() {
683        let ddl = SqliteDdlGenerator;
684        let op = SchemaOperation::DropTable("heroes".to_string());
685        let stmts = ddl.generate(&op);
686
687        assert_eq!(stmts.len(), 1);
688        assert_eq!(stmts[0], "DROP TABLE IF EXISTS \"heroes\"");
689    }
690
691    #[test]
692    fn test_rename_table() {
693        let ddl = SqliteDdlGenerator;
694        let op = SchemaOperation::RenameTable {
695            from: "old_heroes".to_string(),
696            to: "heroes".to_string(),
697        };
698        let stmts = ddl.generate(&op);
699
700        assert_eq!(stmts.len(), 1);
701        assert!(stmts[0].contains("ALTER TABLE"));
702        assert!(stmts[0].contains("RENAME TO"));
703    }
704
705    #[test]
706    fn test_add_column() {
707        let ddl = SqliteDdlGenerator;
708        let op = SchemaOperation::AddColumn {
709            table: "heroes".to_string(),
710            column: make_column("age", "INTEGER", true),
711        };
712        let stmts = ddl.generate(&op);
713
714        assert_eq!(stmts.len(), 1);
715        assert!(stmts[0].contains("ALTER TABLE"));
716        assert!(stmts[0].contains("ADD COLUMN"));
717        assert!(stmts[0].contains("\"age\""));
718    }
719
720    #[test]
721    fn test_drop_column() {
722        let ddl = SqliteDdlGenerator;
723        let mut table = make_table(
724            "heroes",
725            vec![
726                make_column("id", "INTEGER", false),
727                make_column("name", "TEXT", false),
728                make_column("old_field", "TEXT", true),
729            ],
730            vec!["id"],
731        );
732        table.indexes = vec![
733            IndexInfo {
734                name: "idx_name".to_string(),
735                columns: vec!["name".to_string()],
736                unique: false,
737                index_type: None,
738                primary: false,
739            },
740            IndexInfo {
741                name: "idx_old_field".to_string(),
742                columns: vec!["old_field".to_string()],
743                unique: false,
744                index_type: None,
745                primary: false,
746            },
747        ];
748        let op = SchemaOperation::DropColumn {
749            table: "heroes".to_string(),
750            column: "old_field".to_string(),
751            table_info: Some(table),
752        };
753        let stmts = ddl.generate(&op);
754
755        // Table recreation path emits multiple statements.
756        assert!(stmts.len() >= 6);
757        assert!(
758            stmts
759                .iter()
760                .any(|s| s.contains("ALTER TABLE") && s.contains("RENAME TO"))
761        );
762        assert!(
763            stmts
764                .iter()
765                .any(|s| s.contains("CREATE TABLE") && s.contains("\"heroes\""))
766        );
767        assert!(
768            stmts
769                .iter()
770                .any(|s| s.contains("INSERT INTO") && s.contains("SELECT"))
771        );
772        // Index on dropped column should be omitted; remaining index should be recreated.
773        assert!(
774            stmts
775                .iter()
776                .any(|s| s.contains("CREATE INDEX") && s.contains("idx_name"))
777        );
778        assert!(
779            !stmts
780                .iter()
781                .any(|s| s.contains("CREATE INDEX") && s.contains("idx_old_field"))
782        );
783    }
784
785    #[test]
786    fn test_alter_column_type_via_recreate() {
787        let ddl = SqliteDdlGenerator;
788        let table = make_table(
789            "heroes",
790            vec![
791                make_column("id", "INTEGER", false),
792                make_column("age", "INTEGER", false),
793            ],
794            vec!["id"],
795        );
796        let op = SchemaOperation::AlterColumnType {
797            table: "heroes".to_string(),
798            column: "age".to_string(),
799            from_type: "INTEGER".to_string(),
800            to_type: "TEXT".to_string(),
801            table_info: Some(table),
802        };
803        let stmts = ddl.generate(&op);
804
805        assert!(stmts.iter().any(|s| s.contains("CREATE TABLE \"heroes\"")));
806        assert!(!stmts.iter().any(|s| s.contains("IF NOT EXISTS")));
807        assert!(
808            stmts
809                .iter()
810                .any(|s| s.contains("INSERT INTO") && s.contains("CAST"))
811        );
812    }
813
814    #[test]
815    fn test_rename_column() {
816        let ddl = SqliteDdlGenerator;
817        let op = SchemaOperation::RenameColumn {
818            table: "heroes".to_string(),
819            from: "old_name".to_string(),
820            to: "name".to_string(),
821        };
822        let stmts = ddl.generate(&op);
823
824        assert_eq!(stmts.len(), 1);
825        assert!(stmts[0].contains("RENAME COLUMN"));
826    }
827
828    #[test]
829    fn test_create_index() {
830        let ddl = SqliteDdlGenerator;
831        let op = SchemaOperation::CreateIndex {
832            table: "heroes".to_string(),
833            index: IndexInfo {
834                name: "idx_heroes_name".to_string(),
835                columns: vec!["name".to_string()],
836                unique: false,
837                index_type: None,
838                primary: false,
839            },
840        };
841        let stmts = ddl.generate(&op);
842
843        assert_eq!(stmts.len(), 1);
844        assert!(stmts[0].contains("CREATE INDEX"));
845        assert!(stmts[0].contains("\"idx_heroes_name\""));
846    }
847
848    #[test]
849    fn test_create_unique_index() {
850        let ddl = SqliteDdlGenerator;
851        let op = SchemaOperation::CreateIndex {
852            table: "heroes".to_string(),
853            index: IndexInfo {
854                name: "idx_heroes_name_unique".to_string(),
855                columns: vec!["name".to_string()],
856                unique: true,
857                index_type: None,
858                primary: false,
859            },
860        };
861        let stmts = ddl.generate(&op);
862
863        assert_eq!(stmts.len(), 1);
864        assert!(stmts[0].contains("CREATE UNIQUE INDEX"));
865    }
866
867    #[test]
868    fn test_drop_index() {
869        let ddl = SqliteDdlGenerator;
870        let op = SchemaOperation::DropIndex {
871            table: "heroes".to_string(),
872            name: "idx_heroes_name".to_string(),
873        };
874        let stmts = ddl.generate(&op);
875
876        assert_eq!(stmts.len(), 1);
877        assert!(stmts[0].contains("DROP INDEX IF EXISTS"));
878    }
879
880    #[test]
881    fn test_add_unique_creates_index() {
882        let ddl = SqliteDdlGenerator;
883        let op = SchemaOperation::AddUnique {
884            table: "heroes".to_string(),
885            constraint: UniqueConstraintInfo {
886                name: Some("uk_heroes_name".to_string()),
887                columns: vec!["name".to_string()],
888            },
889            table_info: None,
890        };
891        let stmts = ddl.generate(&op);
892
893        assert_eq!(stmts.len(), 1);
894        assert!(stmts[0].contains("CREATE UNIQUE INDEX"));
895    }
896
897    #[test]
898    fn test_add_fk_requires_table_info() {
899        let ddl = SqliteDdlGenerator;
900        let op = SchemaOperation::AddForeignKey {
901            table: "heroes".to_string(),
902            fk: ForeignKeyInfo {
903                name: Some("fk_heroes_team".to_string()),
904                column: "team_id".to_string(),
905                foreign_table: "teams".to_string(),
906                foreign_column: "id".to_string(),
907                on_delete: None,
908                on_update: None,
909            },
910            table_info: None,
911        };
912        let stmts = ddl.generate(&op);
913
914        assert_eq!(stmts.len(), 1);
915        assert!(stmts[0].contains("__sqlmodel_error__"));
916        assert!(stmts[0].contains("requires table_info"));
917    }
918
919    #[test]
920    fn test_dialect() {
921        let ddl = SqliteDdlGenerator;
922        assert_eq!(ddl.dialect(), "sqlite");
923    }
924
925    #[test]
926    fn test_generate_all() {
927        let ddl = SqliteDdlGenerator;
928        let ops = vec![
929            SchemaOperation::CreateTable(make_table(
930                "heroes",
931                vec![make_column("id", "INTEGER", false)],
932                vec!["id"],
933            )),
934            SchemaOperation::CreateIndex {
935                table: "heroes".to_string(),
936                index: IndexInfo {
937                    name: "idx_heroes_name".to_string(),
938                    columns: vec!["name".to_string()],
939                    unique: false,
940                    index_type: None,
941                    primary: false,
942                },
943            },
944        ];
945
946        let stmts = ddl.generate_all(&ops);
947        assert_eq!(stmts.len(), 2);
948    }
949
950    #[test]
951    fn test_generate_rollback() {
952        let ddl = SqliteDdlGenerator;
953        let ops = vec![
954            SchemaOperation::CreateTable(make_table(
955                "heroes",
956                vec![make_column("id", "INTEGER", false)],
957                vec!["id"],
958            )),
959            SchemaOperation::AddColumn {
960                table: "heroes".to_string(),
961                column: make_column("name", "TEXT", false),
962            },
963        ];
964
965        let rollback = ddl.generate_rollback(&ops);
966        // Should have DROP COLUMN first (reverse of AddColumn), then DROP TABLE.
967        // For rollback-generated DropColumn we don't have table_info, so SQLite emits a comment + ALTER.
968        assert_eq!(rollback.len(), 3);
969        assert!(rollback[0].contains("DROP COLUMN") || rollback[1].contains("DROP COLUMN"));
970        assert!(rollback.iter().any(|s| s.contains("DROP TABLE")));
971    }
972}