Skip to main content

prax_migrate/
history.rs

1//! Migration history tracking.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::error::MigrateResult;
7
8/// A record of an applied migration.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct MigrationRecord {
11    /// Migration ID/name.
12    pub id: String,
13    /// Checksum of the migration content.
14    pub checksum: String,
15    /// When the migration was applied.
16    pub applied_at: DateTime<Utc>,
17    /// Duration of the migration in milliseconds.
18    pub duration_ms: i64,
19    /// Whether this migration was rolled back.
20    pub rolled_back: bool,
21}
22
23/// Migration history repository.
24#[async_trait::async_trait]
25pub trait MigrationHistoryRepository: Send + Sync {
26    /// Initialize the migrations table.
27    async fn initialize(&self) -> MigrateResult<()>;
28
29    /// Get all applied migrations.
30    async fn get_applied(&self) -> MigrateResult<Vec<MigrationRecord>>;
31
32    /// Check if a migration has been applied.
33    async fn is_applied(&self, id: &str) -> MigrateResult<bool>;
34
35    /// Record a migration as applied.
36    async fn record_applied(&self, id: &str, checksum: &str, duration_ms: i64)
37    -> MigrateResult<()>;
38
39    /// Mark a migration as rolled back.
40    async fn record_rollback(&self, id: &str) -> MigrateResult<()>;
41
42    /// Get the last applied migration.
43    async fn get_last_applied(&self) -> MigrateResult<Option<MigrationRecord>>;
44
45    /// Acquire an exclusive lock for migrations.
46    async fn acquire_lock(&self) -> MigrateResult<MigrationLock>;
47}
48
49/// Migration lock to prevent concurrent migrations.
50pub struct MigrationLock {
51    lock_id: i64,
52    release_fn: Option<Box<dyn FnOnce() + Send>>,
53}
54
55impl MigrationLock {
56    /// Create a new migration lock.
57    pub fn new(lock_id: i64, release: impl FnOnce() + Send + 'static) -> Self {
58        Self {
59            lock_id,
60            release_fn: Some(Box::new(release)),
61        }
62    }
63
64    /// Get the lock ID.
65    pub fn id(&self) -> i64 {
66        self.lock_id
67    }
68}
69
70impl Drop for MigrationLock {
71    fn drop(&mut self) {
72        if let Some(release) = self.release_fn.take() {
73            release();
74        }
75    }
76}
77
78/// SQL for initializing the migrations table (PostgreSQL).
79pub const POSTGRES_INIT_SQL: &str = r#"
80CREATE TABLE IF NOT EXISTS "_prax_migrations" (
81    id VARCHAR(255) PRIMARY KEY,
82    checksum VARCHAR(64) NOT NULL,
83    applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
84    duration_ms BIGINT NOT NULL DEFAULT 0,
85    rolled_back BOOLEAN NOT NULL DEFAULT FALSE
86);
87
88CREATE INDEX IF NOT EXISTS "_prax_migrations_applied_at_idx"
89    ON "_prax_migrations" (applied_at DESC);
90"#;
91
92/// SQL for advisory lock (PostgreSQL).
93pub const POSTGRES_LOCK_SQL: &str = "SELECT pg_advisory_lock(42424242)";
94pub const POSTGRES_UNLOCK_SQL: &str = "SELECT pg_advisory_unlock(42424242)";
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_migration_record() {
102        let record = MigrationRecord {
103            id: "20231215_create_users".to_string(),
104            checksum: "abc123".to_string(),
105            applied_at: Utc::now(),
106            duration_ms: 150,
107            rolled_back: false,
108        };
109
110        assert!(!record.rolled_back);
111        assert!(record.duration_ms > 0);
112    }
113
114    #[test]
115    fn test_init_sql_has_table() {
116        assert!(POSTGRES_INIT_SQL.contains("_prax_migrations"));
117        assert!(POSTGRES_INIT_SQL.contains("checksum"));
118    }
119}