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, ForeignKeyInfo, TableInfo, UniqueConstraintInfo};
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 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 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 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 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 SchemaOperation::AddUnique {
206 table, constraint, ..
207 } => {
208 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 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 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 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 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#[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 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 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 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}