Skip to main content

mixtape_tools/sqlite/migration/
add.rs

1//! Add migration tool
2
3use crate::prelude::*;
4use crate::sqlite::manager::with_connection;
5
6use super::{compute_checksum, ensure_migrations_table, generate_version, MIGRATIONS_TABLE};
7
8/// Input for adding a new migration
9#[derive(Debug, Deserialize, JsonSchema)]
10pub struct AddMigrationInput {
11    /// Human-readable description of what this migration does
12    /// Example: "add users table", "add email column to users"
13    pub name: String,
14
15    /// The SQL DDL statement(s) to execute
16    /// Example: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);"
17    pub sql: String,
18
19    /// Database to add the migration to (uses default if not specified)
20    #[serde(default)]
21    pub db_path: Option<String>,
22}
23
24/// Adds a new pending migration to the database
25///
26/// The migration is stored but NOT executed. Use `sqlite_run_migrations` to apply it.
27/// A unique version identifier is automatically generated based on the current timestamp.
28pub struct AddMigrationTool;
29
30impl Tool for AddMigrationTool {
31    type Input = AddMigrationInput;
32
33    fn name(&self) -> &str {
34        "sqlite_add_migration"
35    }
36
37    fn description(&self) -> &str {
38        "Add a new pending schema migration to the database. The migration is stored but not \
39         executed until sqlite_run_migrations is called. Version is auto-generated from timestamp."
40    }
41
42    async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
43        let name = input.name;
44        let sql = input.sql;
45
46        let (version, checksum) = with_connection(input.db_path, move |conn| {
47            // Ensure migrations table exists
48            ensure_migrations_table(conn)?;
49
50            // Generate version and checksum
51            let version = generate_version();
52            let checksum = compute_checksum(&sql);
53
54            // Insert the migration as pending (applied_at = NULL)
55            conn.execute(
56                &format!(
57                    "INSERT INTO {MIGRATIONS_TABLE} (version, name, sql, applied_at, checksum) \
58                     VALUES (?1, ?2, ?3, NULL, ?4)"
59                ),
60                rusqlite::params![version, name, sql, checksum],
61            )?;
62
63            Ok((version, checksum))
64        })
65        .await?;
66
67        Ok(ToolResult::Json(serde_json::json!({
68            "status": "success",
69            "version": version,
70            "checksum": checksum,
71            "migration_status": "pending",
72            "message": "Migration added. Use sqlite_run_migrations to apply it."
73        })))
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::sqlite::test_utils::{unwrap_json, TestDatabase};
81
82    #[tokio::test]
83    async fn test_add_migration() {
84        let db = TestDatabase::new().await;
85
86        let tool = AddMigrationTool;
87        let input = AddMigrationInput {
88            name: "create users table".to_string(),
89            sql: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);".to_string(),
90            db_path: Some(db.key()),
91        };
92
93        let result = tool.execute(input).await.unwrap();
94        let json = unwrap_json(result);
95
96        assert_eq!(json["status"], "success");
97        assert_eq!(json["migration_status"], "pending");
98        assert!(json["version"].as_str().unwrap().len() == 22);
99        assert!(json["checksum"].as_str().unwrap().len() == 64);
100    }
101
102    #[tokio::test]
103    async fn test_add_multiple_migrations() {
104        let db = TestDatabase::new().await;
105        let tool = AddMigrationTool;
106
107        // Add first migration
108        let input1 = AddMigrationInput {
109            name: "create users table".to_string(),
110            sql: "CREATE TABLE users (id INTEGER PRIMARY KEY);".to_string(),
111            db_path: Some(db.key()),
112        };
113        let result1 = tool.execute(input1).await.unwrap();
114        let json1 = unwrap_json(result1);
115        let v1 = json1["version"].as_str().unwrap().to_string();
116
117        // Small delay to ensure different timestamp
118        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
119
120        // Add second migration
121        let input2 = AddMigrationInput {
122            name: "create posts table".to_string(),
123            sql: "CREATE TABLE posts (id INTEGER PRIMARY KEY);".to_string(),
124            db_path: Some(db.key()),
125        };
126        let result2 = tool.execute(input2).await.unwrap();
127        let json2 = unwrap_json(result2);
128        let v2 = json2["version"].as_str().unwrap().to_string();
129
130        // Versions should be different and v2 should be greater (later)
131        assert_ne!(v1, v2);
132        assert!(v2 > v1);
133    }
134}