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 comment: None,
123 created_at: None,
124 version: 1,
125 actions: vec![],
126 },
127 0
128 )]
129 #[case::single_action(
130 MigrationPlan {
131 comment: None,
132 created_at: None,
133 version: 1,
134 actions: vec![MigrationAction::DeleteTable {
135 table: "users".into(),
136 }],
137 },
138 1
139 )]
140 #[case::multiple_actions(
141 MigrationPlan {
142 comment: None,
143 created_at: None,
144 version: 1,
145 actions: vec![
146 MigrationAction::CreateTable {
147 table: "users".into(),
148 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
149 constraints: vec![],
150 },
151 MigrationAction::DeleteTable {
152 table: "posts".into(),
153 },
154 ],
155 },
156 2
157 )]
158 fn test_build_plan_queries(#[case] plan: MigrationPlan, #[case] expected_count: usize) {
159 let result = build_plan_queries(&plan, &[]).unwrap();
160 assert_eq!(
161 result.len(),
162 expected_count,
163 "Expected {} queries, got {}",
164 expected_count,
165 result.len()
166 );
167 }
168
169 fn build_sql_snapshot(result: &[BuiltQuery], backend: DatabaseBackend) -> String {
170 result
171 .iter()
172 .map(|q| q.build(backend))
173 .collect::<Vec<_>>()
174 .join(";\n")
175 }
176
177 #[rstest]
182 #[case::postgres("postgres", DatabaseBackend::Postgres)]
183 #[case::mysql("mysql", DatabaseBackend::MySql)]
184 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
185 fn test_delete_column_after_create_table_with_inline_unique(
186 #[case] title: &str,
187 #[case] backend: DatabaseBackend,
188 ) {
189 let mut col_with_unique = col("gift_code", ColumnType::Simple(SimpleColumnType::Text));
190 col_with_unique.unique = Some(vespertide_core::StrOrBoolOrArray::Bool(true));
191
192 let plan = MigrationPlan {
193 comment: None,
194 created_at: None,
195 version: 1,
196 actions: vec![
197 MigrationAction::CreateTable {
198 table: "gift".into(),
199 columns: vec![
200 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
201 col_with_unique,
202 ],
203 constraints: vec![], },
205 MigrationAction::DeleteColumn {
206 table: "gift".into(),
207 column: "gift_code".into(),
208 },
209 ],
210 };
211
212 let result = build_plan_queries(&plan, &[]).unwrap();
213 let queries = match backend {
214 DatabaseBackend::Postgres => &result[1].postgres,
215 DatabaseBackend::MySql => &result[1].mysql,
216 DatabaseBackend::Sqlite => &result[1].sqlite,
217 };
218 let sql = build_sql_snapshot(queries, backend);
219
220 with_settings!({ snapshot_suffix => format!("inline_unique_{}", title) }, {
221 assert_snapshot!(sql);
222 });
223 }
224
225 #[rstest]
227 #[case::postgres("postgres", DatabaseBackend::Postgres)]
228 #[case::mysql("mysql", DatabaseBackend::MySql)]
229 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
230 fn test_delete_column_after_create_table_with_inline_index(
231 #[case] title: &str,
232 #[case] backend: DatabaseBackend,
233 ) {
234 let mut col_with_index = col("email", ColumnType::Simple(SimpleColumnType::Text));
235 col_with_index.index = Some(vespertide_core::StrOrBoolOrArray::Bool(true));
236
237 let plan = MigrationPlan {
238 comment: None,
239 created_at: None,
240 version: 1,
241 actions: vec![
242 MigrationAction::CreateTable {
243 table: "users".into(),
244 columns: vec![
245 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
246 col_with_index,
247 ],
248 constraints: vec![],
249 },
250 MigrationAction::DeleteColumn {
251 table: "users".into(),
252 column: "email".into(),
253 },
254 ],
255 };
256
257 let result = build_plan_queries(&plan, &[]).unwrap();
258 let queries = match backend {
259 DatabaseBackend::Postgres => &result[1].postgres,
260 DatabaseBackend::MySql => &result[1].mysql,
261 DatabaseBackend::Sqlite => &result[1].sqlite,
262 };
263 let sql = build_sql_snapshot(queries, backend);
264
265 with_settings!({ snapshot_suffix => format!("inline_index_{}", title) }, {
266 assert_snapshot!(sql);
267 });
268 }
269
270 #[test]
271 fn test_build_plan_queries_sql_content() {
272 let plan = MigrationPlan {
273 comment: None,
274 created_at: None,
275 version: 1,
276 actions: vec![
277 MigrationAction::CreateTable {
278 table: "users".into(),
279 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
280 constraints: vec![],
281 },
282 MigrationAction::DeleteTable {
283 table: "posts".into(),
284 },
285 ],
286 };
287
288 let result = build_plan_queries(&plan, &[]).unwrap();
289 assert_eq!(result.len(), 2);
290
291 let sql1 = result[0]
293 .postgres
294 .iter()
295 .map(|q| q.build(DatabaseBackend::Postgres))
296 .collect::<Vec<_>>()
297 .join(";\n");
298 assert!(sql1.contains("CREATE TABLE"));
299 assert!(sql1.contains("\"users\""));
300 assert!(sql1.contains("\"id\""));
301
302 let sql2 = result[1]
303 .postgres
304 .iter()
305 .map(|q| q.build(DatabaseBackend::Postgres))
306 .collect::<Vec<_>>()
307 .join(";\n");
308 assert!(sql2.contains("DROP TABLE"));
309 assert!(sql2.contains("\"posts\""));
310
311 let sql1_mysql = result[0]
313 .mysql
314 .iter()
315 .map(|q| q.build(DatabaseBackend::MySql))
316 .collect::<Vec<_>>()
317 .join(";\n");
318 assert!(sql1_mysql.contains("`users`"));
319
320 let sql2_mysql = result[1]
321 .mysql
322 .iter()
323 .map(|q| q.build(DatabaseBackend::MySql))
324 .collect::<Vec<_>>()
325 .join(";\n");
326 assert!(sql2_mysql.contains("`posts`"));
327 }
328
329 use vespertide_core::{ReferenceAction, TableConstraint};
332
333 fn fk_constraint() -> TableConstraint {
334 TableConstraint::ForeignKey {
335 name: None,
336 columns: vec!["category_id".into()],
337 ref_table: "category".into(),
338 ref_columns: vec!["id".into()],
339 on_delete: Some(ReferenceAction::Cascade),
340 on_update: None,
341 }
342 }
343
344 fn unique_constraint() -> TableConstraint {
345 TableConstraint::Unique {
346 name: None,
347 columns: vec!["category_id".into()],
348 }
349 }
350
351 fn index_constraint() -> TableConstraint {
352 TableConstraint::Index {
353 name: None,
354 columns: vec!["category_id".into()],
355 }
356 }
357
358 fn plan_add_column_with_constraints(order: &[TableConstraint]) -> MigrationPlan {
360 let mut actions: Vec<MigrationAction> = vec![MigrationAction::AddColumn {
361 table: "product".into(),
362 column: Box::new(col(
363 "category_id",
364 ColumnType::Simple(SimpleColumnType::BigInt),
365 )),
366 fill_with: None,
367 }];
368 for c in order {
369 actions.push(MigrationAction::AddConstraint {
370 table: "product".into(),
371 constraint: c.clone(),
372 });
373 }
374 MigrationPlan {
375 comment: None,
376 created_at: None,
377 version: 1,
378 actions,
379 }
380 }
381
382 fn plan_remove_constraints_then_drop(order: &[TableConstraint]) -> MigrationPlan {
384 let mut actions: Vec<MigrationAction> = Vec::new();
385 for c in order {
386 actions.push(MigrationAction::RemoveConstraint {
387 table: "product".into(),
388 constraint: c.clone(),
389 });
390 }
391 actions.push(MigrationAction::DeleteColumn {
392 table: "product".into(),
393 column: "category_id".into(),
394 });
395 MigrationPlan {
396 comment: None,
397 created_at: None,
398 version: 1,
399 actions,
400 }
401 }
402
403 fn base_schema_no_constraints() -> Vec<TableDef> {
405 vec![TableDef {
406 name: "product".into(),
407 description: None,
408 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
409 constraints: vec![],
410 }]
411 }
412
413 fn base_schema_with_all_constraints() -> Vec<TableDef> {
415 vec![TableDef {
416 name: "product".into(),
417 description: None,
418 columns: vec![
419 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
420 col("category_id", ColumnType::Simple(SimpleColumnType::BigInt)),
421 ],
422 constraints: vec![fk_constraint(), unique_constraint(), index_constraint()],
423 }]
424 }
425
426 fn collect_all_sql(result: &[PlanQueries], backend: DatabaseBackend) -> String {
428 result
429 .iter()
430 .enumerate()
431 .map(|(i, pq)| {
432 let queries = match backend {
433 DatabaseBackend::Postgres => &pq.postgres,
434 DatabaseBackend::MySql => &pq.mysql,
435 DatabaseBackend::Sqlite => &pq.sqlite,
436 };
437 let sql = build_sql_snapshot(queries, backend);
438 format!("-- Action {}: {:?}\n{}", i, pq.action, sql)
439 })
440 .collect::<Vec<_>>()
441 .join("\n\n")
442 }
443
444 fn assert_no_duplicate_indexes_per_action(result: &[PlanQueries]) {
449 for (i, pq) in result.iter().enumerate() {
450 let stmts: Vec<String> = pq
451 .sqlite
452 .iter()
453 .map(|q| q.build(DatabaseBackend::Sqlite))
454 .collect();
455
456 let index_stmts: Vec<&String> = stmts
457 .iter()
458 .filter(|s| s.contains("CREATE INDEX") || s.contains("CREATE UNIQUE INDEX"))
459 .collect();
460
461 let mut seen = std::collections::HashSet::new();
462 for stmt in &index_stmts {
463 assert!(
464 seen.insert(stmt.as_str()),
465 "Duplicate index within action {} ({:?}):\n {}\nAll index statements in this action:\n{}",
466 i,
467 pq.action,
468 stmt,
469 index_stmts
470 .iter()
471 .map(|s| format!(" {}", s))
472 .collect::<Vec<_>>()
473 .join("\n")
474 );
475 }
476 }
477 }
478
479 fn assert_no_orphan_duplicate_indexes(result: &[PlanQueries]) {
484 let mut live_indexes: std::collections::HashSet<String> = std::collections::HashSet::new();
487
488 for pq in result {
489 let stmts: Vec<String> = pq
490 .sqlite
491 .iter()
492 .map(|q| q.build(DatabaseBackend::Sqlite))
493 .collect();
494
495 if stmts.iter().any(|s| s.starts_with("DROP TABLE")) {
497 live_indexes.clear();
498 }
499
500 for stmt in &stmts {
501 if stmt.contains("CREATE INDEX") || stmt.contains("CREATE UNIQUE INDEX") {
502 assert!(
503 live_indexes.insert(stmt.clone()),
504 "Index would already exist when action {:?} tries to create it:\n {}\nCurrently live indexes:\n{}",
505 pq.action,
506 stmt,
507 live_indexes
508 .iter()
509 .map(|s| format!(" {}", s))
510 .collect::<Vec<_>>()
511 .join("\n")
512 );
513 }
514 }
515
516 for stmt in &stmts {
518 if stmt.starts_with("DROP INDEX") {
519 live_indexes.retain(|s| {
520 let drop_name = stmt
522 .strip_prefix("DROP INDEX \"")
523 .and_then(|s| s.strip_suffix('"'));
524 if let Some(name) = drop_name {
525 !s.contains(&format!("\"{}\"", name))
526 } else {
527 true
528 }
529 });
530 }
531 }
532 }
533 }
534
535 #[rstest]
538 #[case::fk_unique_index("fk_uq_ix", &[fk_constraint(), unique_constraint(), index_constraint()])]
539 #[case::fk_index_unique("fk_ix_uq", &[fk_constraint(), index_constraint(), unique_constraint()])]
540 #[case::unique_fk_index("uq_fk_ix", &[unique_constraint(), fk_constraint(), index_constraint()])]
541 #[case::unique_index_fk("uq_ix_fk", &[unique_constraint(), index_constraint(), fk_constraint()])]
542 #[case::index_fk_unique("ix_fk_uq", &[index_constraint(), fk_constraint(), unique_constraint()])]
543 #[case::index_unique_fk("ix_uq_fk", &[index_constraint(), unique_constraint(), fk_constraint()])]
544 fn test_add_column_with_fk_unique_index_all_orderings(
545 #[case] title: &str,
546 #[case] order: &[TableConstraint],
547 ) {
548 let plan = plan_add_column_with_constraints(order);
549 let schema = base_schema_no_constraints();
550 let result = build_plan_queries(&plan, &schema).unwrap();
551
552 assert_no_duplicate_indexes_per_action(&result);
554 assert_no_orphan_duplicate_indexes(&result);
555
556 for (backend, label) in [
558 (DatabaseBackend::Postgres, "postgres"),
559 (DatabaseBackend::MySql, "mysql"),
560 (DatabaseBackend::Sqlite, "sqlite"),
561 ] {
562 let sql = collect_all_sql(&result, backend);
563 with_settings!({ snapshot_suffix => format!("add_col_{}_{}", title, label) }, {
564 assert_snapshot!(sql);
565 });
566 }
567 }
568
569 #[rstest]
572 #[case::fk_unique_index("fk_uq_ix", &[fk_constraint(), unique_constraint(), index_constraint()])]
573 #[case::fk_index_unique("fk_ix_uq", &[fk_constraint(), index_constraint(), unique_constraint()])]
574 #[case::unique_fk_index("uq_fk_ix", &[unique_constraint(), fk_constraint(), index_constraint()])]
575 #[case::unique_index_fk("uq_ix_fk", &[unique_constraint(), index_constraint(), fk_constraint()])]
576 #[case::index_fk_unique("ix_fk_uq", &[index_constraint(), fk_constraint(), unique_constraint()])]
577 #[case::index_unique_fk("ix_uq_fk", &[index_constraint(), unique_constraint(), fk_constraint()])]
578 fn test_remove_fk_unique_index_then_drop_column_all_orderings(
579 #[case] title: &str,
580 #[case] order: &[TableConstraint],
581 ) {
582 let plan = plan_remove_constraints_then_drop(order);
583 let schema = base_schema_with_all_constraints();
584 let result = build_plan_queries(&plan, &schema).unwrap();
585
586 for (backend, label) in [
588 (DatabaseBackend::Postgres, "postgres"),
589 (DatabaseBackend::MySql, "mysql"),
590 (DatabaseBackend::Sqlite, "sqlite"),
591 ] {
592 let sql = collect_all_sql(&result, backend);
593 with_settings!({ snapshot_suffix => format!("rm_col_{}_{}", title, label) }, {
594 assert_snapshot!(sql);
595 });
596 }
597 }
598
599 #[rstest]
602 #[case::fk_then_index("fk_ix", &[fk_constraint(), index_constraint()])]
603 #[case::index_then_fk("ix_fk", &[index_constraint(), fk_constraint()])]
604 fn test_add_column_with_fk_and_index_pair(
605 #[case] title: &str,
606 #[case] order: &[TableConstraint],
607 ) {
608 let plan = plan_add_column_with_constraints(order);
609 let schema = base_schema_no_constraints();
610 let result = build_plan_queries(&plan, &schema).unwrap();
611
612 assert_no_duplicate_indexes_per_action(&result);
613 assert_no_orphan_duplicate_indexes(&result);
614
615 for (backend, label) in [
616 (DatabaseBackend::Postgres, "postgres"),
617 (DatabaseBackend::MySql, "mysql"),
618 (DatabaseBackend::Sqlite, "sqlite"),
619 ] {
620 let sql = collect_all_sql(&result, backend);
621 with_settings!({ snapshot_suffix => format!("add_col_pair_{}_{}", title, label) }, {
622 assert_snapshot!(sql);
623 });
624 }
625 }
626
627 #[rstest]
630 #[case::fk_then_unique("fk_uq", &[fk_constraint(), unique_constraint()])]
631 #[case::unique_then_fk("uq_fk", &[unique_constraint(), fk_constraint()])]
632 fn test_add_column_with_fk_and_unique_pair(
633 #[case] title: &str,
634 #[case] order: &[TableConstraint],
635 ) {
636 let plan = plan_add_column_with_constraints(order);
637 let schema = base_schema_no_constraints();
638 let result = build_plan_queries(&plan, &schema).unwrap();
639
640 assert_no_duplicate_indexes_per_action(&result);
641 assert_no_orphan_duplicate_indexes(&result);
642
643 for (backend, label) in [
644 (DatabaseBackend::Postgres, "postgres"),
645 (DatabaseBackend::MySql, "mysql"),
646 (DatabaseBackend::Sqlite, "sqlite"),
647 ] {
648 let sql = collect_all_sql(&result, backend);
649 with_settings!({ snapshot_suffix => format!("add_col_pair_{}_{}", title, label) }, {
650 assert_snapshot!(sql);
651 });
652 }
653 }
654
655 #[rstest]
662 #[case::postgres("postgres", DatabaseBackend::Postgres)]
663 #[case::mysql("mysql", DatabaseBackend::MySql)]
664 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
665 fn test_add_column_with_fk_no_duplicate_fk_in_temp_table(
666 #[case] label: &str,
667 #[case] backend: DatabaseBackend,
668 ) {
669 let schema = vec![
670 TableDef {
671 name: "project".into(),
672 description: None,
673 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
674 constraints: vec![],
675 },
676 TableDef {
677 name: "companion".into(),
678 description: None,
679 columns: vec![
680 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
681 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
682 ],
683 constraints: vec![
684 TableConstraint::ForeignKey {
685 name: None,
686 columns: vec!["user_id".into()],
687 ref_table: "user".into(),
688 ref_columns: vec!["id".into()],
689 on_delete: Some(ReferenceAction::Cascade),
690 on_update: None,
691 },
692 TableConstraint::Unique {
693 name: Some("invite_code".into()),
694 columns: vec!["invite_code".into()],
695 },
696 TableConstraint::Index {
697 name: None,
698 columns: vec!["user_id".into()],
699 },
700 ],
701 },
702 ];
703
704 let plan = MigrationPlan {
705 comment: None,
706 created_at: None,
707 version: 1,
708 actions: vec![
709 MigrationAction::AddColumn {
710 table: "companion".into(),
711 column: Box::new(ColumnDef {
712 name: "project_id".into(),
713 r#type: ColumnType::Simple(SimpleColumnType::BigInt),
714 nullable: false,
715 default: None,
716 comment: None,
717 primary_key: None,
718 unique: None,
719 index: None,
720 foreign_key: Some(
721 vespertide_core::schema::foreign_key::ForeignKeySyntax::String(
722 "project.id".into(),
723 ),
724 ),
725 }),
726 fill_with: None,
727 },
728 MigrationAction::AddConstraint {
729 table: "companion".into(),
730 constraint: TableConstraint::ForeignKey {
731 name: None,
732 columns: vec!["project_id".into()],
733 ref_table: "project".into(),
734 ref_columns: vec!["id".into()],
735 on_delete: Some(ReferenceAction::Cascade),
736 on_update: None,
737 },
738 },
739 MigrationAction::AddConstraint {
740 table: "companion".into(),
741 constraint: TableConstraint::Index {
742 name: None,
743 columns: vec!["project_id".into()],
744 },
745 },
746 ],
747 };
748
749 let result = build_plan_queries(&plan, &schema).unwrap();
750
751 assert_no_duplicate_indexes_per_action(&result);
752 assert_no_orphan_duplicate_indexes(&result);
753
754 let sql = collect_all_sql(&result, backend);
755 with_settings!({ snapshot_suffix => format!("dup_fk_{}", label) }, {
756 assert_snapshot!(sql);
757 });
758 }
759}