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;
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                vec![generate_create_table(table, Dialect::Sqlite)]
28            }
29            SchemaOperation::DropTable(name) => {
30                vec![generate_drop_table(name, Dialect::Sqlite)]
31            }
32            SchemaOperation::RenameTable { from, to } => {
33                vec![generate_rename_table(from, to, Dialect::Sqlite)]
34            }
35
36            // Columns
37            SchemaOperation::AddColumn { table, column } => {
38                vec![generate_add_column(table, column, Dialect::Sqlite)]
39            }
40            SchemaOperation::DropColumn { table, column } => {
41                // SQLite 3.35.0+ supports DROP COLUMN directly
42                // For older versions, table recreation would be needed
43                vec![format!(
44                    "ALTER TABLE {} DROP COLUMN {}",
45                    quote_identifier(table, Dialect::Sqlite),
46                    quote_identifier(column, Dialect::Sqlite)
47                )]
48            }
49            SchemaOperation::AlterColumnType {
50                table,
51                column,
52                to_type,
53                ..
54            } => {
55                // SQLite doesn't support ALTER COLUMN TYPE
56                // This requires table recreation
57                tracing::warn!(
58                    table = %table,
59                    column = %column,
60                    to_type = %to_type,
61                    "SQLite does not support ALTER COLUMN TYPE - requires table recreation"
62                );
63                vec![format!(
64                    "-- SQLite: Cannot change column type directly. Requires table recreation.\n\
65                     -- Changing {}.{} to type {}",
66                    table, column, to_type
67                )]
68            }
69            SchemaOperation::AlterColumnNullable {
70                table,
71                column,
72                to_nullable,
73                ..
74            } => {
75                // SQLite doesn't support altering nullability
76                tracing::warn!(
77                    table = %table,
78                    column = %column,
79                    to_nullable = %to_nullable,
80                    "SQLite does not support ALTER COLUMN nullability - requires table recreation"
81                );
82                let action = if *to_nullable {
83                    "allow NULL"
84                } else {
85                    "NOT NULL"
86                };
87                vec![format!(
88                    "-- SQLite: Cannot change column nullability directly. Requires table recreation.\n\
89                     -- Setting {}.{} to {}",
90                    table, column, action
91                )]
92            }
93            SchemaOperation::AlterColumnDefault {
94                table,
95                column,
96                to_default,
97                ..
98            } => {
99                // SQLite doesn't support altering defaults
100                tracing::warn!(
101                    table = %table,
102                    column = %column,
103                    "SQLite does not support ALTER COLUMN DEFAULT - requires table recreation"
104                );
105                let default_str = to_default.as_deref().unwrap_or("NULL");
106                vec![format!(
107                    "-- SQLite: Cannot change column default directly. Requires table recreation.\n\
108                     -- Setting {}.{} DEFAULT to {}",
109                    table, column, default_str
110                )]
111            }
112            SchemaOperation::RenameColumn { table, from, to } => {
113                vec![generate_rename_column(table, from, to, Dialect::Sqlite)]
114            }
115
116            // Primary Keys
117            SchemaOperation::AddPrimaryKey { table, columns } => {
118                // SQLite doesn't support adding PK to existing table
119                tracing::warn!(
120                    table = %table,
121                    columns = ?columns,
122                    "SQLite does not support adding PRIMARY KEY to existing table"
123                );
124                vec![format!(
125                    "-- SQLite: Cannot add PRIMARY KEY to existing table. Requires table recreation.\n\
126                     -- Table: {}, Columns: {}",
127                    table,
128                    columns.join(", ")
129                )]
130            }
131            SchemaOperation::DropPrimaryKey { table } => {
132                tracing::warn!(
133                    table = %table,
134                    "SQLite does not support dropping PRIMARY KEY"
135                );
136                vec![format!(
137                    "-- SQLite: Cannot drop PRIMARY KEY. Requires table recreation.\n\
138                     -- Table: {}",
139                    table
140                )]
141            }
142
143            // Foreign Keys
144            SchemaOperation::AddForeignKey { table, fk } => {
145                // SQLite doesn't support adding FK to existing table
146                tracing::warn!(
147                    table = %table,
148                    column = %fk.column,
149                    "SQLite does not support adding FOREIGN KEY to existing table"
150                );
151                vec![format!(
152                    "-- SQLite: Cannot add FOREIGN KEY to existing table. Requires table recreation.\n\
153                     -- Table: {}, Column: {} -> {}.{}",
154                    table, fk.column, fk.foreign_table, fk.foreign_column
155                )]
156            }
157            SchemaOperation::DropForeignKey { table, name } => {
158                tracing::warn!(
159                    table = %table,
160                    name = %name,
161                    "SQLite does not support dropping FOREIGN KEY"
162                );
163                vec![format!(
164                    "-- SQLite: Cannot drop FOREIGN KEY. Requires table recreation.\n\
165                     -- Table: {}, Constraint: {}",
166                    table, name
167                )]
168            }
169
170            // Unique Constraints
171            SchemaOperation::AddUnique { table, constraint } => {
172                // SQLite: Create a unique index instead
173                let cols: Vec<String> = constraint
174                    .columns
175                    .iter()
176                    .map(|c| quote_identifier(c, Dialect::Sqlite))
177                    .collect();
178                let name = constraint
179                    .name
180                    .clone()
181                    .unwrap_or_else(|| format!("uk_{}_{}", table, constraint.columns.join("_")));
182                vec![format!(
183                    "CREATE UNIQUE INDEX {} ON {}({})",
184                    quote_identifier(&name, Dialect::Sqlite),
185                    quote_identifier(table, Dialect::Sqlite),
186                    cols.join(", ")
187                )]
188            }
189            SchemaOperation::DropUnique { table, name } => {
190                // Drop the unique index
191                vec![generate_drop_index(table, name, Dialect::Sqlite)]
192            }
193
194            // Indexes
195            SchemaOperation::CreateIndex { table, index } => {
196                vec![generate_create_index(table, index, Dialect::Sqlite)]
197            }
198            SchemaOperation::DropIndex { table, name } => {
199                vec![generate_drop_index(table, name, Dialect::Sqlite)]
200            }
201        };
202
203        for stmt in &statements {
204            tracing::trace!(sql = %stmt, "Generated SQLite DDL statement");
205        }
206
207        statements
208    }
209}
210
211// ============================================================================
212// Unit Tests
213// ============================================================================
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::diff::SchemaOperation;
219    use crate::introspect::{
220        ColumnInfo, ForeignKeyInfo, IndexInfo, ParsedSqlType, TableInfo, UniqueConstraintInfo,
221    };
222
223    fn make_column(name: &str, sql_type: &str, nullable: bool) -> ColumnInfo {
224        ColumnInfo {
225            name: name.to_string(),
226            sql_type: sql_type.to_string(),
227            parsed_type: ParsedSqlType::parse(sql_type),
228            nullable,
229            default: None,
230            primary_key: false,
231            auto_increment: false,
232            comment: None,
233        }
234    }
235
236    fn make_table(name: &str, columns: Vec<ColumnInfo>, pk: Vec<&str>) -> TableInfo {
237        TableInfo {
238            name: name.to_string(),
239            columns,
240            primary_key: pk.into_iter().map(String::from).collect(),
241            foreign_keys: Vec::new(),
242            unique_constraints: Vec::new(),
243            check_constraints: Vec::new(),
244            indexes: Vec::new(),
245            comment: None,
246        }
247    }
248
249    #[test]
250    fn test_create_table() {
251        let ddl = SqliteDdlGenerator;
252        let table = make_table(
253            "heroes",
254            vec![
255                make_column("id", "INTEGER", false),
256                make_column("name", "TEXT", false),
257            ],
258            vec!["id"],
259        );
260        let op = SchemaOperation::CreateTable(table);
261        let stmts = ddl.generate(&op);
262
263        assert_eq!(stmts.len(), 1);
264        assert!(stmts[0].contains("CREATE TABLE IF NOT EXISTS"));
265        assert!(stmts[0].contains("\"heroes\""));
266    }
267
268    #[test]
269    fn test_drop_table() {
270        let ddl = SqliteDdlGenerator;
271        let op = SchemaOperation::DropTable("heroes".to_string());
272        let stmts = ddl.generate(&op);
273
274        assert_eq!(stmts.len(), 1);
275        assert_eq!(stmts[0], "DROP TABLE IF EXISTS \"heroes\"");
276    }
277
278    #[test]
279    fn test_rename_table() {
280        let ddl = SqliteDdlGenerator;
281        let op = SchemaOperation::RenameTable {
282            from: "old_heroes".to_string(),
283            to: "heroes".to_string(),
284        };
285        let stmts = ddl.generate(&op);
286
287        assert_eq!(stmts.len(), 1);
288        assert!(stmts[0].contains("ALTER TABLE"));
289        assert!(stmts[0].contains("RENAME TO"));
290    }
291
292    #[test]
293    fn test_add_column() {
294        let ddl = SqliteDdlGenerator;
295        let op = SchemaOperation::AddColumn {
296            table: "heroes".to_string(),
297            column: make_column("age", "INTEGER", true),
298        };
299        let stmts = ddl.generate(&op);
300
301        assert_eq!(stmts.len(), 1);
302        assert!(stmts[0].contains("ALTER TABLE"));
303        assert!(stmts[0].contains("ADD COLUMN"));
304        assert!(stmts[0].contains("\"age\""));
305    }
306
307    #[test]
308    fn test_drop_column() {
309        let ddl = SqliteDdlGenerator;
310        let op = SchemaOperation::DropColumn {
311            table: "heroes".to_string(),
312            column: "old_field".to_string(),
313        };
314        let stmts = ddl.generate(&op);
315
316        assert_eq!(stmts.len(), 1);
317        assert!(stmts[0].contains("ALTER TABLE"));
318        assert!(stmts[0].contains("DROP COLUMN"));
319    }
320
321    #[test]
322    fn test_alter_column_type_unsupported() {
323        let ddl = SqliteDdlGenerator;
324        let op = SchemaOperation::AlterColumnType {
325            table: "heroes".to_string(),
326            column: "age".to_string(),
327            from_type: "INTEGER".to_string(),
328            to_type: "TEXT".to_string(),
329        };
330        let stmts = ddl.generate(&op);
331
332        assert_eq!(stmts.len(), 1);
333        assert!(stmts[0].contains("--")); // Comment indicating unsupported
334        assert!(stmts[0].contains("table recreation"));
335    }
336
337    #[test]
338    fn test_rename_column() {
339        let ddl = SqliteDdlGenerator;
340        let op = SchemaOperation::RenameColumn {
341            table: "heroes".to_string(),
342            from: "old_name".to_string(),
343            to: "name".to_string(),
344        };
345        let stmts = ddl.generate(&op);
346
347        assert_eq!(stmts.len(), 1);
348        assert!(stmts[0].contains("RENAME COLUMN"));
349    }
350
351    #[test]
352    fn test_create_index() {
353        let ddl = SqliteDdlGenerator;
354        let op = SchemaOperation::CreateIndex {
355            table: "heroes".to_string(),
356            index: IndexInfo {
357                name: "idx_heroes_name".to_string(),
358                columns: vec!["name".to_string()],
359                unique: false,
360                index_type: None,
361                primary: false,
362            },
363        };
364        let stmts = ddl.generate(&op);
365
366        assert_eq!(stmts.len(), 1);
367        assert!(stmts[0].contains("CREATE INDEX"));
368        assert!(stmts[0].contains("\"idx_heroes_name\""));
369    }
370
371    #[test]
372    fn test_create_unique_index() {
373        let ddl = SqliteDdlGenerator;
374        let op = SchemaOperation::CreateIndex {
375            table: "heroes".to_string(),
376            index: IndexInfo {
377                name: "idx_heroes_name_unique".to_string(),
378                columns: vec!["name".to_string()],
379                unique: true,
380                index_type: None,
381                primary: false,
382            },
383        };
384        let stmts = ddl.generate(&op);
385
386        assert_eq!(stmts.len(), 1);
387        assert!(stmts[0].contains("CREATE UNIQUE INDEX"));
388    }
389
390    #[test]
391    fn test_drop_index() {
392        let ddl = SqliteDdlGenerator;
393        let op = SchemaOperation::DropIndex {
394            table: "heroes".to_string(),
395            name: "idx_heroes_name".to_string(),
396        };
397        let stmts = ddl.generate(&op);
398
399        assert_eq!(stmts.len(), 1);
400        assert!(stmts[0].contains("DROP INDEX IF EXISTS"));
401    }
402
403    #[test]
404    fn test_add_unique_creates_index() {
405        let ddl = SqliteDdlGenerator;
406        let op = SchemaOperation::AddUnique {
407            table: "heroes".to_string(),
408            constraint: UniqueConstraintInfo {
409                name: Some("uk_heroes_name".to_string()),
410                columns: vec!["name".to_string()],
411            },
412        };
413        let stmts = ddl.generate(&op);
414
415        assert_eq!(stmts.len(), 1);
416        assert!(stmts[0].contains("CREATE UNIQUE INDEX"));
417    }
418
419    #[test]
420    fn test_add_fk_unsupported() {
421        let ddl = SqliteDdlGenerator;
422        let op = SchemaOperation::AddForeignKey {
423            table: "heroes".to_string(),
424            fk: ForeignKeyInfo {
425                name: Some("fk_heroes_team".to_string()),
426                column: "team_id".to_string(),
427                foreign_table: "teams".to_string(),
428                foreign_column: "id".to_string(),
429                on_delete: None,
430                on_update: None,
431            },
432        };
433        let stmts = ddl.generate(&op);
434
435        assert_eq!(stmts.len(), 1);
436        assert!(stmts[0].contains("--")); // Comment
437        assert!(stmts[0].contains("table recreation"));
438    }
439
440    #[test]
441    fn test_dialect() {
442        let ddl = SqliteDdlGenerator;
443        assert_eq!(ddl.dialect(), "sqlite");
444    }
445
446    #[test]
447    fn test_generate_all() {
448        let ddl = SqliteDdlGenerator;
449        let ops = vec![
450            SchemaOperation::CreateTable(make_table(
451                "heroes",
452                vec![make_column("id", "INTEGER", false)],
453                vec!["id"],
454            )),
455            SchemaOperation::CreateIndex {
456                table: "heroes".to_string(),
457                index: IndexInfo {
458                    name: "idx_heroes_name".to_string(),
459                    columns: vec!["name".to_string()],
460                    unique: false,
461                    index_type: None,
462                    primary: false,
463                },
464            },
465        ];
466
467        let stmts = ddl.generate_all(&ops);
468        assert_eq!(stmts.len(), 2);
469    }
470
471    #[test]
472    fn test_generate_rollback() {
473        let ddl = SqliteDdlGenerator;
474        let ops = vec![
475            SchemaOperation::CreateTable(make_table(
476                "heroes",
477                vec![make_column("id", "INTEGER", false)],
478                vec!["id"],
479            )),
480            SchemaOperation::AddColumn {
481                table: "heroes".to_string(),
482                column: make_column("name", "TEXT", false),
483            },
484        ];
485
486        let rollback = ddl.generate_rollback(&ops);
487        // Should have DROP COLUMN first (reverse of AddColumn), then DROP TABLE
488        assert_eq!(rollback.len(), 2);
489        assert!(rollback[0].contains("DROP COLUMN"));
490        assert!(rollback[1].contains("DROP TABLE"));
491    }
492}