Skip to main content

oxide_sql_core/migrations/dialect/
sqlite.rs

1//! SQLite dialect for migrations.
2
3use super::MigrationDialect;
4use crate::ast::DataType;
5use crate::migrations::operation::{
6    AlterColumnChange, AlterColumnOp, DropIndexOp, RenameColumnOp, RenameTableOp,
7};
8use crate::schema::RustTypeMapping;
9
10/// SQLite dialect for migration SQL generation.
11#[derive(Debug, Clone, Copy, Default)]
12pub struct SqliteDialect;
13
14impl SqliteDialect {
15    /// Creates a new SQLite dialect.
16    #[must_use]
17    pub const fn new() -> Self {
18        Self
19    }
20}
21
22impl MigrationDialect for SqliteDialect {
23    fn name(&self) -> &'static str {
24        "sqlite"
25    }
26
27    fn map_data_type(&self, dt: &DataType) -> String {
28        // SQLite has dynamic typing with type affinity
29        match dt {
30            DataType::Smallint | DataType::Integer | DataType::Bigint => "INTEGER".to_string(),
31            DataType::Real | DataType::Double => "REAL".to_string(),
32            DataType::Decimal { .. } | DataType::Numeric { .. } => "REAL".to_string(),
33            DataType::Char(_) | DataType::Varchar(_) | DataType::Text => "TEXT".to_string(),
34            DataType::Blob | DataType::Binary(_) | DataType::Varbinary(_) => "BLOB".to_string(),
35            DataType::Date | DataType::Time | DataType::Timestamp | DataType::Datetime => {
36                "TEXT".to_string()
37            }
38            DataType::Boolean => "INTEGER".to_string(), // SQLite has no bool, use 0/1
39            DataType::Custom(name) => name.clone(),
40        }
41    }
42
43    fn autoincrement_keyword(&self) -> String {
44        " AUTOINCREMENT".to_string()
45    }
46
47    fn rename_table(&self, op: &RenameTableOp) -> String {
48        format!(
49            "ALTER TABLE {} RENAME TO {}",
50            self.quote_identifier(&op.old_name),
51            self.quote_identifier(&op.new_name)
52        )
53    }
54
55    fn rename_column(&self, op: &RenameColumnOp) -> String {
56        // SQLite 3.25.0+ supports RENAME COLUMN
57        format!(
58            "ALTER TABLE {} RENAME COLUMN {} TO {}",
59            self.quote_identifier(&op.table),
60            self.quote_identifier(&op.old_name),
61            self.quote_identifier(&op.new_name)
62        )
63    }
64
65    fn alter_column(&self, op: &AlterColumnOp) -> String {
66        // SQLite has very limited ALTER TABLE support.
67        // Most column alterations require recreating the table.
68        // We generate a comment noting this limitation.
69        match &op.change {
70            AlterColumnChange::SetDataType(_) => {
71                format!(
72                    "-- SQLite does not support ALTER COLUMN TYPE directly for {}.{}; \
73                     table recreation required",
74                    op.table, op.column
75                )
76            }
77            AlterColumnChange::SetNullable(_) => {
78                format!(
79                    "-- SQLite does not support ALTER COLUMN NULL/NOT NULL directly for {}.{}; \
80                     table recreation required",
81                    op.table, op.column
82                )
83            }
84            AlterColumnChange::SetDefault(default) => {
85                // SQLite doesn't support ALTER COLUMN SET DEFAULT either
86                format!(
87                    "-- SQLite does not support ALTER COLUMN SET DEFAULT directly for {}.{}; \
88                     would set to: {}",
89                    op.table,
90                    op.column,
91                    self.render_default(default)
92                )
93            }
94            AlterColumnChange::DropDefault => {
95                format!(
96                    "-- SQLite does not support ALTER COLUMN DROP DEFAULT directly for {}.{}; \
97                     table recreation required",
98                    op.table, op.column
99                )
100            }
101            AlterColumnChange::SetUnique(_) => {
102                format!(
103                    "-- SQLite does not support ALTER COLUMN UNIQUE directly for {}.{}; \
104                     table recreation required",
105                    op.table, op.column
106                )
107            }
108            AlterColumnChange::SetAutoincrement(_) => {
109                format!(
110                    "-- SQLite does not support ALTER autoincrement for {}.{}; \
111                     table recreation required",
112                    op.table, op.column
113                )
114            }
115        }
116    }
117
118    fn drop_index(&self, op: &DropIndexOp) -> String {
119        let mut sql = String::from("DROP INDEX ");
120        if op.if_exists {
121            sql.push_str("IF EXISTS ");
122        }
123        // SQLite index names are global, not per-table
124        sql.push_str(&self.quote_identifier(&op.name));
125        sql
126    }
127
128    fn drop_foreign_key(&self, op: &super::super::operation::DropForeignKeyOp) -> String {
129        // SQLite does not support DROP CONSTRAINT; requires table recreation
130        format!(
131            "-- SQLite does not support DROP CONSTRAINT; \
132             table recreation required to remove foreign key {} from {}",
133            op.name, op.table
134        )
135    }
136}
137
138impl RustTypeMapping for SqliteDialect {
139    fn map_type(&self, rust_type: &str) -> DataType {
140        match rust_type {
141            "bool" => DataType::Integer,
142            "i8" | "i16" | "u8" | "u16" | "i32" | "u32" => DataType::Integer,
143            "i64" | "u64" | "i128" | "u128" | "isize" | "usize" => DataType::Bigint,
144            "f32" => DataType::Real,
145            "f64" => DataType::Double,
146            "String" => DataType::Text,
147            "Vec<u8>" => DataType::Blob,
148            s if s.contains("DateTime") => DataType::Text,
149            s if s.contains("NaiveDate") => DataType::Text,
150            _ => DataType::Text, // safe fallback for SQLite
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::migrations::column_builder::{bigint, boolean, timestamp, varchar};
159    use crate::migrations::operation::{DropTableOp, Operation};
160    use crate::migrations::table_builder::CreateTableBuilder;
161
162    #[test]
163    fn test_sqlite_data_types() {
164        let dialect = SqliteDialect::new();
165        assert_eq!(dialect.map_data_type(&DataType::Integer), "INTEGER");
166        assert_eq!(dialect.map_data_type(&DataType::Bigint), "INTEGER");
167        assert_eq!(dialect.map_data_type(&DataType::Text), "TEXT");
168        assert_eq!(dialect.map_data_type(&DataType::Varchar(Some(255))), "TEXT");
169        assert_eq!(dialect.map_data_type(&DataType::Blob), "BLOB");
170        assert_eq!(dialect.map_data_type(&DataType::Boolean), "INTEGER");
171        assert_eq!(dialect.map_data_type(&DataType::Timestamp), "TEXT");
172    }
173
174    #[test]
175    fn test_create_table_sql() {
176        let dialect = SqliteDialect::new();
177        let op = CreateTableBuilder::new()
178            .name("users")
179            .column(bigint("id").primary_key().autoincrement().build())
180            .column(varchar("username", 255).not_null().unique().build())
181            .column(varchar("email", 255).build())
182            .column(
183                timestamp("created_at")
184                    .not_null()
185                    .default_expr("CURRENT_TIMESTAMP")
186                    .build(),
187            )
188            .build();
189
190        let sql = dialect.create_table(&op);
191        assert!(sql.contains("CREATE TABLE \"users\""));
192        assert!(sql.contains("\"id\" INTEGER PRIMARY KEY AUTOINCREMENT"));
193        assert!(sql.contains("\"username\" TEXT NOT NULL UNIQUE"));
194        assert!(sql.contains("DEFAULT CURRENT_TIMESTAMP"));
195    }
196
197    #[test]
198    fn test_drop_table_sql() {
199        let dialect = SqliteDialect::new();
200
201        let op = DropTableOp {
202            name: "users".to_string(),
203            if_exists: false,
204            cascade: false,
205        };
206        assert_eq!(dialect.drop_table(&op), "DROP TABLE \"users\"");
207
208        let op = DropTableOp {
209            name: "users".to_string(),
210            if_exists: true,
211            cascade: false,
212        };
213        assert_eq!(dialect.drop_table(&op), "DROP TABLE IF EXISTS \"users\"");
214    }
215
216    #[test]
217    fn test_add_column_sql() {
218        let dialect = SqliteDialect::new();
219        let op = Operation::add_column(
220            "users",
221            boolean("active").not_null().default_bool(true).build(),
222        );
223
224        if let Operation::AddColumn(add_op) = op {
225            let sql = dialect.add_column(&add_op);
226            assert!(sql.contains("ALTER TABLE \"users\" ADD COLUMN"));
227            assert!(sql.contains("\"active\" INTEGER NOT NULL DEFAULT TRUE"));
228        }
229    }
230
231    #[test]
232    fn test_rename_table_sql() {
233        let dialect = SqliteDialect::new();
234        let op = RenameTableOp {
235            old_name: "old_users".to_string(),
236            new_name: "users".to_string(),
237        };
238        assert_eq!(
239            dialect.rename_table(&op),
240            "ALTER TABLE \"old_users\" RENAME TO \"users\""
241        );
242    }
243
244    #[test]
245    fn test_rename_column_sql() {
246        let dialect = SqliteDialect::new();
247        let op = RenameColumnOp {
248            table: "users".to_string(),
249            old_name: "name".to_string(),
250            new_name: "full_name".to_string(),
251        };
252        assert_eq!(
253            dialect.rename_column(&op),
254            "ALTER TABLE \"users\" RENAME COLUMN \"name\" TO \"full_name\""
255        );
256    }
257}