Skip to main content

mixtape_tools/sqlite/migration/
remove.rs

1//! Remove migration tool
2
3use crate::prelude::*;
4use crate::sqlite::error::SqliteToolError;
5use crate::sqlite::manager::with_connection;
6
7use super::{ensure_migrations_table, MIGRATIONS_TABLE};
8
9/// Input for removing a pending migration
10#[derive(Debug, Deserialize, JsonSchema)]
11pub struct RemoveMigrationInput {
12    /// The version identifier of the migration to remove
13    pub version: String,
14
15    /// Database to remove the migration from (uses default if not specified)
16    #[serde(default)]
17    pub db_path: Option<String>,
18}
19
20/// Removes a pending migration from the database
21///
22/// Only pending (not yet applied) migrations can be removed.
23/// Applied migrations cannot be removed to maintain schema integrity.
24pub struct RemoveMigrationTool;
25
26impl Tool for RemoveMigrationTool {
27    type Input = RemoveMigrationInput;
28
29    fn name(&self) -> &str {
30        "sqlite_remove_migration"
31    }
32
33    fn description(&self) -> &str {
34        "Remove a pending migration from the database. Only pending (not yet applied) migrations \
35         can be removed. Use sqlite_list_migrations to see pending migrations."
36    }
37
38    async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
39        let version = input.version;
40
41        let name = with_connection(input.db_path, move |conn| {
42            // Ensure migrations table exists
43            ensure_migrations_table(conn)?;
44
45            // Check if migration exists and get its status
46            let query =
47                format!("SELECT name, applied_at FROM {MIGRATIONS_TABLE} WHERE version = ?1");
48
49            let result: Result<(String, Option<String>), _> =
50                conn.query_row(&query, [&version], |row| Ok((row.get(0)?, row.get(1)?)));
51
52            match result {
53                Ok((name, applied_at)) => {
54                    if applied_at.is_some() {
55                        return Err(SqliteToolError::InvalidQuery(format!(
56                            "Cannot remove migration '{}': it has already been applied. \
57                             Applied migrations cannot be removed to maintain schema integrity.",
58                            version
59                        )));
60                    }
61
62                    // Delete the pending migration
63                    conn.execute(
64                        &format!("DELETE FROM {MIGRATIONS_TABLE} WHERE version = ?1"),
65                        [&version],
66                    )?;
67
68                    Ok(name)
69                }
70                Err(rusqlite::Error::QueryReturnedNoRows) => {
71                    Err(SqliteToolError::MigrationNotFound(version))
72                }
73                Err(e) => Err(e.into()),
74            }
75        })
76        .await?;
77
78        Ok(ToolResult::Json(serde_json::json!({
79            "status": "success",
80            "message": format!("Migration '{}' removed", name)
81        })))
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::sqlite::migration::add::AddMigrationInput;
89    use crate::sqlite::migration::run::RunMigrationsInput;
90    use crate::sqlite::migration::{AddMigrationTool, RunMigrationsTool};
91    use crate::sqlite::test_utils::{unwrap_json, TestDatabase};
92
93    #[tokio::test]
94    async fn test_remove_pending_migration() {
95        let db = TestDatabase::new().await;
96
97        // Add a migration
98        let add_tool = AddMigrationTool;
99        let add_result = add_tool
100            .execute(AddMigrationInput {
101                name: "create users table".to_string(),
102                sql: "CREATE TABLE users (id INTEGER PRIMARY KEY);".to_string(),
103                db_path: Some(db.key()),
104            })
105            .await
106            .unwrap();
107
108        let add_json = unwrap_json(add_result);
109        let version = add_json["version"].as_str().unwrap().to_string();
110
111        // Remove the migration
112        let remove_tool = RemoveMigrationTool;
113        let result = remove_tool
114            .execute(RemoveMigrationInput {
115                version: version.clone(),
116                db_path: Some(db.key()),
117            })
118            .await
119            .unwrap();
120
121        let json = unwrap_json(result);
122
123        assert_eq!(json["status"], "success");
124
125        // Verify migration is gone
126        let get_result = crate::sqlite::migration::GetMigrationTool
127            .execute(crate::sqlite::migration::get::GetMigrationInput {
128                version,
129                db_path: Some(db.key()),
130            })
131            .await;
132
133        assert!(get_result.is_err());
134    }
135
136    #[tokio::test]
137    async fn test_cannot_remove_applied_migration() {
138        let db = TestDatabase::new().await;
139
140        // Add and run a migration
141        let add_tool = AddMigrationTool;
142        let add_result = add_tool
143            .execute(AddMigrationInput {
144                name: "create users table".to_string(),
145                sql: "CREATE TABLE users (id INTEGER PRIMARY KEY);".to_string(),
146                db_path: Some(db.key()),
147            })
148            .await
149            .unwrap();
150
151        let add_json = unwrap_json(add_result);
152        let version = add_json["version"].as_str().unwrap().to_string();
153
154        // Apply the migration
155        RunMigrationsTool
156            .execute(RunMigrationsInput {
157                db_path: Some(db.key()),
158            })
159            .await
160            .unwrap();
161
162        // Try to remove it - should fail
163        let remove_tool = RemoveMigrationTool;
164        let result = remove_tool
165            .execute(RemoveMigrationInput {
166                version,
167                db_path: Some(db.key()),
168            })
169            .await;
170
171        assert!(result.is_err());
172        assert!(result
173            .unwrap_err()
174            .to_string()
175            .contains("already been applied"));
176    }
177
178    #[tokio::test]
179    async fn test_remove_nonexistent_migration() {
180        let db = TestDatabase::new().await;
181
182        let tool = RemoveMigrationTool;
183        let result = tool
184            .execute(RemoveMigrationInput {
185                version: "nonexistent".to_string(),
186                db_path: Some(db.key()),
187            })
188            .await;
189
190        assert!(result.is_err());
191        assert!(result
192            .unwrap_err()
193            .to_string()
194            .contains("Migration not found"));
195    }
196}