Skip to main content

azoth/
migration.rs

1//! Database migration system
2//!
3//! Provides a flexible migration framework for evolving projection schemas.
4//!
5//! # Example
6//!
7//! ```no_run
8//! use azoth::prelude::*;
9//! use azoth::{Migration, MigrationManager};
10//! use rusqlite::Connection;
11//!
12//! // Define migrations
13//! struct CreateAccountsTable;
14//!
15//! impl Migration for CreateAccountsTable {
16//!     fn version(&self) -> u32 {
17//!         2
18//!     }
19//!
20//!     fn name(&self) -> &str {
21//!         "create_accounts_table"
22//!     }
23//!
24//!     fn up(&self, conn: &Connection) -> Result<()> {
25//!         conn.execute(
26//!             "CREATE TABLE accounts (
27//!                 id INTEGER PRIMARY KEY,
28//!                 balance INTEGER NOT NULL DEFAULT 0,
29//!                 created_at TEXT NOT NULL DEFAULT (datetime('now'))
30//!             )",
31//!             [],
32//!         ).map_err(|e| AzothError::Projection(e.to_string()))?;
33//!         Ok(())
34//!     }
35//!
36//!     fn down(&self, conn: &Connection) -> Result<()> {
37//!         conn.execute("DROP TABLE accounts", [])
38//!             .map_err(|e| AzothError::Projection(e.to_string()))?;
39//!         Ok(())
40//!     }
41//! }
42//!
43//! # fn main() -> Result<()> {
44//! // Create manager and register migrations
45//! let mut manager = MigrationManager::new();
46//! manager.add(Box::new(CreateAccountsTable));
47//!
48//! // Run migrations
49//! let db = AzothDb::open("./data")?;
50//! manager.run(db.projection())?;
51//! # Ok(())
52//! # }
53//! ```
54
55use crate::{AzothError, ProjectionStore, Result};
56use rusqlite::Connection;
57use std::sync::Arc;
58
59/// Migration trait
60///
61/// Implement this to define a database migration.
62pub trait Migration: Send + Sync {
63    /// The version number this migration targets
64    ///
65    /// Versions should be sequential starting from 2 (version 1 is the base schema).
66    fn version(&self) -> u32;
67
68    /// Human-readable name for this migration
69    fn name(&self) -> &str;
70
71    /// Apply the migration (upgrade)
72    fn up(&self, conn: &Connection) -> Result<()>;
73
74    /// Rollback the migration (downgrade)
75    ///
76    /// Optional - default implementation returns an error.
77    fn down(&self, _conn: &Connection) -> Result<()> {
78        Err(AzothError::InvalidState(format!(
79            "Migration '{}' does not support rollback",
80            self.name()
81        )))
82    }
83
84    /// Optional: verify the migration was applied correctly
85    fn verify(&self, _conn: &Connection) -> Result<()> {
86        Ok(())
87    }
88}
89
90/// Migration manager
91///
92/// Manages a collection of migrations and runs them in order.
93pub struct MigrationManager {
94    migrations: Vec<Box<dyn Migration>>,
95}
96
97impl MigrationManager {
98    /// Create a new migration manager
99    pub fn new() -> Self {
100        Self {
101            migrations: Vec::new(),
102        }
103    }
104
105    /// Add a migration
106    ///
107    /// Migrations will be sorted by version when run.
108    pub fn add(&mut self, migration: Box<dyn Migration>) {
109        self.migrations.push(migration);
110    }
111
112    /// Add multiple migrations
113    pub fn add_all(&mut self, migrations: Vec<Box<dyn Migration>>) {
114        for m in migrations {
115            self.add(m);
116        }
117    }
118
119    /// Run all pending migrations on a SQLite projection store
120    ///
121    /// Applies migrations in version order, starting from the current schema version.
122    pub fn run(&self, projection: &Arc<crate::SqliteProjectionStore>) -> Result<()> {
123        // Get current version
124        let current_version = projection.schema_version()?;
125
126        // Sort migrations by version
127        let mut sorted: Vec<_> = self.migrations.iter().collect();
128        sorted.sort_by_key(|m| m.version());
129
130        // Find pending migrations
131        let pending: Vec<_> = sorted
132            .into_iter()
133            .filter(|m| m.version() > current_version)
134            .collect();
135
136        if pending.is_empty() {
137            tracing::info!("No pending migrations");
138            return Ok(());
139        }
140
141        tracing::info!("Running {} pending migrations", pending.len());
142
143        // Run each migration
144        for migration in pending {
145            self.run_single(projection, &**migration)?;
146        }
147
148        Ok(())
149    }
150
151    /// Run a single migration
152    fn run_single(
153        &self,
154        projection: &Arc<crate::SqliteProjectionStore>,
155        migration: &dyn Migration,
156    ) -> Result<()> {
157        tracing::info!(
158            "Applying migration v{}: {}",
159            migration.version(),
160            migration.name()
161        );
162
163        // Execute the migration SQL
164        {
165            let conn = projection.conn().lock().unwrap();
166            migration.up(&conn)?;
167        }
168
169        // Update schema version
170        projection.migrate(migration.version())?;
171
172        tracing::info!("Migration v{} complete", migration.version());
173        Ok(())
174    }
175
176    /// Rollback the last migration
177    ///
178    /// Warning: This is dangerous and may result in data loss.
179    pub fn rollback_last(&self, projection: &Arc<crate::SqliteProjectionStore>) -> Result<()> {
180        let current_version = projection.schema_version()?;
181
182        if current_version <= 1 {
183            return Err(AzothError::InvalidState(
184                "Cannot rollback base schema".into(),
185            ));
186        }
187
188        // Find migration for current version
189        let migration = self
190            .migrations
191            .iter()
192            .find(|m| m.version() == current_version)
193            .ok_or_else(|| {
194                AzothError::InvalidState(format!(
195                    "No migration found for version {}",
196                    current_version
197                ))
198            })?;
199
200        tracing::warn!(
201            "Rolling back migration v{}: {}",
202            migration.version(),
203            migration.name()
204        );
205
206        // Execute the down() migration
207        {
208            let conn = projection.conn().lock().unwrap();
209            migration.down(&conn)?;
210        }
211
212        // Update schema version
213        projection.migrate(current_version - 1)?;
214
215        Ok(())
216    }
217
218    /// List all migrations
219    pub fn list(&self) -> Vec<MigrationInfo> {
220        let mut sorted: Vec<_> = self.migrations.iter().collect();
221        sorted.sort_by_key(|m| m.version());
222
223        sorted
224            .into_iter()
225            .map(|m| MigrationInfo {
226                version: m.version(),
227                name: m.name().to_string(),
228            })
229            .collect()
230    }
231
232    /// Get pending migrations
233    pub fn pending(
234        &self,
235        projection: &Arc<crate::SqliteProjectionStore>,
236    ) -> Result<Vec<MigrationInfo>> {
237        let current_version = projection.schema_version()?;
238
239        let mut sorted: Vec<_> = self.migrations.iter().collect();
240        sorted.sort_by_key(|m| m.version());
241
242        Ok(sorted
243            .into_iter()
244            .filter(|m| m.version() > current_version)
245            .map(|m| MigrationInfo {
246                version: m.version(),
247                name: m.name().to_string(),
248            })
249            .collect())
250    }
251}
252
253impl Default for MigrationManager {
254    fn default() -> Self {
255        Self::new()
256    }
257}
258
259/// Migration information
260#[derive(Debug, Clone)]
261pub struct MigrationInfo {
262    pub version: u32,
263    pub name: String,
264}
265
266/// Helper macro to define a migration
267///
268/// # Example
269///
270/// ```ignore
271/// migration!(
272///     CreateUsersTable,
273///     version: 2,
274///     name: "create_users_table",
275///     up: |conn| {
276///         conn.execute(
277///             "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
278///             [],
279///         )?;
280///         Ok(())
281///     },
282///     down: |conn| {
283///         conn.execute("DROP TABLE users", [])?;
284///         Ok(())
285///     }
286/// );
287/// ```
288#[macro_export]
289macro_rules! migration {
290    (
291        $name:ident,
292        version: $version:expr,
293        name: $migration_name:expr,
294        up: |$up_conn:ident| $up_body:block
295        $(, down: |$down_conn:ident| $down_body:block)?
296    ) => {
297        struct $name;
298
299        impl $crate::Migration for $name {
300            fn version(&self) -> u32 {
301                $version
302            }
303
304            fn name(&self) -> &str {
305                $migration_name
306            }
307
308            fn up(&self, $up_conn: &rusqlite::Connection) -> $crate::Result<()> {
309                $up_body
310            }
311
312            $(
313                fn down(&self, $down_conn: &rusqlite::Connection) -> $crate::Result<()> {
314                    $down_body
315                }
316            )?
317        }
318    };
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    struct TestMigration;
326
327    impl Migration for TestMigration {
328        fn version(&self) -> u32 {
329            2
330        }
331
332        fn name(&self) -> &str {
333            "test_migration"
334        }
335
336        fn up(&self, _conn: &Connection) -> Result<()> {
337            Ok(())
338        }
339    }
340
341    #[test]
342    fn test_migration_manager() {
343        let mut manager = MigrationManager::new();
344        manager.add(Box::new(TestMigration));
345
346        let migrations = manager.list();
347        assert_eq!(migrations.len(), 1);
348        assert_eq!(migrations[0].version, 2);
349        assert_eq!(migrations[0].name, "test_migration");
350    }
351}