backbone_orm/
migrations.rs1use anyhow::Result;
4use sqlx::{PgPool, Row};
5use std::fs;
6use std::path::Path;
7use colored::*;
8use chrono::{DateTime, Utc};
9
10pub struct MigrationManager {
12 pool: PgPool,
13}
14
15impl MigrationManager {
16 pub fn new(pool: PgPool) -> Self {
17 Self { pool }
18 }
19
20 pub async fn migrate(&self) -> Result<()> {
22 println!("đ Running database migrations...");
23
24 self.create_migrations_table().await?;
26
27 let migrations = self.load_migration_files()?;
29
30 let applied_migrations = self.get_applied_migrations().await?;
32
33 for migration in migrations {
35 if !applied_migrations.contains(&migration.name) {
36 println!(" âŗ Applying migration: {}", migration.name.bright_green());
37 self.apply_migration(&migration).await?;
38 } else {
39 println!(" â Skipping already applied: {}", migration.name);
40 }
41 }
42
43 println!("â
Migrations completed successfully");
44 Ok(())
45 }
46
47 pub async fn rollback(&self) -> Result<()> {
49 println!("đ Rolling back last migration...");
50
51 self.create_migrations_table().await?;
53
54 let last_migration = self.get_last_applied_migration().await?;
56
57 match last_migration {
58 Some(migration_name) => {
59 println!(" âŗ Rolling back migration: {}", migration_name.bright_yellow());
60
61 let down_migration = self.find_down_migration(&migration_name)?;
63
64 if let Some(down_content) = down_migration {
65 self.rollback_migration(&migration_name, &down_content).await?;
66 println!(" â Successfully rolled back: {}", migration_name.bright_green());
67 } else {
68 println!(" â ī¸ No down migration found for: {}", migration_name.yellow());
69 }
70 }
71 None => {
72 println!(" âšī¸ No migrations to rollback");
73 }
74 }
75
76 println!("â
Rollback completed");
77 Ok(())
78 }
79
80 pub async fn rollback_n(&self, count: usize) -> Result<()> {
82 println!("đ Rolling back {} migrations...", count);
83
84 for i in 0..count {
85 let last_migration = self.get_last_applied_migration().await?;
86
87 match last_migration {
88 Some(migration_name) => {
89 println!(" âŗ Rolling back {}/1: {}", i + 1, migration_name.bright_yellow());
90
91 let down_migration = self.find_down_migration(&migration_name)?;
92
93 if let Some(down_content) = down_migration {
94 self.rollback_migration(&migration_name, &down_content).await?;
95 println!(" â Successfully rolled back: {}", migration_name.bright_green());
96 } else {
97 println!(" â ī¸ No down migration found for: {}", migration_name.yellow());
98 break;
99 }
100 }
101 None => {
102 println!(" âšī¸ No more migrations to rollback");
103 break;
104 }
105 }
106 }
107
108 println!("â
Rollback of {} migrations completed", count);
109 Ok(())
110 }
111
112 pub async fn history(&self) -> Result<Vec<MigrationRecord>> {
114 self.create_migrations_table().await?;
116
117 let rows = sqlx::query("SELECT id, name, applied_at FROM schema_migrations ORDER BY applied_at DESC")
118 .fetch_all(&self.pool)
119 .await?;
120
121 let records: Result<Vec<MigrationRecord>, sqlx::Error> = rows.into_iter()
122 .map(|row| {
123 Ok(MigrationRecord {
124 id: row.get("id"),
125 name: row.get("name"),
126 applied_at: row.get("applied_at"),
127 })
128 })
129 .collect();
130
131 match records {
132 Ok(history) => {
133 if history.is_empty() {
134 println!(" âšī¸ No migrations have been applied yet");
135 } else {
136 println!("đ Migration History:");
137 for (i, record) in history.iter().enumerate() {
138 let status = if i == 0 {
139 "đš".to_string() } else {
141 " ".to_string()
142 };
143 println!(" {} {} - Applied at {}", status, record.name.bright_cyan(), record.applied_at.format("%Y-%m-%d %H:%M:%S"));
144 }
145 }
146 Ok(history)
147 }
148 Err(e) => Err(anyhow::anyhow!("Failed to load migration history: {}", e))
149 }
150 }
151
152 pub async fn status(&self) -> Result<MigrationStatus> {
154 self.create_migrations_table().await?;
156
157 let migration_files = self.load_migration_files()?;
159
160 let applied_migrations = self.get_applied_migrations().await?;
162
163 let mut pending_migrations = Vec::new();
165 let mut applied_migration_details = Vec::new();
166
167 for migration_file in migration_files {
168 if applied_migrations.contains(&migration_file.name) {
169 applied_migration_details.push(migration_file.name);
170 } else {
171 pending_migrations.push(migration_file.name);
172 }
173 }
174
175 let status = MigrationStatus {
176 total_migrations: applied_migration_details.len() + pending_migrations.len(),
177 applied_migrations: applied_migration_details,
178 pending_migrations,
179 last_migration: applied_migrations.last().cloned(),
180 };
181
182 println!("đ Migration Status:");
184 println!(" Total migrations: {}", status.total_migrations);
185 println!(" Applied migrations: {}", status.applied_migrations.len());
186 println!(" Pending migrations: {}", status.pending_migrations.len());
187
188 if !status.pending_migrations.is_empty() {
189 println!(" Pending:");
190 for migration in &status.pending_migrations {
191 println!(" - {}", migration.yellow());
192 }
193 }
194
195 Ok(status)
196 }
197
198 async fn get_last_applied_migration(&self) -> Result<Option<String>> {
200 let row = sqlx::query("SELECT name FROM schema_migrations ORDER BY applied_at DESC LIMIT 1")
201 .fetch_optional(&self.pool)
202 .await?;
203
204 Ok(row.map(|r| r.get("name")))
205 }
206
207 fn find_down_migration(&self, migration_name: &str) -> Result<Option<String>> {
214 let path = Path::new("migrations").join(format!("{}.down.sql", migration_name));
215 if path.exists() {
216 Ok(Some(fs::read_to_string(&path)?))
217 } else {
218 Ok(None)
219 }
220 }
221
222 async fn rollback_migration(&self, migration_name: &str, down_content: &str) -> Result<()> {
229 let mut tx = self.pool.begin().await?;
230
231 sqlx::raw_sql(down_content)
232 .execute(&mut *tx)
233 .await?;
234
235 sqlx::query("DELETE FROM schema_migrations WHERE name = $1")
236 .bind(migration_name)
237 .execute(&mut *tx)
238 .await?;
239
240 tx.commit().await?;
241 Ok(())
242 }
243
244 pub async fn create_migration(&self, name: &str) -> Result<()> {
249 println!("đ Creating new migration: {}", name.bright_cyan());
250
251 let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S");
252 let stem = format!("{}_{}", timestamp, name);
253 let migrations_dir = "migrations";
254 fs::create_dir_all(migrations_dir)?;
255
256 let up_content = format!(
257 r#"-- Migration: {name}
258-- Created: {timestamp}
259
260-- Add your migration SQL here
261-- Example:
262-- CREATE TABLE example_table (
263-- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
264-- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
265-- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
266-- );
267"#,
268 name = name,
269 timestamp = timestamp
270 );
271 let up_path = Path::new(migrations_dir).join(format!("{}.up.sql", stem));
272 fs::write(&up_path, up_content)?;
273
274 let down_content = format!(
275 r#"-- Down Migration: {name}
276-- Created: {timestamp}
277
278-- Add your rollback SQL here
279-- Example:
280-- DROP TABLE IF EXISTS example_table CASCADE;
281"#,
282 name = name,
283 timestamp = timestamp
284 );
285 let down_path = Path::new(migrations_dir).join(format!("{}.down.sql", stem));
286 fs::write(&down_path, down_content)?;
287
288 println!(" â Created: {}", up_path.display().to_string().bright_green());
289 println!(" â Created: {}", down_path.display().to_string().bright_green());
290 Ok(())
291 }
292
293 async fn create_migrations_table(&self) -> Result<()> {
295 sqlx::query(
296 r#"
297 CREATE TABLE IF NOT EXISTS schema_migrations (
298 id SERIAL PRIMARY KEY,
299 name VARCHAR(255) NOT NULL UNIQUE,
300 applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
301 )
302 "#
303 )
304 .execute(&self.pool)
305 .await?;
306
307 Ok(())
308 }
309
310 fn load_migration_files(&self) -> Result<Vec<MigrationFile>> {
315 let migrations_dir = Path::new("migrations");
316 let mut migrations = Vec::new();
317
318 if migrations_dir.exists() {
319 for entry in fs::read_dir(migrations_dir)? {
320 let entry = entry?;
321 let path = entry.path();
322 let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else {
323 continue;
324 };
325 let Some(name) = file_name.strip_suffix(".up.sql") else {
326 continue;
327 };
328 let content = fs::read_to_string(&path)?;
329 migrations.push(MigrationFile {
330 name: name.to_string(),
331 content,
332 });
333 }
334 }
335
336 migrations.sort_by(|a, b| a.name.cmp(&b.name));
337 Ok(migrations)
338 }
339
340 async fn get_applied_migrations(&self) -> Result<Vec<String>> {
342 let rows = sqlx::query("SELECT name FROM schema_migrations ORDER BY name")
343 .fetch_all(&self.pool)
344 .await?;
345
346 let migrations: Vec<String> = rows
347 .into_iter()
348 .map(|row| row.get::<String, _>("name"))
349 .collect();
350
351 Ok(migrations)
352 }
353
354 async fn apply_migration(&self, migration: &MigrationFile) -> Result<()> {
361 let mut tx = self.pool.begin().await?;
362
363 sqlx::raw_sql(&migration.content)
364 .execute(&mut *tx)
365 .await?;
366
367 sqlx::query("INSERT INTO schema_migrations (name) VALUES ($1)")
368 .bind(&migration.name)
369 .execute(&mut *tx)
370 .await?;
371
372 tx.commit().await?;
373 Ok(())
374 }
375}
376
377struct MigrationFile {
379 name: String,
380 content: String,
381}
382
383pub trait Migration {
385 fn name(&self) -> &str;
386 fn up(&self) -> &str;
387 fn down(&self) -> &str;
388}
389
390#[derive(Debug, Clone)]
392pub struct MigrationRecord {
393 pub id: i32,
394 pub name: String,
395 pub applied_at: DateTime<Utc>,
396}
397
398#[derive(Debug, Clone)]
400pub struct MigrationStatus {
401 pub total_migrations: usize,
402 pub applied_migrations: Vec<String>,
403 pub pending_migrations: Vec<String>,
404 pub last_migration: Option<String>,
405}