Skip to main content

chopin_orm/
migrations.rs

1use crate::{Executor, OrmResult};
2
3/// Defines a single database migration with forward and reverse operations.
4pub trait Migration {
5    /// A unique name identifying this migration (e.g., "001_create_users").
6    fn name(&self) -> &'static str;
7    /// Apply this migration.
8    fn up(&self, executor: &mut dyn Executor) -> OrmResult<()>;
9    /// Revert this migration.
10    fn down(&self, executor: &mut dyn Executor) -> OrmResult<()>;
11}
12
13/// Represents the status of a single migration.
14#[derive(Debug, Clone)]
15pub struct MigrationStatus {
16    pub name: String,
17    pub applied: bool,
18}
19
20/// Coordinates the execution, rollback, and status reporting of database migrations.
21pub struct MigrationManager;
22
23impl MigrationManager {
24    /// Creates the internal `__chopin_migrations` ledger table if it does not exist.
25    pub fn ensure_migrations_table(executor: &mut dyn Executor) -> OrmResult<()> {
26        let sql = r#"
27            CREATE TABLE IF NOT EXISTS __chopin_migrations (
28                name TEXT PRIMARY KEY,
29                applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
30            )
31        "#;
32        executor.execute(sql, &[])?;
33        Ok(())
34    }
35
36    /// Returns the status of each migration (applied or pending).
37    pub fn status(
38        executor: &mut dyn Executor,
39        migrations: &[&dyn Migration],
40    ) -> OrmResult<Vec<MigrationStatus>> {
41        Self::ensure_migrations_table(executor)?;
42
43        let mut statuses = Vec::with_capacity(migrations.len());
44        for m in migrations {
45            let name = m.name();
46            let check_sql = "SELECT 1 FROM __chopin_migrations WHERE name = $1";
47            let rows = executor.query(check_sql, &[&name])?;
48            statuses.push(MigrationStatus {
49                name: name.to_string(),
50                applied: !rows.is_empty(),
51            });
52        }
53        Ok(statuses)
54    }
55
56    /// Applies all pending migrations in order.
57    pub fn up(executor: &mut dyn Executor, migrations: &[&dyn Migration]) -> OrmResult<()> {
58        Self::ensure_migrations_table(executor)?;
59
60        for m in migrations {
61            let name = m.name();
62            let check_sql = "SELECT 1 FROM __chopin_migrations WHERE name = $1";
63            let rows = executor.query(check_sql, &[&name])?;
64
65            if rows.is_empty() {
66                #[cfg(feature = "log")]
67                log::info!("Applying migration: {}", name);
68                m.up(executor)?;
69                let insert_sql = "INSERT INTO __chopin_migrations (name) VALUES ($1)";
70                executor.execute(insert_sql, &[&name])?;
71                #[cfg(feature = "log")]
72                log::info!("Successfully applied: {}", name);
73            }
74        }
75        Ok(())
76    }
77
78    /// Reverts all applied migrations in reverse order.
79    pub fn down(executor: &mut dyn Executor, migrations: &[&dyn Migration]) -> OrmResult<()> {
80        Self::ensure_migrations_table(executor)?;
81
82        for m in migrations.iter().rev() {
83            let name = m.name();
84            let check_sql = "SELECT 1 FROM __chopin_migrations WHERE name = $1";
85            let rows = executor.query(check_sql, &[&name])?;
86
87            if !rows.is_empty() {
88                #[cfg(feature = "log")]
89                log::info!("Reverting migration: {}", name);
90                m.down(executor)?;
91                let delete_sql = "DELETE FROM __chopin_migrations WHERE name = $1";
92                executor.execute(delete_sql, &[&name])?;
93                #[cfg(feature = "log")]
94                log::info!("Successfully reverted: {}", name);
95            }
96        }
97        Ok(())
98    }
99}
100
101/// Declares a database index to be created during schema sync or migrations.
102pub struct Index {
103    pub name: &'static str,
104    pub columns: &'static [&'static str],
105    pub unique: bool,
106}