mixtape_tools/sqlite/migration/
export.rs1use crate::prelude::*;
4use crate::sqlite::manager::with_connection;
5
6use super::{ensure_migrations_table, Migration, MigrationStatusFilter, MIGRATIONS_TABLE};
7
8#[derive(Debug, Deserialize, JsonSchema)]
10pub struct ExportMigrationsInput {
11 #[serde(default)]
13 pub db_path: Option<String>,
14
15 #[serde(default)]
17 pub filter: MigrationStatusFilter,
18
19 #[serde(default)]
21 pub format: ExportFormat,
22}
23
24#[derive(Debug, Default, Clone, Deserialize, JsonSchema)]
26#[serde(rename_all = "lowercase")]
27pub enum ExportFormat {
28 #[default]
30 Json,
31 Sql,
33}
34
35pub struct ExportMigrationsTool;
41
42impl Tool for ExportMigrationsTool {
43 type Input = ExportMigrationsInput;
44
45 fn name(&self) -> &str {
46 "sqlite_export_migrations"
47 }
48
49 fn description(&self) -> &str {
50 "Export migrations from the database for transfer to another database. \
51 Exports migration records (version, name, SQL, status) that can be imported \
52 elsewhere using sqlite_import_migrations. Use filter to export only pending \
53 or applied migrations."
54 }
55
56 async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
57 let filter = input.filter;
58 let format = input.format;
59
60 let migrations = with_connection(input.db_path, move |conn| {
61 ensure_migrations_table(conn)?;
62
63 let query = match filter {
65 MigrationStatusFilter::All => {
66 format!("SELECT version, name, sql, applied_at, checksum FROM {MIGRATIONS_TABLE} ORDER BY version")
67 }
68 MigrationStatusFilter::Pending => {
69 format!("SELECT version, name, sql, applied_at, checksum FROM {MIGRATIONS_TABLE} WHERE applied_at IS NULL ORDER BY version")
70 }
71 MigrationStatusFilter::Applied => {
72 format!("SELECT version, name, sql, applied_at, checksum FROM {MIGRATIONS_TABLE} WHERE applied_at IS NOT NULL ORDER BY version")
73 }
74 };
75
76 let mut stmt = conn.prepare(&query)?;
77 let migrations: Vec<Migration> = stmt
78 .query_map([], |row| {
79 Ok(Migration {
80 version: row.get(0)?,
81 name: row.get(1)?,
82 sql: row.get(2)?,
83 applied_at: row.get(3)?,
84 checksum: row.get(4)?,
85 })
86 })?
87 .filter_map(|r| r.ok())
88 .collect();
89
90 Ok(migrations)
91 })
92 .await?;
93
94 let count = migrations.len();
95 let output = match format {
96 ExportFormat::Json => {
97 serde_json::json!({
98 "status": "success",
99 "format": "json",
100 "count": count,
101 "migrations": migrations
102 })
103 }
104 ExportFormat::Sql => {
105 let mut sql = String::new();
107 sql.push_str("-- Exported migrations\n");
108 sql.push_str("-- Import using sqlite_import_migrations tool\n\n");
109
110 for m in &migrations {
111 sql.push_str(&format!("-- Migration: {} ({})\n", m.name, m.version));
112 sql.push_str(&format!("-- Checksum: {}\n", m.checksum));
113 if let Some(applied) = &m.applied_at {
114 sql.push_str(&format!("-- Applied: {}\n", applied));
115 } else {
116 sql.push_str("-- Status: pending\n");
117 }
118 sql.push_str(&m.sql);
119 if !m.sql.ends_with(';') {
120 sql.push(';');
121 }
122 sql.push_str("\n\n");
123 }
124
125 serde_json::json!({
126 "status": "success",
127 "format": "sql",
128 "count": count,
129 "script": sql
130 })
131 }
132 };
133 Ok(ToolResult::Json(output))
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::sqlite::migration::add::AddMigrationInput;
141 use crate::sqlite::migration::run::RunMigrationsInput;
142 use crate::sqlite::migration::{AddMigrationTool, RunMigrationsTool};
143 use crate::sqlite::test_utils::{unwrap_json, TestDatabase};
144
145 #[tokio::test]
146 async fn test_export_all_migrations() {
147 let db = TestDatabase::new().await;
148
149 let add_tool = AddMigrationTool;
151 add_tool
152 .execute(AddMigrationInput {
153 name: "create users".to_string(),
154 sql: "CREATE TABLE users (id INTEGER PRIMARY KEY);".to_string(),
155 db_path: Some(db.key()),
156 })
157 .await
158 .unwrap();
159
160 add_tool
161 .execute(AddMigrationInput {
162 name: "create posts".to_string(),
163 sql: "CREATE TABLE posts (id INTEGER PRIMARY KEY);".to_string(),
164 db_path: Some(db.key()),
165 })
166 .await
167 .unwrap();
168
169 RunMigrationsTool
171 .execute(RunMigrationsInput {
172 db_path: Some(db.key()),
173 })
174 .await
175 .unwrap();
176
177 let tool = ExportMigrationsTool;
179 let result = tool
180 .execute(ExportMigrationsInput {
181 db_path: Some(db.key()),
182 filter: MigrationStatusFilter::All,
183 format: ExportFormat::Json,
184 })
185 .await
186 .unwrap();
187
188 let json = unwrap_json(result);
189
190 assert_eq!(json["status"], "success");
191 assert_eq!(json["count"], 2);
192 assert_eq!(json["migrations"].as_array().unwrap().len(), 2);
193 }
194
195 #[tokio::test]
196 async fn test_export_pending_only() {
197 let db = TestDatabase::new().await;
198
199 let add_tool = AddMigrationTool;
201 add_tool
202 .execute(AddMigrationInput {
203 name: "create users".to_string(),
204 sql: "CREATE TABLE users (id INTEGER PRIMARY KEY);".to_string(),
205 db_path: Some(db.key()),
206 })
207 .await
208 .unwrap();
209
210 RunMigrationsTool
212 .execute(RunMigrationsInput {
213 db_path: Some(db.key()),
214 })
215 .await
216 .unwrap();
217
218 add_tool
220 .execute(AddMigrationInput {
221 name: "create posts".to_string(),
222 sql: "CREATE TABLE posts (id INTEGER PRIMARY KEY);".to_string(),
223 db_path: Some(db.key()),
224 })
225 .await
226 .unwrap();
227
228 let tool = ExportMigrationsTool;
230 let result = tool
231 .execute(ExportMigrationsInput {
232 db_path: Some(db.key()),
233 filter: MigrationStatusFilter::Pending,
234 format: ExportFormat::Json,
235 })
236 .await
237 .unwrap();
238
239 let json = unwrap_json(result);
240
241 assert_eq!(json["count"], 1);
242 let migrations = json["migrations"].as_array().unwrap();
243 assert_eq!(migrations[0]["name"], "create posts");
244 }
245
246 #[tokio::test]
247 async fn test_export_sql_format() {
248 let db = TestDatabase::new().await;
249
250 let add_tool = AddMigrationTool;
251 add_tool
252 .execute(AddMigrationInput {
253 name: "create users".to_string(),
254 sql: "CREATE TABLE users (id INTEGER PRIMARY KEY)".to_string(),
255 db_path: Some(db.key()),
256 })
257 .await
258 .unwrap();
259
260 let tool = ExportMigrationsTool;
261 let result = tool
262 .execute(ExportMigrationsInput {
263 db_path: Some(db.key()),
264 filter: MigrationStatusFilter::All,
265 format: ExportFormat::Sql,
266 })
267 .await
268 .unwrap();
269
270 let json = unwrap_json(result);
271
272 assert_eq!(json["format"], "sql");
273 let script = json["script"].as_str().unwrap();
274 assert!(script.contains("CREATE TABLE users"));
275 assert!(script.contains("Migration: create users"));
276 }
277
278 #[tokio::test]
279 async fn test_export_empty() {
280 let db = TestDatabase::new().await;
281
282 let tool = ExportMigrationsTool;
283 let result = tool
284 .execute(ExportMigrationsInput {
285 db_path: Some(db.key()),
286 filter: MigrationStatusFilter::All,
287 format: ExportFormat::Json,
288 })
289 .await
290 .unwrap();
291
292 let json = unwrap_json(result);
293
294 assert_eq!(json["status"], "success");
295 assert_eq!(json["count"], 0);
296 }
297}