Skip to main content

boost/tools/
migrations.rs

1//! `list-migrations` — pending vs. applied per the migrations table.
2
3use async_trait::async_trait;
4use serde_json::{json, Value};
5
6use crate::protocol::CallToolResult;
7use crate::tool::{Context, Tool};
8
9pub struct ListMigrations;
10
11#[async_trait]
12impl Tool for ListMigrations {
13    fn name(&self) -> &'static str {
14        "list-migrations"
15    }
16    fn description(&self) -> &'static str {
17        "List migration files and whether each has been applied. Reads the `migrations` table."
18    }
19
20    async fn call(&self, ctx: &Context, _args: Value) -> CallToolResult {
21        let pool = ctx.container.driver_pool();
22        let driver = pool.driver();
23
24        // Pull `name, batch, applied_at` from the migrations table. Handles a
25        // missing table (fresh project) gracefully.
26        let rows: Result<Vec<(String, Option<i64>, Option<chrono::DateTime<chrono::Utc>>)>, _> =
27            match pool {
28                cast_core::Pool::Postgres(p) => sqlx::query_as::<
29                    _,
30                    (String, Option<i64>, Option<chrono::DateTime<chrono::Utc>>),
31                >(
32                    "SELECT name, batch, applied_at FROM migrations ORDER BY id",
33                )
34                .fetch_all(&p)
35                .await
36                .map_err(|e| e.to_string()),
37                cast_core::Pool::MySql(p) => sqlx::query_as::<
38                    _,
39                    (String, Option<i64>, Option<chrono::DateTime<chrono::Utc>>),
40                >(
41                    "SELECT name, batch, applied_at FROM migrations ORDER BY id",
42                )
43                .fetch_all(&p)
44                .await
45                .map_err(|e| e.to_string()),
46                cast_core::Pool::Sqlite(p) => sqlx::query_as::<
47                    _,
48                    (String, Option<i64>, Option<chrono::DateTime<chrono::Utc>>),
49                >(
50                    "SELECT name, batch, applied_at FROM migrations ORDER BY id",
51                )
52                .fetch_all(&p)
53                .await
54                .map_err(|e| e.to_string()),
55            };
56
57        let applied = match rows {
58            Ok(rows) => rows,
59            Err(e) => {
60                return CallToolResult::json(&json!({
61                    "driver": format!("{:?}", driver),
62                    "error": format!("could not read migrations table: {e}"),
63                    "hint": "run `anvil migrate:install` to create the table, or `anvil migrate` to apply pending migrations.",
64                    "applied": [],
65                }));
66            }
67        };
68
69        CallToolResult::json(&json!({
70            "driver": format!("{:?}", driver),
71            "count": applied.len(),
72            "applied": applied.iter().map(|(name, batch, ts)| {
73                json!({
74                    "name": name,
75                    "batch": batch,
76                    "applied_at": ts.map(|t| t.to_rfc3339()),
77                })
78            }).collect::<Vec<_>>(),
79        }))
80    }
81}