mixtape_tools/sqlite/migration/
add.rs1use crate::prelude::*;
4use crate::sqlite::manager::with_connection;
5
6use super::{compute_checksum, ensure_migrations_table, generate_version, MIGRATIONS_TABLE};
7
8#[derive(Debug, Deserialize, JsonSchema)]
10pub struct AddMigrationInput {
11 pub name: String,
14
15 pub sql: String,
18
19 #[serde(default)]
21 pub db_path: Option<String>,
22}
23
24pub 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(conn)?;
49
50 let version = generate_version();
52 let checksum = compute_checksum(&sql);
53
54 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 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 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
119
120 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 assert_ne!(v1, v2);
132 assert!(v2 > v1);
133 }
134}