mixtape_tools/sqlite/migration/
list.rs1use crate::prelude::*;
4use crate::sqlite::manager::with_connection;
5
6use super::types::MigrationStatusFilter;
7use super::{ensure_migrations_table, MIGRATIONS_TABLE};
8
9#[derive(Debug, Deserialize, JsonSchema)]
11pub struct ListMigrationsInput {
12 #[serde(default)]
14 pub filter: MigrationStatusFilter,
15
16 #[serde(default)]
18 pub db_path: Option<String>,
19}
20
21pub struct ListMigrationsTool;
25
26impl Tool for ListMigrationsTool {
27 type Input = ListMigrationsInput;
28
29 fn name(&self) -> &str {
30 "sqlite_list_migrations"
31 }
32
33 fn description(&self) -> &str {
34 "List all schema migrations with their status. Filter by 'pending', 'applied', or 'all'. \
35 Returns migrations ordered by version (oldest first)."
36 }
37
38 async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
39 let filter = input.filter;
40
41 let (migrations, pending_count, applied_count) = with_connection(input.db_path, move |conn| {
42 ensure_migrations_table(conn)?;
44
45 let where_clause = match filter {
47 MigrationStatusFilter::All => String::new(),
48 MigrationStatusFilter::Pending => " WHERE applied_at IS NULL".to_string(),
49 MigrationStatusFilter::Applied => " WHERE applied_at IS NOT NULL".to_string(),
50 };
51
52 let query = format!(
53 "SELECT version, name, applied_at FROM {MIGRATIONS_TABLE}{where_clause} \
54 ORDER BY version ASC"
55 );
56
57 let mut stmt = conn.prepare(&query)?;
58
59 let migrations: Vec<serde_json::Value> = stmt
60 .query_map([], |row| {
61 let version: String = row.get(0)?;
62 let name: String = row.get(1)?;
63 let applied_at: Option<String> = row.get(2)?;
64
65 Ok(serde_json::json!({
66 "version": version,
67 "name": name,
68 "migration_status": if applied_at.is_some() { "applied" } else { "pending" },
69 "applied_at": applied_at
70 }))
71 })?
72 .collect::<Result<Vec<_>, _>>()?;
73
74 let pending_count = migrations
75 .iter()
76 .filter(|m| m["migration_status"] == "pending")
77 .count();
78 let applied_count = migrations.len() - pending_count;
79
80 Ok((migrations, pending_count, applied_count))
81 })
82 .await?;
83
84 Ok(ToolResult::Json(serde_json::json!({
85 "status": "success",
86 "total": migrations.len(),
87 "pending_count": pending_count,
88 "applied_count": applied_count,
89 "migrations": migrations
90 })))
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::sqlite::migration::add::AddMigrationInput;
98 use crate::sqlite::migration::run::RunMigrationsInput;
99 use crate::sqlite::migration::{AddMigrationTool, RunMigrationsTool};
100 use crate::sqlite::test_utils::{unwrap_json, TestDatabase};
101
102 #[tokio::test]
103 async fn test_list_empty() {
104 let db = TestDatabase::new().await;
105
106 let tool = ListMigrationsTool;
107 let result = tool
108 .execute(ListMigrationsInput {
109 filter: MigrationStatusFilter::All,
110 db_path: Some(db.key()),
111 })
112 .await
113 .unwrap();
114
115 let json = unwrap_json(result);
116
117 assert_eq!(json["total"], 0);
118 assert_eq!(json["pending_count"], 0);
119 assert_eq!(json["applied_count"], 0);
120 }
121
122 #[tokio::test]
123 async fn test_list_pending_migrations() {
124 let db = TestDatabase::new().await;
125
126 let add_tool = AddMigrationTool;
128 add_tool
129 .execute(AddMigrationInput {
130 name: "first".to_string(),
131 sql: "CREATE TABLE t1 (id INTEGER);".to_string(),
132 db_path: Some(db.key()),
133 })
134 .await
135 .unwrap();
136
137 add_tool
138 .execute(AddMigrationInput {
139 name: "second".to_string(),
140 sql: "CREATE TABLE t2 (id INTEGER);".to_string(),
141 db_path: Some(db.key()),
142 })
143 .await
144 .unwrap();
145
146 let list_tool = ListMigrationsTool;
148 let result = list_tool
149 .execute(ListMigrationsInput {
150 filter: MigrationStatusFilter::All,
151 db_path: Some(db.key()),
152 })
153 .await
154 .unwrap();
155
156 let json = unwrap_json(result);
157
158 assert_eq!(json["total"], 2);
159 assert_eq!(json["pending_count"], 2);
160 assert_eq!(json["applied_count"], 0);
161
162 let result = list_tool
164 .execute(ListMigrationsInput {
165 filter: MigrationStatusFilter::Pending,
166 db_path: Some(db.key()),
167 })
168 .await
169 .unwrap();
170
171 let json = unwrap_json(result);
172 assert_eq!(json["total"], 2);
173 }
174
175 #[tokio::test]
176 async fn test_list_mixed_status() {
177 let db = TestDatabase::new().await;
178
179 let add_tool = AddMigrationTool;
181 add_tool
182 .execute(AddMigrationInput {
183 name: "first".to_string(),
184 sql: "CREATE TABLE t1 (id INTEGER);".to_string(),
185 db_path: Some(db.key()),
186 })
187 .await
188 .unwrap();
189
190 let run_tool = RunMigrationsTool;
192 run_tool
193 .execute(RunMigrationsInput {
194 db_path: Some(db.key()),
195 })
196 .await
197 .unwrap();
198
199 add_tool
201 .execute(AddMigrationInput {
202 name: "second".to_string(),
203 sql: "CREATE TABLE t2 (id INTEGER);".to_string(),
204 db_path: Some(db.key()),
205 })
206 .await
207 .unwrap();
208
209 let list_tool = ListMigrationsTool;
211 let result = list_tool
212 .execute(ListMigrationsInput {
213 filter: MigrationStatusFilter::All,
214 db_path: Some(db.key()),
215 })
216 .await
217 .unwrap();
218
219 let json = unwrap_json(result);
220
221 assert_eq!(json["total"], 2);
222 assert_eq!(json["pending_count"], 1);
223 assert_eq!(json["applied_count"], 1);
224
225 let result = list_tool
227 .execute(ListMigrationsInput {
228 filter: MigrationStatusFilter::Applied,
229 db_path: Some(db.key()),
230 })
231 .await
232 .unwrap();
233
234 let json = unwrap_json(result);
235 assert_eq!(json["total"], 1);
236 assert_eq!(json["migrations"][0]["name"], "first");
237 assert_eq!(json["migrations"][0]["migration_status"], "applied");
238 }
239}