1use vespertide_core::{MigrationAction, MigrationPlan, TableDef};
2use vespertide_planner::apply_action;
3
4use crate::DatabaseBackend;
5use crate::error::QueryError;
6use crate::sql::BuiltQuery;
7use crate::sql::build_action_queries_with_pending;
8
9pub struct PlanQueries {
10 pub action: MigrationAction,
11 pub postgres: Vec<BuiltQuery>,
12 pub mysql: Vec<BuiltQuery>,
13 pub sqlite: Vec<BuiltQuery>,
14}
15
16pub fn build_plan_queries(
17 plan: &MigrationPlan,
18 current_schema: &[TableDef],
19) -> Result<Vec<PlanQueries>, QueryError> {
20 let mut queries: Vec<PlanQueries> = Vec::new();
21 let mut evolving_schema = current_schema.to_vec();
23
24 for (i, action) in plan.actions.iter().enumerate() {
25 let pending_constraints: Vec<vespertide_core::TableConstraint> =
31 if let MigrationAction::AddConstraint { table, .. } = action {
32 plan.actions[i + 1..]
33 .iter()
34 .filter_map(|a| {
35 if let MigrationAction::AddConstraint {
36 table: t,
37 constraint,
38 } = a
39 {
40 if t == table
41 && matches!(
42 constraint,
43 vespertide_core::TableConstraint::Index { .. }
44 | vespertide_core::TableConstraint::Unique { .. }
45 )
46 {
47 Some(constraint.clone())
48 } else {
49 None
50 }
51 } else {
52 None
53 }
54 })
55 .collect()
56 } else {
57 vec![]
58 };
59
60 let postgres_queries = build_action_queries_with_pending(
62 &DatabaseBackend::Postgres,
63 action,
64 &evolving_schema,
65 &pending_constraints,
66 )?;
67 let mysql_queries = build_action_queries_with_pending(
68 &DatabaseBackend::MySql,
69 action,
70 &evolving_schema,
71 &pending_constraints,
72 )?;
73 let sqlite_queries = build_action_queries_with_pending(
74 &DatabaseBackend::Sqlite,
75 action,
76 &evolving_schema,
77 &pending_constraints,
78 )?;
79 queries.push(PlanQueries {
80 action: action.clone(),
81 postgres: postgres_queries,
82 mysql: mysql_queries,
83 sqlite: sqlite_queries,
84 });
85
86 let _ = apply_action(&mut evolving_schema, action);
91 }
92 Ok(queries)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::sql::DatabaseBackend;
99 use insta::{assert_snapshot, with_settings};
100 use rstest::rstest;
101 use vespertide_core::{
102 ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType,
103 };
104
105 fn col(name: &str, ty: ColumnType) -> ColumnDef {
106 ColumnDef {
107 name: name.to_string(),
108 r#type: ty,
109 nullable: true,
110 default: None,
111 comment: None,
112 primary_key: None,
113 unique: None,
114 index: None,
115 foreign_key: None,
116 }
117 }
118
119 #[rstest]
120 #[case::empty(
121 MigrationPlan {
122 id: String::new(),
123 comment: None,
124 created_at: None,
125 version: 1,
126 actions: vec![],
127 },
128 0
129 )]
130 #[case::single_action(
131 MigrationPlan {
132 id: String::new(),
133 comment: None,
134 created_at: None,
135 version: 1,
136 actions: vec![MigrationAction::DeleteTable {
137 table: "users".into(),
138 }],
139 },
140 1
141 )]
142 #[case::multiple_actions(
143 MigrationPlan {
144 id: String::new(),
145 comment: None,
146 created_at: None,
147 version: 1,
148 actions: vec![
149 MigrationAction::CreateTable {
150 table: "users".into(),
151 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
152 constraints: vec![],
153 },
154 MigrationAction::DeleteTable {
155 table: "posts".into(),
156 },
157 ],
158 },
159 2
160 )]
161 fn test_build_plan_queries(#[case] plan: MigrationPlan, #[case] expected_count: usize) {
162 let result = build_plan_queries(&plan, &[]).unwrap();
163 assert_eq!(
164 result.len(),
165 expected_count,
166 "Expected {} queries, got {}",
167 expected_count,
168 result.len()
169 );
170 }
171
172 fn build_sql_snapshot(result: &[BuiltQuery], backend: DatabaseBackend) -> String {
173 result
174 .iter()
175 .map(|q| q.build(backend))
176 .collect::<Vec<_>>()
177 .join(";\n")
178 }
179
180 #[rstest]
185 #[case::postgres("postgres", DatabaseBackend::Postgres)]
186 #[case::mysql("mysql", DatabaseBackend::MySql)]
187 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
188 fn test_delete_column_after_create_table_with_inline_unique(
189 #[case] title: &str,
190 #[case] backend: DatabaseBackend,
191 ) {
192 let mut col_with_unique = col("gift_code", ColumnType::Simple(SimpleColumnType::Text));
193 col_with_unique.unique = Some(vespertide_core::StrOrBoolOrArray::Bool(true));
194
195 let plan = MigrationPlan {
196 id: String::new(),
197 comment: None,
198 created_at: None,
199 version: 1,
200 actions: vec![
201 MigrationAction::CreateTable {
202 table: "gift".into(),
203 columns: vec![
204 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
205 col_with_unique,
206 ],
207 constraints: vec![], },
209 MigrationAction::DeleteColumn {
210 table: "gift".into(),
211 column: "gift_code".into(),
212 },
213 ],
214 };
215
216 let result = build_plan_queries(&plan, &[]).unwrap();
217 let queries = match backend {
218 DatabaseBackend::Postgres => &result[1].postgres,
219 DatabaseBackend::MySql => &result[1].mysql,
220 DatabaseBackend::Sqlite => &result[1].sqlite,
221 };
222 let sql = build_sql_snapshot(queries, backend);
223
224 with_settings!({ snapshot_suffix => format!("inline_unique_{}", title) }, {
225 assert_snapshot!(sql);
226 });
227 }
228
229 #[rstest]
231 #[case::postgres("postgres", DatabaseBackend::Postgres)]
232 #[case::mysql("mysql", DatabaseBackend::MySql)]
233 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
234 fn test_delete_column_after_create_table_with_inline_index(
235 #[case] title: &str,
236 #[case] backend: DatabaseBackend,
237 ) {
238 let mut col_with_index = col("email", ColumnType::Simple(SimpleColumnType::Text));
239 col_with_index.index = Some(vespertide_core::StrOrBoolOrArray::Bool(true));
240
241 let plan = MigrationPlan {
242 id: String::new(),
243 comment: None,
244 created_at: None,
245 version: 1,
246 actions: vec![
247 MigrationAction::CreateTable {
248 table: "users".into(),
249 columns: vec![
250 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
251 col_with_index,
252 ],
253 constraints: vec![],
254 },
255 MigrationAction::DeleteColumn {
256 table: "users".into(),
257 column: "email".into(),
258 },
259 ],
260 };
261
262 let result = build_plan_queries(&plan, &[]).unwrap();
263 let queries = match backend {
264 DatabaseBackend::Postgres => &result[1].postgres,
265 DatabaseBackend::MySql => &result[1].mysql,
266 DatabaseBackend::Sqlite => &result[1].sqlite,
267 };
268 let sql = build_sql_snapshot(queries, backend);
269
270 with_settings!({ snapshot_suffix => format!("inline_index_{}", title) }, {
271 assert_snapshot!(sql);
272 });
273 }
274
275 #[test]
276 fn test_build_plan_queries_sql_content() {
277 let plan = MigrationPlan {
278 id: String::new(),
279 comment: None,
280 created_at: None,
281 version: 1,
282 actions: vec![
283 MigrationAction::CreateTable {
284 table: "users".into(),
285 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
286 constraints: vec![],
287 },
288 MigrationAction::DeleteTable {
289 table: "posts".into(),
290 },
291 ],
292 };
293
294 let result = build_plan_queries(&plan, &[]).unwrap();
295 assert_eq!(result.len(), 2);
296
297 let sql1 = result[0]
299 .postgres
300 .iter()
301 .map(|q| q.build(DatabaseBackend::Postgres))
302 .collect::<Vec<_>>()
303 .join(";\n");
304 assert!(sql1.contains("CREATE TABLE"));
305 assert!(sql1.contains("\"users\""));
306 assert!(sql1.contains("\"id\""));
307
308 let sql2 = result[1]
309 .postgres
310 .iter()
311 .map(|q| q.build(DatabaseBackend::Postgres))
312 .collect::<Vec<_>>()
313 .join(";\n");
314 assert!(sql2.contains("DROP TABLE"));
315 assert!(sql2.contains("\"posts\""));
316
317 let sql1_mysql = result[0]
319 .mysql
320 .iter()
321 .map(|q| q.build(DatabaseBackend::MySql))
322 .collect::<Vec<_>>()
323 .join(";\n");
324 assert!(sql1_mysql.contains("`users`"));
325
326 let sql2_mysql = result[1]
327 .mysql
328 .iter()
329 .map(|q| q.build(DatabaseBackend::MySql))
330 .collect::<Vec<_>>()
331 .join(";\n");
332 assert!(sql2_mysql.contains("`posts`"));
333 }
334
335 use vespertide_core::{ReferenceAction, TableConstraint};
338
339 fn fk_constraint() -> TableConstraint {
340 TableConstraint::ForeignKey {
341 name: None,
342 columns: vec!["category_id".into()],
343 ref_table: "category".into(),
344 ref_columns: vec!["id".into()],
345 on_delete: Some(ReferenceAction::Cascade),
346 on_update: None,
347 }
348 }
349
350 fn unique_constraint() -> TableConstraint {
351 TableConstraint::Unique {
352 name: None,
353 columns: vec!["category_id".into()],
354 }
355 }
356
357 fn index_constraint() -> TableConstraint {
358 TableConstraint::Index {
359 name: None,
360 columns: vec!["category_id".into()],
361 }
362 }
363
364 fn plan_add_column_with_constraints(order: &[TableConstraint]) -> MigrationPlan {
366 let mut actions: Vec<MigrationAction> = vec![MigrationAction::AddColumn {
367 table: "product".into(),
368 column: Box::new(col(
369 "category_id",
370 ColumnType::Simple(SimpleColumnType::BigInt),
371 )),
372 fill_with: None,
373 }];
374 for c in order {
375 actions.push(MigrationAction::AddConstraint {
376 table: "product".into(),
377 constraint: c.clone(),
378 });
379 }
380 MigrationPlan {
381 id: String::new(),
382 comment: None,
383 created_at: None,
384 version: 1,
385 actions,
386 }
387 }
388
389 fn plan_remove_constraints_then_drop(order: &[TableConstraint]) -> MigrationPlan {
391 let mut actions: Vec<MigrationAction> = Vec::new();
392 for c in order {
393 actions.push(MigrationAction::RemoveConstraint {
394 table: "product".into(),
395 constraint: c.clone(),
396 });
397 }
398 actions.push(MigrationAction::DeleteColumn {
399 table: "product".into(),
400 column: "category_id".into(),
401 });
402 MigrationPlan {
403 id: String::new(),
404 comment: None,
405 created_at: None,
406 version: 1,
407 actions,
408 }
409 }
410
411 fn base_schema_no_constraints() -> Vec<TableDef> {
413 vec![TableDef {
414 name: "product".into(),
415 description: None,
416 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
417 constraints: vec![],
418 }]
419 }
420
421 fn base_schema_with_all_constraints() -> Vec<TableDef> {
423 vec![TableDef {
424 name: "product".into(),
425 description: None,
426 columns: vec![
427 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
428 col("category_id", ColumnType::Simple(SimpleColumnType::BigInt)),
429 ],
430 constraints: vec![fk_constraint(), unique_constraint(), index_constraint()],
431 }]
432 }
433
434 fn collect_all_sql(result: &[PlanQueries], backend: DatabaseBackend) -> String {
436 result
437 .iter()
438 .enumerate()
439 .map(|(i, pq)| {
440 let queries = match backend {
441 DatabaseBackend::Postgres => &pq.postgres,
442 DatabaseBackend::MySql => &pq.mysql,
443 DatabaseBackend::Sqlite => &pq.sqlite,
444 };
445 let sql = build_sql_snapshot(queries, backend);
446 format!("-- Action {}: {:?}\n{}", i, pq.action, sql)
447 })
448 .collect::<Vec<_>>()
449 .join("\n\n")
450 }
451
452 fn assert_no_duplicate_indexes_per_action(result: &[PlanQueries]) {
457 for (i, pq) in result.iter().enumerate() {
458 let stmts: Vec<String> = pq
459 .sqlite
460 .iter()
461 .map(|q| q.build(DatabaseBackend::Sqlite))
462 .collect();
463
464 let index_stmts: Vec<&String> = stmts
465 .iter()
466 .filter(|s| s.contains("CREATE INDEX") || s.contains("CREATE UNIQUE INDEX"))
467 .collect();
468
469 let mut seen = std::collections::HashSet::new();
470 for stmt in &index_stmts {
471 assert!(
472 seen.insert(stmt.as_str()),
473 "Duplicate index within action {} ({:?}):\n {}\nAll index statements in this action:\n{}",
474 i,
475 pq.action,
476 stmt,
477 index_stmts
478 .iter()
479 .map(|s| format!(" {}", s))
480 .collect::<Vec<_>>()
481 .join("\n")
482 );
483 }
484 }
485 }
486
487 fn assert_no_orphan_duplicate_indexes(result: &[PlanQueries]) {
492 let mut live_indexes: std::collections::HashSet<String> = std::collections::HashSet::new();
495
496 for pq in result {
497 let stmts: Vec<String> = pq
498 .sqlite
499 .iter()
500 .map(|q| q.build(DatabaseBackend::Sqlite))
501 .collect();
502
503 if stmts.iter().any(|s| s.starts_with("DROP TABLE")) {
505 live_indexes.clear();
506 }
507
508 for stmt in &stmts {
509 if stmt.contains("CREATE INDEX") || stmt.contains("CREATE UNIQUE INDEX") {
510 assert!(
511 live_indexes.insert(stmt.clone()),
512 "Index would already exist when action {:?} tries to create it:\n {}\nCurrently live indexes:\n{}",
513 pq.action,
514 stmt,
515 live_indexes
516 .iter()
517 .map(|s| format!(" {}", s))
518 .collect::<Vec<_>>()
519 .join("\n")
520 );
521 }
522 }
523
524 for stmt in &stmts {
526 if stmt.starts_with("DROP INDEX") {
527 live_indexes.retain(|s| {
528 let drop_name = stmt
530 .strip_prefix("DROP INDEX \"")
531 .and_then(|s| s.strip_suffix('"'));
532 if let Some(name) = drop_name {
533 !s.contains(&format!("\"{}\"", name))
534 } else {
535 true
536 }
537 });
538 }
539 }
540 }
541 }
542
543 #[rstest]
546 #[case::fk_unique_index("fk_uq_ix", &[fk_constraint(), unique_constraint(), index_constraint()])]
547 #[case::fk_index_unique("fk_ix_uq", &[fk_constraint(), index_constraint(), unique_constraint()])]
548 #[case::unique_fk_index("uq_fk_ix", &[unique_constraint(), fk_constraint(), index_constraint()])]
549 #[case::unique_index_fk("uq_ix_fk", &[unique_constraint(), index_constraint(), fk_constraint()])]
550 #[case::index_fk_unique("ix_fk_uq", &[index_constraint(), fk_constraint(), unique_constraint()])]
551 #[case::index_unique_fk("ix_uq_fk", &[index_constraint(), unique_constraint(), fk_constraint()])]
552 fn test_add_column_with_fk_unique_index_all_orderings(
553 #[case] title: &str,
554 #[case] order: &[TableConstraint],
555 ) {
556 let plan = plan_add_column_with_constraints(order);
557 let schema = base_schema_no_constraints();
558 let result = build_plan_queries(&plan, &schema).unwrap();
559
560 assert_no_duplicate_indexes_per_action(&result);
562 assert_no_orphan_duplicate_indexes(&result);
563
564 for (backend, label) in [
566 (DatabaseBackend::Postgres, "postgres"),
567 (DatabaseBackend::MySql, "mysql"),
568 (DatabaseBackend::Sqlite, "sqlite"),
569 ] {
570 let sql = collect_all_sql(&result, backend);
571 with_settings!({ snapshot_suffix => format!("add_col_{}_{}", title, label) }, {
572 assert_snapshot!(sql);
573 });
574 }
575 }
576
577 #[rstest]
580 #[case::fk_unique_index("fk_uq_ix", &[fk_constraint(), unique_constraint(), index_constraint()])]
581 #[case::fk_index_unique("fk_ix_uq", &[fk_constraint(), index_constraint(), unique_constraint()])]
582 #[case::unique_fk_index("uq_fk_ix", &[unique_constraint(), fk_constraint(), index_constraint()])]
583 #[case::unique_index_fk("uq_ix_fk", &[unique_constraint(), index_constraint(), fk_constraint()])]
584 #[case::index_fk_unique("ix_fk_uq", &[index_constraint(), fk_constraint(), unique_constraint()])]
585 #[case::index_unique_fk("ix_uq_fk", &[index_constraint(), unique_constraint(), fk_constraint()])]
586 fn test_remove_fk_unique_index_then_drop_column_all_orderings(
587 #[case] title: &str,
588 #[case] order: &[TableConstraint],
589 ) {
590 let plan = plan_remove_constraints_then_drop(order);
591 let schema = base_schema_with_all_constraints();
592 let result = build_plan_queries(&plan, &schema).unwrap();
593
594 for (backend, label) in [
596 (DatabaseBackend::Postgres, "postgres"),
597 (DatabaseBackend::MySql, "mysql"),
598 (DatabaseBackend::Sqlite, "sqlite"),
599 ] {
600 let sql = collect_all_sql(&result, backend);
601 with_settings!({ snapshot_suffix => format!("rm_col_{}_{}", title, label) }, {
602 assert_snapshot!(sql);
603 });
604 }
605 }
606
607 #[rstest]
610 #[case::fk_then_index("fk_ix", &[fk_constraint(), index_constraint()])]
611 #[case::index_then_fk("ix_fk", &[index_constraint(), fk_constraint()])]
612 fn test_add_column_with_fk_and_index_pair(
613 #[case] title: &str,
614 #[case] order: &[TableConstraint],
615 ) {
616 let plan = plan_add_column_with_constraints(order);
617 let schema = base_schema_no_constraints();
618 let result = build_plan_queries(&plan, &schema).unwrap();
619
620 assert_no_duplicate_indexes_per_action(&result);
621 assert_no_orphan_duplicate_indexes(&result);
622
623 for (backend, label) in [
624 (DatabaseBackend::Postgres, "postgres"),
625 (DatabaseBackend::MySql, "mysql"),
626 (DatabaseBackend::Sqlite, "sqlite"),
627 ] {
628 let sql = collect_all_sql(&result, backend);
629 with_settings!({ snapshot_suffix => format!("add_col_pair_{}_{}", title, label) }, {
630 assert_snapshot!(sql);
631 });
632 }
633 }
634
635 #[rstest]
638 #[case::fk_then_unique("fk_uq", &[fk_constraint(), unique_constraint()])]
639 #[case::unique_then_fk("uq_fk", &[unique_constraint(), fk_constraint()])]
640 fn test_add_column_with_fk_and_unique_pair(
641 #[case] title: &str,
642 #[case] order: &[TableConstraint],
643 ) {
644 let plan = plan_add_column_with_constraints(order);
645 let schema = base_schema_no_constraints();
646 let result = build_plan_queries(&plan, &schema).unwrap();
647
648 assert_no_duplicate_indexes_per_action(&result);
649 assert_no_orphan_duplicate_indexes(&result);
650
651 for (backend, label) in [
652 (DatabaseBackend::Postgres, "postgres"),
653 (DatabaseBackend::MySql, "mysql"),
654 (DatabaseBackend::Sqlite, "sqlite"),
655 ] {
656 let sql = collect_all_sql(&result, backend);
657 with_settings!({ snapshot_suffix => format!("add_col_pair_{}_{}", title, label) }, {
658 assert_snapshot!(sql);
659 });
660 }
661 }
662
663 #[rstest]
670 #[case::postgres("postgres", DatabaseBackend::Postgres)]
671 #[case::mysql("mysql", DatabaseBackend::MySql)]
672 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
673 fn test_add_column_with_fk_no_duplicate_fk_in_temp_table(
674 #[case] label: &str,
675 #[case] backend: DatabaseBackend,
676 ) {
677 let schema = vec![
678 TableDef {
679 name: "project".into(),
680 description: None,
681 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
682 constraints: vec![],
683 },
684 TableDef {
685 name: "companion".into(),
686 description: None,
687 columns: vec![
688 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
689 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
690 ],
691 constraints: vec![
692 TableConstraint::ForeignKey {
693 name: None,
694 columns: vec!["user_id".into()],
695 ref_table: "user".into(),
696 ref_columns: vec!["id".into()],
697 on_delete: Some(ReferenceAction::Cascade),
698 on_update: None,
699 },
700 TableConstraint::Unique {
701 name: Some("invite_code".into()),
702 columns: vec!["invite_code".into()],
703 },
704 TableConstraint::Index {
705 name: None,
706 columns: vec!["user_id".into()],
707 },
708 ],
709 },
710 ];
711
712 let plan = MigrationPlan {
713 id: String::new(),
714 comment: None,
715 created_at: None,
716 version: 1,
717 actions: vec![
718 MigrationAction::AddColumn {
719 table: "companion".into(),
720 column: Box::new(ColumnDef {
721 name: "project_id".into(),
722 r#type: ColumnType::Simple(SimpleColumnType::BigInt),
723 nullable: false,
724 default: None,
725 comment: None,
726 primary_key: None,
727 unique: None,
728 index: None,
729 foreign_key: Some(
730 vespertide_core::schema::foreign_key::ForeignKeySyntax::String(
731 "project.id".into(),
732 ),
733 ),
734 }),
735 fill_with: None,
736 },
737 MigrationAction::AddConstraint {
738 table: "companion".into(),
739 constraint: TableConstraint::ForeignKey {
740 name: None,
741 columns: vec!["project_id".into()],
742 ref_table: "project".into(),
743 ref_columns: vec!["id".into()],
744 on_delete: Some(ReferenceAction::Cascade),
745 on_update: None,
746 },
747 },
748 MigrationAction::AddConstraint {
749 table: "companion".into(),
750 constraint: TableConstraint::Index {
751 name: None,
752 columns: vec!["project_id".into()],
753 },
754 },
755 ],
756 };
757
758 let result = build_plan_queries(&plan, &schema).unwrap();
759
760 assert_no_duplicate_indexes_per_action(&result);
761 assert_no_orphan_duplicate_indexes(&result);
762
763 let sql = collect_all_sql(&result, backend);
764 with_settings!({ snapshot_suffix => format!("dup_fk_{}", label) }, {
765 assert_snapshot!(sql);
766 });
767 }
768}