kimberlite_migration/
lock.rs1use crate::{Error, MigrationFile, Result};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct LockEntry {
11 pub id: u32,
13
14 pub name: String,
16
17 pub checksum: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct LockFile {
28 pub version: u32,
30
31 #[serde(rename = "migration")]
33 pub migrations: Vec<LockEntry>,
34}
35
36impl LockFile {
37 pub fn new() -> Self {
39 Self {
40 version: 1,
41 migrations: Vec::new(),
42 }
43 }
44
45 pub fn load(path: &Path) -> Result<Self> {
47 if !path.exists() {
48 return Ok(Self::new());
49 }
50
51 let content = fs::read_to_string(path)?;
52
53 if content.trim().is_empty() {
54 return Ok(Self::new());
55 }
56
57 let lock_file: Self = toml::from_str(&content)?;
58
59 Ok(lock_file)
60 }
61
62 pub fn save(&self, path: &Path) -> Result<()> {
64 if let Some(parent) = path.parent() {
66 fs::create_dir_all(parent)?;
67 }
68
69 let content = toml::to_string_pretty(self)?;
70 fs::write(path, content)?;
71
72 Ok(())
73 }
74
75 pub fn lock(&mut self, file: &MigrationFile) {
77 let entry = LockEntry {
78 id: file.migration.id,
79 name: file.migration.name.clone(),
80 checksum: file.checksum.clone(),
81 };
82
83 self.migrations.retain(|e| e.id != file.migration.id);
85 self.migrations.push(entry);
86
87 self.migrations.sort_by_key(|e| e.id);
89 }
90
91 pub fn validate(&self, files: &[MigrationFile]) -> Result<()> {
93 for file in files {
94 if let Some(locked) = self.migrations.iter().find(|e| e.id == file.migration.id) {
95 if locked.checksum != file.checksum {
97 return Err(Error::ChecksumMismatch {
98 id: file.migration.id,
99 expected: locked.checksum.clone(),
100 actual: file.checksum.clone(),
101 });
102 }
103 }
104 }
105
106 Ok(())
107 }
108
109 pub fn is_locked(&self, id: u32) -> bool {
111 self.migrations.iter().any(|e| e.id == id)
112 }
113
114 pub fn update(&mut self, files: &[MigrationFile]) -> Result<()> {
116 self.validate(files)?;
118
119 for file in files {
121 if !self.is_locked(file.migration.id) {
122 self.lock(file);
123 }
124 }
125
126 Ok(())
127 }
128}
129
130impl Default for LockFile {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::Migration;
140 use chrono::Utc;
141 use std::path::PathBuf;
142 use tempfile::TempDir;
143
144 fn create_test_migration_file(id: u32, name: &str, sql: &str) -> MigrationFile {
145 let migration = Migration {
146 id,
147 name: name.to_string(),
148 sql: sql.to_string(),
149 created_at: Utc::now(),
150 author: None,
151 };
152
153 let checksum = migration.checksum();
154
155 MigrationFile {
156 migration,
157 path: PathBuf::from(format!("{id:04}_{name}.sql")),
158 checksum,
159 }
160 }
161
162 #[test]
163 fn test_lock_file_creation() {
164 let lock = LockFile::new();
165
166 assert_eq!(lock.version, 1);
167 assert!(lock.migrations.is_empty());
168 }
169
170 #[test]
171 fn test_lock_migration() {
172 let mut lock = LockFile::new();
173 let file = create_test_migration_file(1, "test", "SELECT 1;");
174
175 lock.lock(&file);
176
177 assert!(lock.is_locked(1));
178 assert_eq!(
179 lock.migrations.iter().find(|e| e.id == 1).unwrap().name,
180 "test"
181 );
182 }
183
184 #[test]
185 fn test_validate_success() {
186 let mut lock = LockFile::new();
187 let file = create_test_migration_file(1, "test", "SELECT 1;");
188
189 lock.lock(&file);
190
191 assert!(lock.validate(&[file]).is_ok());
193 }
194
195 #[test]
196 fn test_validate_checksum_mismatch() {
197 let mut lock = LockFile::new();
198 let file1 = create_test_migration_file(1, "test", "SELECT 1;");
199
200 lock.lock(&file1);
201
202 let file2 = create_test_migration_file(1, "test", "SELECT 2;");
204
205 let result = lock.validate(&[file2]);
206
207 assert!(matches!(result, Err(Error::ChecksumMismatch { .. })));
208 }
209
210 #[test]
211 fn test_update_lock_file() {
212 let mut lock = LockFile::new();
213 let file1 = create_test_migration_file(1, "first", "SELECT 1;");
214
215 lock.lock(&file1);
216
217 let file2 = create_test_migration_file(2, "second", "SELECT 2;");
219 lock.update(&[file1.clone(), file2.clone()]).unwrap();
220
221 assert!(lock.is_locked(1));
222 assert!(lock.is_locked(2));
223 }
224
225 #[test]
226 fn test_save_and_load() {
227 let temp = TempDir::new().unwrap();
228 let path = temp.path().join(".lock");
229
230 let mut lock = LockFile::new();
231 let file = create_test_migration_file(1, "test", "SELECT 1;");
232 lock.lock(&file);
233
234 lock.save(&path).unwrap();
236 assert!(path.exists());
237
238 let loaded = LockFile::load(&path).unwrap();
240
241 assert_eq!(loaded.version, 1);
242 assert_eq!(loaded.migrations.len(), 1);
243 assert!(loaded.is_locked(1));
244 }
245
246 #[test]
247 fn test_load_nonexistent_file() {
248 let temp = TempDir::new().unwrap();
249 let path = temp.path().join(".lock");
250
251 let lock = LockFile::load(&path).unwrap();
252
253 assert!(lock.migrations.is_empty());
254 }
255}