1use 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
13pub 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 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 SchemaOperation::AddColumn { table, column } => {
38 vec![generate_add_column(table, column, Dialect::Sqlite)]
39 }
40 SchemaOperation::DropColumn { table, column } => {
41 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 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 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 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 SchemaOperation::AddPrimaryKey { table, columns } => {
118 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 SchemaOperation::AddForeignKey { table, fk } => {
145 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 SchemaOperation::AddUnique { table, constraint } => {
172 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 vec![generate_drop_index(table, name, Dialect::Sqlite)]
192 }
193
194 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#[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("--")); 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("--")); 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 assert_eq!(rollback.len(), 2);
489 assert!(rollback[0].contains("DROP COLUMN"));
490 assert!(rollback[1].contains("DROP TABLE"));
491 }
492}