Skip to main content

mixtape_tools/sqlite/migration/
list.rs

1//! List migrations tool
2
3use crate::prelude::*;
4use crate::sqlite::manager::with_connection;
5
6use super::types::MigrationStatusFilter;
7use super::{ensure_migrations_table, MIGRATIONS_TABLE};
8
9/// Input for listing migrations
10#[derive(Debug, Deserialize, JsonSchema)]
11pub struct ListMigrationsInput {
12    /// Filter by migration status (default: all)
13    #[serde(default)]
14    pub filter: MigrationStatusFilter,
15
16    /// Database to list migrations from (uses default if not specified)
17    #[serde(default)]
18    pub db_path: Option<String>,
19}
20
21/// Lists all migrations with their status
22///
23/// Returns migrations ordered by version (oldest first).
24pub 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 exists
43            ensure_migrations_table(conn)?;
44
45            // Build query based on status filter
46            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        // Add migrations but don't run them
127        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        // List all
147        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        // Filter pending only
163        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        // Add first migration
180        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        // Run it
191        let run_tool = RunMigrationsTool;
192        run_tool
193            .execute(RunMigrationsInput {
194                db_path: Some(db.key()),
195            })
196            .await
197            .unwrap();
198
199        // Add second migration (pending)
200        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        // List all
210        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        // Filter applied only
226        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}