cetane 0.1.1

Django-inspired database migrations for Diesel
Documentation
use rusqlite::Connection;

use crate::migrator::MigrationStateStore;

const DEFAULT_TABLE_NAME: &str = "schema_migrations";

pub struct SqliteMigrationState<'a> {
    conn: &'a Connection,
    table_name: String,
}

impl<'a> SqliteMigrationState<'a> {
    pub fn new(conn: &'a Connection) -> Result<Self, String> {
        Self::with_table_name(conn, DEFAULT_TABLE_NAME)
    }

    pub fn with_table_name(conn: &'a Connection, table_name: &str) -> Result<Self, String> {
        let state = Self {
            conn,
            table_name: table_name.to_string(),
        };
        state.ensure_table()?;
        Ok(state)
    }

    fn ensure_table(&self) -> Result<(), String> {
        self.conn
            .execute(
                &format!(
                    "CREATE TABLE IF NOT EXISTS {} (
                        migration_name TEXT PRIMARY KEY,
                        applied INTEGER NOT NULL DEFAULT 1
                    )",
                    self.table_name
                ),
                [],
            )
            .map_err(|e| e.to_string())?;
        Ok(())
    }
}

impl MigrationStateStore for SqliteMigrationState<'_> {
    fn applied_migrations(&mut self) -> Result<Vec<String>, String> {
        let mut stmt = self
            .conn
            .prepare(&format!(
                "SELECT migration_name FROM {} WHERE applied = 1 ORDER BY rowid",
                self.table_name
            ))
            .map_err(|e| e.to_string())?;

        let names = stmt
            .query_map([], |row| row.get(0))
            .map_err(|e| e.to_string())?
            .collect::<Result<Vec<String>, _>>()
            .map_err(|e| e.to_string())?;

        Ok(names)
    }

    fn mark_applied(&mut self, name: &str) -> Result<(), String> {
        self.conn
            .execute(
                &format!(
                    "INSERT INTO {} (migration_name, applied) VALUES (?1, 1)
                     ON CONFLICT(migration_name) DO UPDATE SET applied = 1",
                    self.table_name
                ),
                [name],
            )
            .map_err(|e| e.to_string())?;
        Ok(())
    }

    fn mark_unapplied(&mut self, name: &str) -> Result<(), String> {
        self.conn
            .execute(
                &format!(
                    "UPDATE {} SET applied = 0 WHERE migration_name = ?1",
                    self.table_name
                ),
                [name],
            )
            .map_err(|e| e.to_string())?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn creates_table_on_init() {
        let conn = Connection::open_in_memory().unwrap();
        let _state = SqliteMigrationState::new(&conn).unwrap();

        let exists: bool = conn
            .query_row(
                "SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'",
                [],
                |_| Ok(true),
            )
            .unwrap_or(false);

        assert!(exists);
    }

    #[test]
    fn custom_table_name() {
        let conn = Connection::open_in_memory().unwrap();
        let _state = SqliteMigrationState::with_table_name(&conn, "my_migrations").unwrap();

        let exists: bool = conn
            .query_row(
                "SELECT 1 FROM sqlite_master WHERE type='table' AND name='my_migrations'",
                [],
                |_| Ok(true),
            )
            .unwrap_or(false);

        assert!(exists);
    }

    #[test]
    fn mark_applied_and_query() {
        let conn = Connection::open_in_memory().unwrap();
        let mut state = SqliteMigrationState::new(&conn).unwrap();

        state.mark_applied("0001_initial").unwrap();
        state.mark_applied("0002_add_users").unwrap();

        let applied = state.applied_migrations().unwrap();
        assert_eq!(applied, vec!["0001_initial", "0002_add_users"]);
    }

    #[test]
    fn mark_unapplied() {
        let conn = Connection::open_in_memory().unwrap();
        let mut state = SqliteMigrationState::new(&conn).unwrap();

        state.mark_applied("0001_initial").unwrap();
        state.mark_applied("0002_add_users").unwrap();
        state.mark_unapplied("0002_add_users").unwrap();

        let applied = state.applied_migrations().unwrap();
        assert_eq!(applied, vec!["0001_initial"]);
    }

    #[test]
    fn mark_applied_is_idempotent() {
        let conn = Connection::open_in_memory().unwrap();
        let mut state = SqliteMigrationState::new(&conn).unwrap();

        state.mark_applied("0001_initial").unwrap();
        state.mark_applied("0001_initial").unwrap();

        let applied = state.applied_migrations().unwrap();
        assert_eq!(applied, vec!["0001_initial"]);
    }

    #[test]
    fn reapply_after_unapply() {
        let conn = Connection::open_in_memory().unwrap();
        let mut state = SqliteMigrationState::new(&conn).unwrap();

        state.mark_applied("0001_initial").unwrap();
        state.mark_unapplied("0001_initial").unwrap();
        state.mark_applied("0001_initial").unwrap();

        let applied = state.applied_migrations().unwrap();
        assert_eq!(applied, vec!["0001_initial"]);
    }

    #[test]
    fn unapplied_migration_not_in_list() {
        let conn = Connection::open_in_memory().unwrap();
        let mut state = SqliteMigrationState::new(&conn).unwrap();

        state.mark_applied("0001_a").unwrap();
        state.mark_applied("0002_b").unwrap();
        state.mark_unapplied("0001_a").unwrap();

        let applied = state.applied_migrations().unwrap();
        assert_eq!(applied, vec!["0002_b"]);
    }
}