vespertide_query/
builder.rs

1use vespertide_core::{MigrationAction, MigrationPlan, TableDef};
2use vespertide_planner::apply_action;
3
4use crate::DatabaseBackend;
5use crate::error::QueryError;
6use crate::sql::{BuiltQuery, build_action_queries};
7
8pub struct PlanQueries {
9    pub action: MigrationAction,
10    pub postgres: Vec<BuiltQuery>,
11    pub mysql: Vec<BuiltQuery>,
12    pub sqlite: Vec<BuiltQuery>,
13}
14
15pub fn build_plan_queries(
16    plan: &MigrationPlan,
17    current_schema: &[TableDef],
18) -> Result<Vec<PlanQueries>, QueryError> {
19    let mut queries: Vec<PlanQueries> = Vec::new();
20    // Clone the schema so we can mutate it as we apply actions
21    let mut evolving_schema = current_schema.to_vec();
22
23    for action in &plan.actions {
24        // Build queries with the current state of the schema
25        let postgres_queries =
26            build_action_queries(&DatabaseBackend::Postgres, action, &evolving_schema)?;
27        let mysql_queries =
28            build_action_queries(&DatabaseBackend::MySql, action, &evolving_schema)?;
29        let sqlite_queries =
30            build_action_queries(&DatabaseBackend::Sqlite, action, &evolving_schema)?;
31        queries.push(PlanQueries {
32            action: action.clone(),
33            postgres: postgres_queries,
34            mysql: mysql_queries,
35            sqlite: sqlite_queries,
36        });
37
38        // Apply the action to update the schema for the next iteration
39        // Note: We ignore errors here because some actions (like DeleteTable) may reference
40        // tables that don't exist in the provided current_schema. This is OK for SQL generation
41        // purposes - we still generate the correct SQL, and the schema evolution is best-effort.
42        let _ = apply_action(&mut evolving_schema, action);
43    }
44    Ok(queries)
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::sql::DatabaseBackend;
51    use rstest::rstest;
52    use vespertide_core::{
53        ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType,
54    };
55
56    fn col(name: &str, ty: ColumnType) -> ColumnDef {
57        ColumnDef {
58            name: name.to_string(),
59            r#type: ty,
60            nullable: true,
61            default: None,
62            comment: None,
63            primary_key: None,
64            unique: None,
65            index: None,
66            foreign_key: None,
67        }
68    }
69
70    #[rstest]
71    #[case::empty(
72        MigrationPlan {
73            comment: None,
74            created_at: None,
75            version: 1,
76            actions: vec![],
77        },
78        0
79    )]
80    #[case::single_action(
81        MigrationPlan {
82            comment: None,
83            created_at: None,
84            version: 1,
85            actions: vec![MigrationAction::DeleteTable {
86                table: "users".into(),
87            }],
88        },
89        1
90    )]
91    #[case::multiple_actions(
92        MigrationPlan {
93            comment: None,
94            created_at: None,
95            version: 1,
96            actions: vec![
97                MigrationAction::CreateTable {
98                    table: "users".into(),
99                    columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
100                    constraints: vec![],
101                },
102                MigrationAction::DeleteTable {
103                    table: "posts".into(),
104                },
105            ],
106        },
107        2
108    )]
109    fn test_build_plan_queries(#[case] plan: MigrationPlan, #[case] expected_count: usize) {
110        let result = build_plan_queries(&plan, &[]).unwrap();
111        assert_eq!(
112            result.len(),
113            expected_count,
114            "Expected {} queries, got {}",
115            expected_count,
116            result.len()
117        );
118    }
119
120    #[test]
121    fn test_build_plan_queries_sql_content() {
122        let plan = MigrationPlan {
123            comment: None,
124            created_at: None,
125            version: 1,
126            actions: vec![
127                MigrationAction::CreateTable {
128                    table: "users".into(),
129                    columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
130                    constraints: vec![],
131                },
132                MigrationAction::DeleteTable {
133                    table: "posts".into(),
134                },
135            ],
136        };
137
138        let result = build_plan_queries(&plan, &[]).unwrap();
139        assert_eq!(result.len(), 2);
140
141        // Test PostgreSQL output
142        let sql1 = result[0]
143            .postgres
144            .iter()
145            .map(|q| q.build(DatabaseBackend::Postgres))
146            .collect::<Vec<_>>()
147            .join(";\n");
148        assert!(sql1.contains("CREATE TABLE"));
149        assert!(sql1.contains("\"users\""));
150        assert!(sql1.contains("\"id\""));
151
152        let sql2 = result[1]
153            .postgres
154            .iter()
155            .map(|q| q.build(DatabaseBackend::Postgres))
156            .collect::<Vec<_>>()
157            .join(";\n");
158        assert!(sql2.contains("DROP TABLE"));
159        assert!(sql2.contains("\"posts\""));
160
161        // Test MySQL output
162        let sql1_mysql = result[0]
163            .mysql
164            .iter()
165            .map(|q| q.build(DatabaseBackend::MySql))
166            .collect::<Vec<_>>()
167            .join(";\n");
168        assert!(sql1_mysql.contains("`users`"));
169
170        let sql2_mysql = result[1]
171            .mysql
172            .iter()
173            .map(|q| q.build(DatabaseBackend::MySql))
174            .collect::<Vec<_>>()
175            .join(";\n");
176        assert!(sql2_mysql.contains("`posts`"));
177    }
178}