1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::error::MigrateResult;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct MigrationRecord {
11 pub id: String,
13 pub checksum: String,
15 pub applied_at: DateTime<Utc>,
17 pub duration_ms: i64,
19 pub rolled_back: bool,
21}
22
23#[async_trait::async_trait]
25pub trait MigrationHistoryRepository: Send + Sync {
26 async fn initialize(&self) -> MigrateResult<()>;
28
29 async fn get_applied(&self) -> MigrateResult<Vec<MigrationRecord>>;
31
32 async fn is_applied(&self, id: &str) -> MigrateResult<bool>;
34
35 async fn record_applied(&self, id: &str, checksum: &str, duration_ms: i64)
37 -> MigrateResult<()>;
38
39 async fn record_rollback(&self, id: &str) -> MigrateResult<()>;
41
42 async fn get_last_applied(&self) -> MigrateResult<Option<MigrationRecord>>;
44
45 async fn acquire_lock(&self) -> MigrateResult<MigrationLock>;
47}
48
49pub struct MigrationLock {
51 lock_id: i64,
52 release_fn: Option<Box<dyn FnOnce() + Send>>,
53}
54
55impl MigrationLock {
56 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 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
78pub 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
92pub 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}