1use std::collections::HashMap;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::Arc;
17
18use parking_lot::RwLock;
19
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub enum FieldDefault {
27 Zero,
29 One,
31 Null,
33 StringValue(String),
35}
36
37#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
43pub enum FieldMigration {
44 AddField {
49 name: String,
50 data_type: String,
51 nullable: bool,
52 default_value: FieldDefault,
53 },
54
55 DropField { name: String },
57
58 RenameField { from: String, to: String },
60
61 SetNullable { name: String, nullable: bool },
63}
64
65#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
72pub struct SchemaMigration {
73 pub from_version: u64,
75 pub to_version: u64,
77 pub description: String,
79 pub operations: Vec<FieldMigration>,
81}
82
83impl SchemaMigration {
84 pub fn new(
86 from: u64,
87 to: u64,
88 description: impl Into<String>,
89 ops: Vec<FieldMigration>,
90 ) -> Self {
91 Self {
92 from_version: from,
93 to_version: to,
94 description: description.into(),
95 operations: ops,
96 }
97 }
98
99 pub fn is_forward(&self) -> bool {
102 self.to_version > self.from_version
103 }
104
105 pub fn field_names_added(&self) -> Vec<&str> {
107 self.operations
108 .iter()
109 .filter_map(|op| {
110 if let FieldMigration::AddField { name, .. } = op {
111 Some(name.as_str())
112 } else {
113 None
114 }
115 })
116 .collect()
117 }
118
119 pub fn field_names_removed(&self) -> Vec<&str> {
121 self.operations
122 .iter()
123 .filter_map(|op| {
124 if let FieldMigration::DropField { name } = op {
125 Some(name.as_str())
126 } else {
127 None
128 }
129 })
130 .collect()
131 }
132}
133
134#[derive(Debug)]
140pub enum MigrationError {
141 NoPath { from: u64, to: u64 },
143 AlreadyAtVersion { version: u64 },
145 Downgrade { from: u64, to: u64 },
147}
148
149impl std::fmt::Display for MigrationError {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 match self {
152 MigrationError::NoPath { from, to } => {
153 write!(f, "no migration path from version {from} to version {to}")
154 }
155 MigrationError::AlreadyAtVersion { version } => {
156 write!(f, "schema is already at version {version}")
157 }
158 MigrationError::Downgrade { from, to } => {
159 write!(f, "downgrade from version {from} to {to} is not supported")
160 }
161 }
162 }
163}
164
165impl std::error::Error for MigrationError {}
166
167pub struct SchemaEvolutionManager {
180 migrations: RwLock<HashMap<u64, SchemaMigration>>,
182 current_version: AtomicU64,
184 applied_count: AtomicU64,
186}
187
188impl SchemaEvolutionManager {
189 pub fn new(initial_version: u64) -> Arc<Self> {
193 Arc::new(Self {
194 migrations: RwLock::new(HashMap::new()),
195 current_version: AtomicU64::new(initial_version),
196 applied_count: AtomicU64::new(0),
197 })
198 }
199
200 pub fn register_migration(&self, migration: SchemaMigration) -> bool {
205 let mut map = self.migrations.write();
206 if map.contains_key(&migration.from_version) {
207 return false;
208 }
209 map.insert(migration.from_version, migration);
210 true
211 }
212
213 pub fn migration_path(
219 &self,
220 from_version: u64,
221 to_version: u64,
222 ) -> Option<Vec<SchemaMigration>> {
223 if from_version == to_version {
224 return Some(Vec::new());
225 }
226 if from_version > to_version {
227 return None;
228 }
229
230 let map = self.migrations.read();
231 let mut path = Vec::new();
232 let mut current = from_version;
233
234 while current < to_version {
235 let step = map.get(¤t)?;
236 if step.to_version <= current {
237 return None;
239 }
240 path.push(step.clone());
241 current = step.to_version;
242 }
243
244 if current == to_version {
245 Some(path)
246 } else {
247 None
248 }
249 }
250
251 pub fn migrate_to(&self, target_version: u64) -> Result<Vec<SchemaMigration>, MigrationError> {
259 let current = self.current_version.load(Ordering::Acquire);
260
261 if current == target_version {
262 return Err(MigrationError::AlreadyAtVersion {
263 version: target_version,
264 });
265 }
266
267 if target_version < current {
268 return Err(MigrationError::Downgrade {
269 from: current,
270 to: target_version,
271 });
272 }
273
274 let path = self
275 .migration_path(current, target_version)
276 .ok_or(MigrationError::NoPath {
277 from: current,
278 to: target_version,
279 })?;
280
281 let steps = path.len() as u64;
282
283 self.current_version
287 .store(target_version, Ordering::Release);
288 self.applied_count.fetch_add(steps, Ordering::Relaxed);
289
290 Ok(path)
291 }
292
293 pub fn current_version(&self) -> u64 {
295 self.current_version.load(Ordering::Acquire)
296 }
297
298 pub fn applied_count(&self) -> u64 {
300 self.applied_count.load(Ordering::Relaxed)
301 }
302
303 pub fn can_migrate(&self, from_version: u64, to_version: u64) -> bool {
306 self.migration_path(from_version, to_version).is_some()
307 }
308
309 pub fn registered_versions(&self) -> Vec<u64> {
311 let map = self.migrations.read();
312 let mut versions: Vec<u64> = map.keys().copied().collect();
313 versions.sort_unstable();
314 versions
315 }
316}
317
318#[cfg(test)]
323mod tests {
324 use super::*;
325
326 fn make_add_op(name: &str) -> FieldMigration {
327 FieldMigration::AddField {
328 name: name.to_owned(),
329 data_type: "Int64".to_owned(),
330 nullable: true,
331 default_value: FieldDefault::Null,
332 }
333 }
334
335 fn make_drop_op(name: &str) -> FieldMigration {
336 FieldMigration::DropField {
337 name: name.to_owned(),
338 }
339 }
340
341 fn simple_migration(from: u64, to: u64, op: FieldMigration) -> SchemaMigration {
342 SchemaMigration::new(from, to, format!("v{from}->v{to}"), vec![op])
343 }
344
345 #[test]
346 fn test_register_migration() {
347 let mgr = SchemaEvolutionManager::new(1);
348 let m = simple_migration(1, 2, make_add_op("score"));
349 assert!(mgr.register_migration(m));
350 assert_eq!(mgr.registered_versions(), vec![1]);
351 }
352
353 #[test]
354 fn test_register_duplicate_returns_false() {
355 let mgr = SchemaEvolutionManager::new(1);
356 let m1 = simple_migration(1, 2, make_add_op("a"));
357 let m2 = simple_migration(1, 3, make_add_op("b"));
358 assert!(mgr.register_migration(m1));
359 assert!(!mgr.register_migration(m2));
361 }
362
363 #[test]
364 fn test_migration_path_direct() {
365 let mgr = SchemaEvolutionManager::new(1);
366 mgr.register_migration(simple_migration(1, 2, make_add_op("x")));
367
368 let path = mgr.migration_path(1, 2).expect("path should exist");
369 assert_eq!(path.len(), 1);
370 assert_eq!(path[0].from_version, 1);
371 assert_eq!(path[0].to_version, 2);
372 }
373
374 #[test]
375 fn test_migration_path_chained() {
376 let mgr = SchemaEvolutionManager::new(1);
377 mgr.register_migration(simple_migration(1, 2, make_add_op("a")));
378 mgr.register_migration(simple_migration(2, 3, make_add_op("b")));
379
380 let path = mgr.migration_path(1, 3).expect("chained path should exist");
381 assert_eq!(path.len(), 2);
382 assert_eq!(path[0].from_version, 1);
383 assert_eq!(path[1].from_version, 2);
384 }
385
386 #[test]
387 fn test_migration_path_missing() {
388 let mgr = SchemaEvolutionManager::new(1);
389 mgr.register_migration(simple_migration(1, 2, make_add_op("a")));
390 assert!(mgr.migration_path(1, 3).is_none());
392 }
393
394 #[test]
395 fn test_migrate_to_success() {
396 let mgr = SchemaEvolutionManager::new(1);
397 mgr.register_migration(simple_migration(1, 2, make_add_op("score")));
398
399 let applied = mgr.migrate_to(2).expect("migration should succeed");
400 assert_eq!(applied.len(), 1);
401 assert_eq!(mgr.current_version(), 2);
402 }
403
404 #[test]
405 fn test_migrate_to_already_at_version() {
406 let mgr = SchemaEvolutionManager::new(2);
407 let err = mgr.migrate_to(2).expect_err("should fail");
408 assert!(matches!(
409 err,
410 MigrationError::AlreadyAtVersion { version: 2 }
411 ));
412 }
413
414 #[test]
415 fn test_migrate_to_downgrade() {
416 let mgr = SchemaEvolutionManager::new(5);
417 let err = mgr.migrate_to(3).expect_err("downgrade should fail");
418 assert!(matches!(err, MigrationError::Downgrade { from: 5, to: 3 }));
419 }
420
421 #[test]
422 fn test_migrate_increments_applied_count() {
423 let mgr = SchemaEvolutionManager::new(1);
424 mgr.register_migration(simple_migration(1, 2, make_add_op("a")));
425 mgr.register_migration(simple_migration(2, 3, make_add_op("b")));
426
427 assert_eq!(mgr.applied_count(), 0);
428 mgr.migrate_to(3).expect("ok");
429 assert_eq!(mgr.applied_count(), 2);
430 }
431
432 #[test]
433 fn test_can_migrate() {
434 let mgr = SchemaEvolutionManager::new(1);
435 mgr.register_migration(simple_migration(1, 2, make_add_op("x")));
436
437 assert!(mgr.can_migrate(1, 2));
438 assert!(!mgr.can_migrate(1, 3));
439 assert!(!mgr.can_migrate(2, 1)); }
441
442 #[test]
443 fn test_field_names_added_removed() {
444 let ops = vec![
445 make_add_op("new_col"),
446 make_drop_op("old_col"),
447 FieldMigration::RenameField {
448 from: "alpha".to_owned(),
449 to: "beta".to_owned(),
450 },
451 ];
452 let m = SchemaMigration::new(1, 2, "mixed", ops);
453 assert_eq!(m.field_names_added(), vec!["new_col"]);
454 assert_eq!(m.field_names_removed(), vec!["old_col"]);
455 }
456
457 #[test]
458 fn test_registered_versions() {
459 let mgr = SchemaEvolutionManager::new(1);
460 mgr.register_migration(simple_migration(3, 4, make_add_op("z")));
461 mgr.register_migration(simple_migration(1, 2, make_add_op("a")));
462 mgr.register_migration(simple_migration(2, 3, make_add_op("b")));
463
464 assert_eq!(mgr.registered_versions(), vec![1, 2, 3]);
465 }
466
467 #[test]
468 fn test_is_forward() {
469 let forward = SchemaMigration::new(1, 2, "fwd", vec![]);
470 assert!(forward.is_forward());
471 let same = SchemaMigration::new(2, 2, "same", vec![]);
472 assert!(!same.is_forward());
473 }
474}