1use crate::sqlite::ddl::{
8 CheckConstraint, Column, ForeignKey, Index, PrimaryKey, Table, TableSql, UniqueConstraint, View,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13pub const BREAKPOINT: &str = "--> statement-breakpoint";
15
16fn quote_ident(ident: &str) -> String {
17 format!("`{}`", ident.replace('`', "``"))
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct TableFull {
28 pub name: String,
29 pub columns: Vec<Column>,
30 pub pk: Option<PrimaryKey>,
31 pub fks: Vec<ForeignKey>,
32 pub uniques: Vec<UniqueConstraint>,
33 pub checks: Vec<CheckConstraint>,
34 #[serde(default)]
36 pub strict: bool,
37 #[serde(default)]
39 pub without_rowid: bool,
40}
41
42impl TableFull {
43 #[must_use]
44 pub fn new(name: &str) -> Self {
45 Self {
46 name: name.to_string(),
47 columns: Vec::new(),
48 pk: None,
49 fks: Vec::new(),
50 uniques: Vec::new(),
51 checks: Vec::new(),
52 strict: false,
53 without_rowid: false,
54 }
55 }
56}
57
58fn table_full_to_table(table: &TableFull) -> Table {
59 Table {
60 name: table.name.clone().into(),
61 strict: table.strict,
62 without_rowid: table.without_rowid,
63 }
64}
65
66fn create_table_sql(table: &TableFull) -> String {
67 let ddl_table = table_full_to_table(table);
68 TableSql::new(&ddl_table)
69 .columns(&table.columns)
70 .primary_key(table.pk.as_ref())
71 .foreign_keys(&table.fks)
72 .unique_constraints(&table.uniques)
73 .check_constraints(&table.checks)
74 .create_table_sql()
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(tag = "type", rename_all = "snake_case")]
80pub enum JsonStatement {
81 CreateTable(CreateTableStatement),
82 DropTable(DropTableStatement),
83 RenameTable(RenameTableStatement),
84 AddColumn(AddColumnStatement),
85 DropColumn(DropColumnStatement),
86 RenameColumn(RenameColumnStatement),
87 RecreateColumn(RecreateColumnStatement),
88 RecreateTable(RecreateTableStatement),
89 CreateIndex(CreateIndexStatement),
90 DropIndex(DropIndexStatement),
91 CreateView(CreateViewStatement),
92 DropView(DropViewStatement),
93 RenameView(RenameViewStatement),
94}
95
96impl JsonStatement {
97 #[must_use]
99 pub const fn type_name(&self) -> &'static str {
100 match self {
101 Self::CreateTable(_) => "create_table",
102 Self::DropTable(_) => "drop_table",
103 Self::RenameTable(_) => "rename_table",
104 Self::AddColumn(_) => "add_column",
105 Self::DropColumn(_) => "drop_column",
106 Self::RenameColumn(_) => "rename_column",
107 Self::RecreateColumn(_) => "recreate_column",
108 Self::RecreateTable(_) => "recreate_table",
109 Self::CreateIndex(_) => "create_index",
110 Self::DropIndex(_) => "drop_index",
111 Self::CreateView(_) => "create_view",
112 Self::DropView(_) => "drop_view",
113 Self::RenameView(_) => "rename_view",
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct CreateTableStatement {
121 pub table: TableFull,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct DropTableStatement {
127 pub table_name: String,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct RenameTableStatement {
133 pub from: String,
134 pub to: String,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct AddColumnStatement {
140 pub column: Column,
141 pub fk: Option<ForeignKey>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct DropColumnStatement {
147 pub column: Column,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct RenameColumnStatement {
153 pub table: String,
154 pub from: String,
155 pub to: String,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct RecreateColumnStatement {
161 pub column: Column,
162 pub fk: Option<ForeignKey>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct RecreateTableStatement {
168 pub from: TableFull,
169 pub to: TableFull,
170 #[serde(default)]
171 pub data: Option<RebuildTableData>,
172}
173
174#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "camelCase", deny_unknown_fields)]
177pub struct RebuildTableData {
178 pub copies: BTreeMap<String, RebuildCopyExpression>,
179 pub validations: Vec<RebuildDataValidation>,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
184pub enum RebuildCopyExpression {
185 HexTextToBlob {
186 source: String,
187 },
188 IntegerMap {
189 source: String,
190 cases: Vec<(i64, i64)>,
191 },
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
195#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
196pub enum RebuildDataValidation {
197 HexText { column: String, bytes: usize },
198 JsonValid { column: String },
199 IntegerSet { column: String, allowed: Vec<i64> },
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct CreateIndexStatement {
205 pub index: Index,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct DropIndexStatement {
211 pub index: Index,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct CreateViewStatement {
217 pub view: View,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct DropViewStatement {
223 pub view: View,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct RenameViewStatement {
229 pub from: View,
230 pub to: View,
231}
232
233#[must_use]
239pub fn convert_statement(statement: &JsonStatement) -> Vec<String> {
240 match statement {
241 JsonStatement::CreateTable(st) => vec![convert_create_table(st)],
242 JsonStatement::DropTable(st) => vec![convert_drop_table(st)],
243 JsonStatement::RenameTable(st) => vec![convert_rename_table(st)],
244 JsonStatement::AddColumn(st) => vec![convert_add_column(st)],
245 JsonStatement::DropColumn(st) => vec![convert_drop_column(st)],
246 JsonStatement::RenameColumn(st) => vec![convert_rename_column(st)],
247 JsonStatement::RecreateColumn(st) => convert_recreate_column(st),
248 JsonStatement::RecreateTable(st) => convert_recreate_table(st),
249 JsonStatement::CreateIndex(st) => vec![convert_create_index(st)],
250 JsonStatement::DropIndex(st) => vec![convert_drop_index(st)],
251 JsonStatement::CreateView(st) => vec![convert_create_view(st)],
252 JsonStatement::DropView(st) => vec![convert_drop_view(st)],
253 JsonStatement::RenameView(st) => convert_rename_view(st),
254 }
255}
256
257pub fn statements_to_sql(statements: &[JsonStatement], breakpoints: bool) -> String {
259 let sql_statements: Vec<String> = statements.iter().flat_map(convert_statement).collect();
260
261 if breakpoints {
262 sql_statements.join(&format!("\n{BREAKPOINT}\n"))
263 } else {
264 sql_statements.join("\n")
265 }
266}
267
268#[derive(Debug, Clone)]
270pub struct GroupedStatement {
271 pub json_statement: JsonStatement,
273 pub sql_statements: Vec<String>,
275}
276
277#[derive(Debug, Clone)]
279pub struct ConversionResult {
280 pub sql_statements: Vec<String>,
282 pub grouped_statements: Vec<GroupedStatement>,
284}
285
286#[must_use]
288pub fn from_json(statements: Vec<JsonStatement>) -> ConversionResult {
289 let grouped: Vec<GroupedStatement> = statements
290 .into_iter()
291 .map(|statement| {
292 let sql_statements = convert_statement(&statement);
293 GroupedStatement {
294 json_statement: statement,
295 sql_statements,
296 }
297 })
298 .collect();
299
300 let sql_statements: Vec<String> = grouped
301 .iter()
302 .flat_map(|g| g.sql_statements.clone())
303 .collect();
304
305 ConversionResult {
306 sql_statements,
307 grouped_statements: grouped,
308 }
309}
310
311fn convert_create_table(st: &CreateTableStatement) -> String {
316 create_table_sql(&st.table)
317}
318
319fn convert_drop_table(st: &DropTableStatement) -> String {
320 format!("DROP TABLE {};", quote_ident(&st.table_name))
321}
322
323fn convert_rename_table(st: &RenameTableStatement) -> String {
324 format!(
325 "ALTER TABLE {} RENAME TO {};",
326 quote_ident(&st.from),
327 quote_ident(&st.to)
328 )
329}
330
331fn convert_add_column(st: &AddColumnStatement) -> String {
332 let column = &st.column;
333 let column_def = column.to_column_sql(false, false);
334
335 let reference = st
336 .fk
337 .as_ref()
338 .map(|fk| {
339 let to_cols = fk
340 .columns_to
341 .iter()
342 .map(|c| quote_ident(c))
343 .collect::<Vec<_>>()
344 .join(",");
345 if fk.name_explicit {
346 format!(
347 " CONSTRAINT {} REFERENCES {}({})",
348 quote_ident(&fk.name),
349 quote_ident(&fk.table_to),
350 to_cols
351 )
352 } else {
353 format!(" REFERENCES {}({})", quote_ident(&fk.table_to), to_cols)
354 }
355 })
356 .unwrap_or_default();
357
358 format!(
359 "ALTER TABLE {} ADD {}{};",
360 quote_ident(&column.table),
361 column_def,
362 reference
363 )
364}
365
366fn convert_drop_column(st: &DropColumnStatement) -> String {
367 format!(
368 "ALTER TABLE {} DROP COLUMN {};",
369 quote_ident(&st.column.table),
370 quote_ident(&st.column.name)
371 )
372}
373
374fn convert_rename_column(st: &RenameColumnStatement) -> String {
375 format!(
376 "ALTER TABLE {} RENAME COLUMN {} TO {};",
377 quote_ident(&st.table),
378 quote_ident(&st.from),
379 quote_ident(&st.to)
380 )
381}
382
383fn convert_recreate_column(st: &RecreateColumnStatement) -> Vec<String> {
384 let drop = format!(
386 "ALTER TABLE {} DROP COLUMN {};",
387 quote_ident(&st.column.table),
388 quote_ident(&st.column.name)
389 );
390 let add = convert_add_column(&AddColumnStatement {
391 column: st.column.clone(),
392 fk: st.fk.clone(),
393 });
394 vec![drop, add]
395}
396
397fn convert_recreate_table(st: &RecreateTableStatement) -> Vec<String> {
398 let name = &st.to.name;
399 let new_table_name = format!("__new_{name}");
400
401 let data = st.data.as_ref();
402 let copied_columns = st
403 .to
404 .columns
405 .iter()
406 .filter(|target| {
407 target.generated.is_none()
408 && (data.is_some_and(|data| data.copies.contains_key(target.name.as_ref()))
409 || st
410 .from
411 .columns
412 .iter()
413 .any(|source| source.name == target.name && source.generated.is_none()))
414 })
415 .collect::<Vec<_>>();
416 let cols_str = copied_columns
417 .iter()
418 .map(|column| quote_ident(&column.name))
419 .collect::<Vec<_>>()
420 .join(", ");
421 let select_str = copied_columns
422 .iter()
423 .map(|column| {
424 data.and_then(|data| data.copies.get(column.name.as_ref()))
425 .map_or_else(|| quote_ident(&column.name), render_copy_expression)
426 })
427 .collect::<Vec<_>>()
428 .join(", ");
429
430 let mut statements = Vec::new();
431
432 if let Some(data) = data
433 && !data.validations.is_empty()
434 {
435 let guard = format!("__drizzle_rebuild_guard_{name}");
436 statements.push(format!(
437 "CREATE TEMP TABLE {} (`valid` INTEGER NOT NULL CHECK (`valid` = 1)) STRICT;",
438 quote_ident(&guard)
439 ));
440 for validation in &data.validations {
441 statements.push(format!(
442 "INSERT INTO {}(`valid`) SELECT 0 FROM {} WHERE {} LIMIT 1;",
443 quote_ident(&guard),
444 quote_ident(name),
445 render_invalid_predicate(validation)
446 ));
447 }
448 statements.push(format!("DROP TABLE {};", quote_ident(&guard)));
449 }
450
451 statements.push("PRAGMA foreign_keys=OFF;".to_string());
453
454 let mut tmp_table = st.to.clone();
456 tmp_table.name.clone_from(&new_table_name);
457 for check in &mut tmp_table.checks {
459 check.table = new_table_name.clone().into();
460 }
461 statements.push(convert_create_table(&CreateTableStatement {
462 table: tmp_table,
463 }));
464
465 let missing_not_null: Vec<&str> = st
469 .to
470 .columns
471 .iter()
472 .filter(|col| {
473 col.not_null
474 && col.default.is_none()
475 && col.generated.is_none()
476 && !st.from.columns.iter().any(|from| from.name == col.name)
477 })
478 .map(|col| col.name.as_ref())
479 .collect();
480 let warning = if missing_not_null.is_empty() {
481 String::new()
482 } else {
483 format!(
484 "-- WARNING: new NOT NULL column(s) without a default: {} — the copy below fails if `{name}` has rows\n",
485 missing_not_null.join(", ")
486 )
487 };
488 statements.push(format!(
489 "{warning}INSERT INTO {}({cols_str}) SELECT {select_str} FROM {};",
490 quote_ident(&new_table_name),
491 quote_ident(name)
492 ));
493
494 statements.push(format!("DROP TABLE {};", quote_ident(name)));
496
497 statements.push(format!(
499 "ALTER TABLE {} RENAME TO {};",
500 quote_ident(&new_table_name),
501 quote_ident(name)
502 ));
503
504 statements.push("PRAGMA foreign_keys=ON;".to_string());
506
507 statements
508}
509
510fn render_copy_expression(expression: &RebuildCopyExpression) -> String {
511 match expression {
512 RebuildCopyExpression::HexTextToBlob { source } => {
513 format!("unhex({})", quote_ident(source))
514 }
515 RebuildCopyExpression::IntegerMap { source, cases } => {
516 let cases = cases
517 .iter()
518 .map(|(from, to)| format!(" WHEN {from} THEN {to}"))
519 .collect::<String>();
520 format!("CASE {}{cases} ELSE NULL END", quote_ident(source))
521 }
522 }
523}
524
525fn render_invalid_predicate(validation: &RebuildDataValidation) -> String {
526 match validation {
527 RebuildDataValidation::HexText { column, bytes } => {
528 let column = quote_ident(column);
529 let chars = bytes.saturating_mul(2);
530 format!(
531 "{column} IS NOT NULL AND (typeof({column}) <> 'text' OR length({column}) <> {chars} OR coalesce(length(unhex({column})), -1) <> {bytes})"
532 )
533 }
534 RebuildDataValidation::JsonValid { column } => {
535 let column = quote_ident(column);
536 format!(
537 "{column} IS NOT NULL AND (typeof({column}) <> 'text' OR json_valid({column}) <> 1)"
538 )
539 }
540 RebuildDataValidation::IntegerSet { column, allowed } => {
541 let column = quote_ident(column);
542 let allowed = allowed
543 .iter()
544 .map(i64::to_string)
545 .collect::<Vec<_>>()
546 .join(", ");
547 format!(
548 "{column} IS NOT NULL AND (typeof({column}) <> 'integer' OR {column} NOT IN ({allowed}))"
549 )
550 }
551 }
552}
553
554fn convert_create_index(st: &CreateIndexStatement) -> String {
555 st.index.create_index_sql()
556}
557
558fn convert_drop_index(st: &DropIndexStatement) -> String {
559 format!("DROP INDEX IF EXISTS {};", quote_ident(&st.index.name))
560}
561
562fn convert_create_view(st: &CreateViewStatement) -> String {
563 st.view.create_view_sql()
564}
565
566fn convert_drop_view(st: &DropViewStatement) -> String {
567 format!("DROP VIEW {};", quote_ident(&st.view.name))
568}
569
570fn convert_rename_view(st: &RenameViewStatement) -> Vec<String> {
571 vec![
574 format!("DROP VIEW IF EXISTS {};", quote_ident(&st.from.name)),
575 st.to.create_view_sql(),
576 ]
577}
578
579#[must_use]
585pub fn prepare_add_columns(columns: &[Column], fks: &[ForeignKey]) -> Vec<AddColumnStatement> {
586 columns
587 .iter()
588 .map(|col| {
589 let fk = fks
590 .iter()
591 .find(|fk| {
592 fk.columns.len() == 1 && fk.columns[0] == col.name && fk.table == col.table
593 })
594 .cloned();
595 AddColumnStatement {
596 column: col.clone(),
597 fk,
598 }
599 })
600 .collect()
601}
602
603use crate::sqlite::SchemaDiff;
608use crate::sqlite::collection::{DiffType, EntityDiff};
609use crate::traits::EntityKind;
610use std::collections::{HashMap, HashSet};
611
612pub struct TopologicalSortResult<'a> {
614 pub tables: Vec<&'a EntityDiff>,
616 pub has_circular_deps: bool,
618}
619
620fn topological_sort_tables_for_create<'a>(
622 tables: &[&'a EntityDiff],
623 diff: &SchemaDiff,
624) -> TopologicalSortResult<'a> {
625 if tables.len() <= 1 {
626 return TopologicalSortResult {
627 tables: tables.to_vec(),
628 has_circular_deps: false,
629 };
630 }
631
632 let mut table_map: HashMap<String, &EntityDiff> = HashMap::new();
634 for t in tables {
635 if let Some(name) = t.name.split(':').next_back() {
636 table_map.insert(name.to_string(), *t);
637 }
638 }
639
640 let mut dependencies: HashMap<String, HashSet<String>> = HashMap::new();
642 for table_name in table_map.keys() {
643 dependencies.insert(table_name.clone(), HashSet::new());
644 }
645
646 for fk_diff in diff.by_kind(EntityKind::ForeignKey) {
648 if fk_diff.diff_type == DiffType::Create
649 && let Some(crate::sqlite::ddl::SqliteEntity::ForeignKey(fk)) = fk_diff.right.as_ref()
650 {
651 let from_table = fk.table.to_string();
652 let to_table = fk.table_to.to_string();
653 if table_map.contains_key(&from_table)
655 && table_map.contains_key(&to_table)
656 && let Some(deps) = dependencies.get_mut(&from_table)
657 {
658 deps.insert(to_table);
659 }
660 }
661 }
662
663 let mut result = Vec::new();
667 let mut remaining: HashSet<String> = table_map.keys().cloned().collect();
668 let mut satisfied: HashSet<String> = HashSet::new();
669 let mut has_circular_deps = false;
670
671 while !remaining.is_empty() {
672 let mut ready: Vec<String> = remaining
674 .iter()
675 .filter(|t| {
676 dependencies
677 .get(*t)
678 .is_none_or(|deps| deps.iter().all(|d| satisfied.contains(d)))
679 })
680 .cloned()
681 .collect();
682 ready.sort_unstable();
683
684 if ready.is_empty() {
685 has_circular_deps = true;
687 let mut leftover: Vec<&String> = remaining.iter().collect();
688 leftover.sort_unstable();
689 for t in leftover {
690 if let Some(entity) = table_map.get(t) {
691 result.push(*entity);
692 }
693 }
694 break;
695 }
696
697 for t in ready {
698 remaining.remove(&t);
699 satisfied.insert(t.clone());
700 if let Some(entity) = table_map.get(&t) {
701 result.push(*entity);
702 }
703 }
704 }
705
706 TopologicalSortResult {
707 tables: result,
708 has_circular_deps,
709 }
710}
711
712fn topological_sort_tables_for_drop<'a>(
714 tables: &[&'a EntityDiff],
715 diff: &SchemaDiff,
716) -> TopologicalSortResult<'a> {
717 let create_result = topological_sort_tables_for_create(tables, diff);
719 TopologicalSortResult {
720 tables: create_result.tables.into_iter().rev().collect(),
721 has_circular_deps: create_result.has_circular_deps,
722 }
723}
724
725fn append_drop_table_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
726 let dropped_tables = diff.dropped_tables();
727 let drop_result = topological_sort_tables_for_drop(&dropped_tables, diff);
728
729 if drop_result.has_circular_deps && !drop_result.tables.is_empty() {
731 statements.push("PRAGMA foreign_keys=OFF;".to_string());
732 }
733
734 for entity_diff in &drop_result.tables {
735 if let Some(name) = entity_diff.name.split(':').next_back() {
736 statements.push(convert_drop_table(&DropTableStatement {
737 table_name: name.to_string(),
738 }));
739 }
740 }
741
742 if drop_result.has_circular_deps && !drop_result.tables.is_empty() {
743 statements.push("PRAGMA foreign_keys=ON;".to_string());
744 }
745}
746
747fn build_table_full(diff: &SchemaDiff, table: &crate::sqlite::ddl::Table) -> TableFull {
748 let columns_for_table: Vec<Column> = diff
749 .by_kind(EntityKind::Column)
750 .into_iter()
751 .filter(|d| d.diff_type == DiffType::Create)
752 .filter_map(|d| d.right.as_ref())
753 .filter_map(|e| match e {
754 crate::sqlite::ddl::SqliteEntity::Column(c) if c.table == table.name => Some(c.clone()),
755 _ => None,
756 })
757 .collect();
758
759 let pk = diff
760 .by_kind(EntityKind::PrimaryKey)
761 .into_iter()
762 .filter(|d| d.diff_type == DiffType::Create)
763 .filter_map(|d| d.right.as_ref())
764 .find_map(|e| match e {
765 crate::sqlite::ddl::SqliteEntity::PrimaryKey(p) if p.table == table.name => {
766 Some(p.clone())
767 }
768 _ => None,
769 });
770
771 let fks: Vec<ForeignKey> = diff
772 .by_kind(EntityKind::ForeignKey)
773 .into_iter()
774 .filter(|d| d.diff_type == DiffType::Create)
775 .filter_map(|d| d.right.as_ref())
776 .filter_map(|e| match e {
777 crate::sqlite::ddl::SqliteEntity::ForeignKey(fk) if fk.table == table.name => {
778 Some(fk.clone())
779 }
780 _ => None,
781 })
782 .collect();
783
784 let uniques: Vec<UniqueConstraint> = diff
785 .by_kind(EntityKind::UniqueConstraint)
786 .into_iter()
787 .filter(|d| d.diff_type == DiffType::Create)
788 .filter_map(|d| d.right.as_ref())
789 .filter_map(|e| match e {
790 crate::sqlite::ddl::SqliteEntity::UniqueConstraint(u) if u.table == table.name => {
791 Some(u.clone())
792 }
793 _ => None,
794 })
795 .collect();
796
797 let checks: Vec<CheckConstraint> = diff
798 .by_kind(EntityKind::CheckConstraint)
799 .into_iter()
800 .filter(|d| d.diff_type == DiffType::Create)
801 .filter_map(|d| d.right.as_ref())
802 .filter_map(|e| match e {
803 crate::sqlite::ddl::SqliteEntity::CheckConstraint(c) if c.table == table.name => {
804 Some(c.clone())
805 }
806 _ => None,
807 })
808 .collect();
809
810 TableFull {
811 name: table.name.to_string(),
812 columns: columns_for_table,
813 pk,
814 fks,
815 uniques,
816 checks,
817 strict: table.strict,
818 without_rowid: table.without_rowid,
819 }
820}
821
822fn append_create_table_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
823 let created_tables = diff.created_tables();
824 let create_result = topological_sort_tables_for_create(&created_tables, diff);
825
826 if create_result.has_circular_deps && !create_result.tables.is_empty() {
828 statements.push("PRAGMA foreign_keys=OFF;".to_string());
829 }
830
831 for entity_diff in &create_result.tables {
832 if let Some(crate::sqlite::ddl::SqliteEntity::Table(table)) = entity_diff.right.as_ref() {
833 let table_full = build_table_full(diff, table);
834 statements.push(convert_create_table(&CreateTableStatement {
835 table: table_full,
836 }));
837 }
838 }
839
840 if create_result.has_circular_deps && !create_result.tables.is_empty() {
842 statements.push("PRAGMA foreign_keys=ON;".to_string());
843 }
844}
845
846fn append_add_column_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
847 for entity_diff in diff.by_kind(EntityKind::Column) {
848 if entity_diff.diff_type == DiffType::Create
849 && let Some(crate::sqlite::ddl::SqliteEntity::Column(col)) = entity_diff.right.as_ref()
850 {
851 let table_was_created = diff.created_tables().iter().any(|t| t.name == col.table);
852
853 if !table_was_created {
854 statements.push(convert_add_column(&AddColumnStatement {
855 column: col.clone(),
856 fk: None,
857 }));
858 }
859 }
860 }
861}
862
863fn append_index_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
864 for entity_diff in diff.by_kind(EntityKind::Index) {
865 match entity_diff.diff_type {
866 DiffType::Drop => {
867 if let Some(crate::sqlite::ddl::SqliteEntity::Index(idx)) =
868 entity_diff.left.as_ref()
869 {
870 statements.push(convert_drop_index(&DropIndexStatement {
871 index: idx.clone(),
872 }));
873 }
874 }
875 DiffType::Create => {
876 if let Some(crate::sqlite::ddl::SqliteEntity::Index(idx)) =
877 entity_diff.right.as_ref()
878 {
879 statements.push(convert_create_index(&CreateIndexStatement {
880 index: idx.clone(),
881 }));
882 }
883 }
884 DiffType::Alter => {
885 if let (
887 Some(crate::sqlite::ddl::SqliteEntity::Index(old)),
888 Some(crate::sqlite::ddl::SqliteEntity::Index(new)),
889 ) = (entity_diff.left.as_ref(), entity_diff.right.as_ref())
890 {
891 statements.push(convert_drop_index(&DropIndexStatement {
892 index: old.clone(),
893 }));
894 statements.push(convert_create_index(&CreateIndexStatement {
895 index: new.clone(),
896 }));
897 }
898 }
899 }
900 }
901}
902
903pub struct Generator {
915 pub breakpoints: bool,
917}
918
919impl Default for Generator {
920 fn default() -> Self {
921 Self::new()
922 }
923}
924
925impl Generator {
926 #[must_use]
927 pub const fn new() -> Self {
928 Self { breakpoints: true }
929 }
930
931 #[must_use]
932 pub const fn with_breakpoints(mut self, breakpoints: bool) -> Self {
933 self.breakpoints = breakpoints;
934 self
935 }
936
937 #[must_use]
939 pub fn generate_migration(&self, diff: &SchemaDiff) -> Vec<String> {
940 let mut statements = Vec::new();
941
942 append_drop_table_stmts(&mut statements, diff);
943 append_create_table_stmts(&mut statements, diff);
944 append_add_column_stmts(&mut statements, diff);
945 append_index_stmts(&mut statements, diff);
946
947 statements
948 }
949
950 #[must_use]
952 pub fn statements_to_sql(&self, statements: &[String]) -> String {
953 if self.breakpoints {
954 statements.join(&format!("\n{BREAKPOINT}\n"))
955 } else {
956 statements.join("\n")
957 }
958 }
959}
960
961#[cfg(test)]
962mod tests {
963 use super::*;
964 use crate::sqlite::ddl::{Column, IndexColumn};
965
966 #[test]
967 fn test_create_table_simple() {
968 let table = TableFull {
969 name: "users".to_string(),
970 columns: vec![
971 Column::new("users", "id", "integer").not_null(),
972 Column::new("users", "name", "text").not_null(),
973 ],
974 pk: None,
975 fks: Vec::new(),
976 uniques: Vec::new(),
977 checks: Vec::new(),
978 strict: false,
979 without_rowid: false,
980 };
981
982 let sql = convert_create_table(&CreateTableStatement { table });
983 assert_eq!(
984 sql,
985 "CREATE TABLE `users` (\n\t`id` INTEGER NOT NULL,\n\t`name` TEXT NOT NULL\n);"
986 );
987 }
988
989 #[test]
990 fn test_create_table_with_pk() {
991 let table = TableFull {
992 name: "users".to_string(),
993 columns: vec![Column::new("users", "id", "integer")],
994 pk: Some(PrimaryKey::from_strings(
995 "users".to_string(),
996 "users_pk".to_string(),
997 vec!["id".to_string()],
998 )),
999 fks: Vec::new(),
1000 uniques: Vec::new(),
1001 checks: Vec::new(),
1002 strict: false,
1003 without_rowid: false,
1004 };
1005
1006 let sql = convert_create_table(&CreateTableStatement { table });
1007 assert_eq!(
1008 sql,
1009 "CREATE TABLE `users` (\n\t`id` INTEGER PRIMARY KEY\n);"
1010 );
1011 }
1012
1013 #[test]
1014 fn test_drop_table() {
1015 let sql = convert_drop_table(&DropTableStatement {
1016 table_name: "users".to_string(),
1017 });
1018 assert_eq!(sql, "DROP TABLE `users`;");
1019 }
1020
1021 #[test]
1022 fn test_add_column() {
1023 let sql = convert_add_column(&AddColumnStatement {
1024 column: Column::new("users", "email", "text").not_null(),
1025 fk: None,
1026 });
1027 assert_eq!(sql, "ALTER TABLE `users` ADD `email` TEXT NOT NULL;");
1028 }
1029
1030 #[test]
1031 fn test_create_index() {
1032 let index = Index::new(
1033 "users",
1034 "idx_users_email",
1035 vec![IndexColumn {
1036 value: "email".into(),
1037 is_expression: false,
1038 }],
1039 )
1040 .unique();
1041
1042 let sql = convert_create_index(&CreateIndexStatement { index });
1043 assert_eq!(
1044 sql,
1045 "CREATE UNIQUE INDEX `idx_users_email` ON `users`(`email`);"
1046 );
1047 }
1048
1049 #[test]
1050 fn test_rename_view_returns_two_statements() {
1051 let mut from = View::new("old_view");
1052 from.definition = Some("SELECT 1".into());
1053 let mut to = View::new("new_view");
1054 to.definition = Some("SELECT 1".into());
1055
1056 let statements =
1057 convert_statement(&JsonStatement::RenameView(RenameViewStatement { from, to }));
1058 assert_eq!(
1059 statements,
1060 vec![
1061 "DROP VIEW IF EXISTS `old_view`;".to_string(),
1062 "CREATE VIEW `new_view` AS SELECT 1;".to_string(),
1063 ]
1064 );
1065 }
1066
1067 #[test]
1068 fn test_recreate_warns_on_new_not_null_column_without_default() {
1069 let from = TableFull {
1070 name: "users".to_string(),
1071 columns: vec![Column::new("users", "id", "integer").not_null()],
1072 pk: None,
1073 fks: Vec::new(),
1074 uniques: Vec::new(),
1075 checks: Vec::new(),
1076 strict: false,
1077 without_rowid: false,
1078 };
1079 let to = TableFull {
1080 name: "users".to_string(),
1081 columns: vec![
1082 Column::new("users", "id", "integer").not_null(),
1083 Column::new("users", "email", "text").not_null(),
1084 ],
1085 pk: None,
1086 fks: Vec::new(),
1087 uniques: Vec::new(),
1088 checks: Vec::new(),
1089 strict: false,
1090 without_rowid: false,
1091 };
1092
1093 let statements = convert_recreate_table(&RecreateTableStatement {
1094 from,
1095 to,
1096 data: None,
1097 });
1098 let insert = statements
1099 .iter()
1100 .find(|s| s.contains("INSERT INTO"))
1101 .expect("insert statement");
1102 assert!(
1103 insert.starts_with("-- WARNING:") && insert.contains("email"),
1104 "expected warning comment ahead of the INSERT, got: {insert}"
1105 );
1106 }
1107
1108 #[test]
1109 fn test_create_table_strict() {
1110 let table = TableFull {
1111 name: "data".to_string(),
1112 columns: vec![
1113 Column::new("data", "id", "integer").not_null(),
1114 Column::new("data", "value", "text").not_null(),
1115 ],
1116 pk: None,
1117 fks: Vec::new(),
1118 uniques: Vec::new(),
1119 checks: Vec::new(),
1120 strict: true,
1121 without_rowid: false,
1122 };
1123
1124 let sql = convert_create_table(&CreateTableStatement { table });
1125 assert_eq!(
1126 sql,
1127 "CREATE TABLE `data` (\n\t`id` INTEGER NOT NULL,\n\t`value` TEXT NOT NULL\n) STRICT;"
1128 );
1129 }
1130
1131 #[test]
1132 fn test_create_table_without_rowid() {
1133 let table = TableFull {
1134 name: "kv".to_string(),
1135 columns: vec![
1136 Column::new("kv", "key", "text").not_null(),
1137 Column::new("kv", "value", "blob"),
1138 ],
1139 pk: Some(PrimaryKey::from_strings(
1140 "kv".to_string(),
1141 "kv_pk".to_string(),
1142 vec!["key".to_string()],
1143 )),
1144 fks: Vec::new(),
1145 uniques: Vec::new(),
1146 checks: Vec::new(),
1147 strict: false,
1148 without_rowid: true,
1149 };
1150
1151 let sql = convert_create_table(&CreateTableStatement { table });
1152 assert_eq!(
1153 sql,
1154 "CREATE TABLE `kv` (\n\t`key` TEXT PRIMARY KEY NOT NULL,\n\t`value` BLOB\n) WITHOUT ROWID;"
1155 );
1156 }
1157
1158 #[test]
1159 fn test_create_table_strict_without_rowid() {
1160 let table = TableFull {
1161 name: "cache".to_string(),
1162 columns: vec![
1163 Column::new("cache", "key", "text").not_null(),
1164 Column::new("cache", "data", "blob"),
1165 ],
1166 pk: Some(PrimaryKey::from_strings(
1167 "cache".to_string(),
1168 "cache_pk".to_string(),
1169 vec!["key".to_string()],
1170 )),
1171 fks: Vec::new(),
1172 uniques: Vec::new(),
1173 checks: Vec::new(),
1174 strict: true,
1175 without_rowid: true,
1176 };
1177
1178 let sql = convert_create_table(&CreateTableStatement { table });
1179 assert_eq!(
1180 sql,
1181 "CREATE TABLE `cache` (\n\t`key` TEXT PRIMARY KEY NOT NULL,\n\t`data` BLOB\n) WITHOUT ROWID, STRICT;"
1182 );
1183 }
1184}