mixtape_tools/sqlite/migration/
remove.rs1use crate::prelude::*;
4use crate::sqlite::error::SqliteToolError;
5use crate::sqlite::manager::with_connection;
6
7use super::{ensure_migrations_table, MIGRATIONS_TABLE};
8
9#[derive(Debug, Deserialize, JsonSchema)]
11pub struct RemoveMigrationInput {
12 pub version: String,
14
15 #[serde(default)]
17 pub db_path: Option<String>,
18}
19
20pub 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(conn)?;
44
45 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 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 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 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 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 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 RunMigrationsTool
156 .execute(RunMigrationsInput {
157 db_path: Some(db.key()),
158 })
159 .await
160 .unwrap();
161
162 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}