kimberlite_migration/
lib.rs1pub mod error;
10pub mod file;
11pub mod lock;
12pub mod tracker;
13
14pub use error::{Error, Result};
15pub use file::{Migration, MigrationFile};
16pub use lock::LockFile;
17pub use tracker::MigrationTracker;
18
19use std::path::PathBuf;
20
21#[derive(Debug, Clone)]
23pub struct MigrationConfig {
24 pub migrations_dir: PathBuf,
26
27 pub state_dir: PathBuf,
29
30 pub auto_timestamp: bool,
32}
33
34impl Default for MigrationConfig {
35 fn default() -> Self {
36 Self {
37 migrations_dir: PathBuf::from("migrations"),
38 state_dir: PathBuf::from(".kimberlite/migrations"),
39 auto_timestamp: true,
40 }
41 }
42}
43
44impl MigrationConfig {
45 pub fn with_migrations_dir(dir: impl Into<PathBuf>) -> Self {
47 Self {
48 migrations_dir: dir.into(),
49 ..Default::default()
50 }
51 }
52
53 pub fn lock_file_path(&self) -> PathBuf {
55 self.state_dir.join(".lock")
56 }
57}
58
59pub struct MigrationManager {
61 config: MigrationConfig,
62 tracker: MigrationTracker,
63}
64
65impl MigrationManager {
66 pub fn new(config: MigrationConfig) -> Result<Self> {
68 let tracker = MigrationTracker::new(config.state_dir.clone())?;
69 Ok(Self { config, tracker })
70 }
71
72 pub fn list_files(&self) -> Result<Vec<MigrationFile>> {
74 MigrationFile::discover(&self.config.migrations_dir)
75 }
76
77 pub fn list_pending(&self) -> Result<Vec<MigrationFile>> {
79 let all_files = self.list_files()?;
80 let applied = self.tracker.list_applied()?;
81
82 let applied_ids: std::collections::HashSet<_> = applied.iter().map(|m| m.id).collect();
83
84 Ok(all_files
85 .into_iter()
86 .filter(|f| !applied_ids.contains(&f.migration.id))
87 .collect())
88 }
89
90 pub fn create(&self, name: &str) -> Result<MigrationFile> {
92 MigrationFile::create(
93 &self.config.migrations_dir,
94 name,
95 self.config.auto_timestamp,
96 )
97 }
98
99 pub fn record_applied(&self, file: &MigrationFile) -> Result<tracker::AppliedMigration> {
101 self.tracker.record_applied(
102 file.migration.id,
103 file.migration.name.clone(),
104 file.checksum.clone(),
105 )
106 }
107
108 pub fn remove_applied(&self, id: u32) -> Result<()> {
110 self.tracker.remove_applied(id)
111 }
112
113 pub fn up_sql(file: &MigrationFile) -> &str {
115 if let Some(idx) = file.migration.sql.find("-- Down Migration") {
116 file.migration.sql[..idx].trim_end()
117 } else {
118 file.migration.sql.trim()
119 }
120 }
121
122 pub fn down_sql(file: &MigrationFile) -> Option<&str> {
124 file.migration
125 .sql
126 .find("-- Down Migration")
127 .map(|idx| {
128 let after = &file.migration.sql[idx..];
129 after.find('\n').map_or("", |nl| after[nl + 1..].trim())
131 })
132 .filter(|s| !s.is_empty())
133 }
134
135 pub fn validate(&self) -> Result<()> {
137 let lock = LockFile::load(&self.config.lock_file_path())?;
138 let files = self.list_files()?;
139
140 lock.validate(&files)?;
141
142 for (i, file) in files.iter().enumerate() {
144 let expected_id = (i + 1) as u32;
145 if file.migration.id != expected_id {
146 return Err(Error::InvalidSequence {
147 expected: expected_id,
148 found: file.migration.id,
149 });
150 }
151 }
152
153 Ok(())
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use tempfile::TempDir;
161
162 #[test]
163 fn test_migration_config_default() {
164 let config = MigrationConfig::default();
165 assert_eq!(config.migrations_dir, PathBuf::from("migrations"));
166 assert_eq!(config.state_dir, PathBuf::from(".kimberlite/migrations"));
167 assert!(config.auto_timestamp);
168 }
169
170 #[test]
171 fn test_migration_config_lock_path() {
172 let config = MigrationConfig::default();
173 assert_eq!(
174 config.lock_file_path(),
175 PathBuf::from(".kimberlite/migrations/.lock")
176 );
177 }
178
179 #[test]
180 fn test_migration_manager_creation() {
181 let temp = TempDir::new().unwrap();
182 let config = MigrationConfig {
183 migrations_dir: temp.path().join("migrations"),
184 state_dir: temp.path().join("state"),
185 auto_timestamp: false,
186 };
187
188 std::fs::create_dir_all(&config.migrations_dir).unwrap();
189 std::fs::create_dir_all(&config.state_dir).unwrap();
190
191 let manager = MigrationManager::new(config);
192 assert!(manager.is_ok());
193 }
194}