use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use parking_lot::RwLock;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum FieldDefault {
Zero,
One,
Null,
StringValue(String),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum FieldMigration {
AddField {
name: String,
data_type: String,
nullable: bool,
default_value: FieldDefault,
},
DropField { name: String },
RenameField { from: String, to: String },
SetNullable { name: String, nullable: bool },
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SchemaMigration {
pub from_version: u64,
pub to_version: u64,
pub description: String,
pub operations: Vec<FieldMigration>,
}
impl SchemaMigration {
pub fn new(
from: u64,
to: u64,
description: impl Into<String>,
ops: Vec<FieldMigration>,
) -> Self {
Self {
from_version: from,
to_version: to,
description: description.into(),
operations: ops,
}
}
pub fn is_forward(&self) -> bool {
self.to_version > self.from_version
}
pub fn field_names_added(&self) -> Vec<&str> {
self.operations
.iter()
.filter_map(|op| {
if let FieldMigration::AddField { name, .. } = op {
Some(name.as_str())
} else {
None
}
})
.collect()
}
pub fn field_names_removed(&self) -> Vec<&str> {
self.operations
.iter()
.filter_map(|op| {
if let FieldMigration::DropField { name } = op {
Some(name.as_str())
} else {
None
}
})
.collect()
}
}
#[derive(Debug)]
pub enum MigrationError {
NoPath { from: u64, to: u64 },
AlreadyAtVersion { version: u64 },
Downgrade { from: u64, to: u64 },
}
impl std::fmt::Display for MigrationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MigrationError::NoPath { from, to } => {
write!(f, "no migration path from version {from} to version {to}")
}
MigrationError::AlreadyAtVersion { version } => {
write!(f, "schema is already at version {version}")
}
MigrationError::Downgrade { from, to } => {
write!(f, "downgrade from version {from} to {to} is not supported")
}
}
}
}
impl std::error::Error for MigrationError {}
pub struct SchemaEvolutionManager {
migrations: RwLock<HashMap<u64, SchemaMigration>>,
current_version: AtomicU64,
applied_count: AtomicU64,
}
impl SchemaEvolutionManager {
pub fn new(initial_version: u64) -> Arc<Self> {
Arc::new(Self {
migrations: RwLock::new(HashMap::new()),
current_version: AtomicU64::new(initial_version),
applied_count: AtomicU64::new(0),
})
}
pub fn register_migration(&self, migration: SchemaMigration) -> bool {
let mut map = self.migrations.write();
if map.contains_key(&migration.from_version) {
return false;
}
map.insert(migration.from_version, migration);
true
}
pub fn migration_path(
&self,
from_version: u64,
to_version: u64,
) -> Option<Vec<SchemaMigration>> {
if from_version == to_version {
return Some(Vec::new());
}
if from_version > to_version {
return None;
}
let map = self.migrations.read();
let mut path = Vec::new();
let mut current = from_version;
while current < to_version {
let step = map.get(¤t)?;
if step.to_version <= current {
return None;
}
path.push(step.clone());
current = step.to_version;
}
if current == to_version {
Some(path)
} else {
None
}
}
pub fn migrate_to(&self, target_version: u64) -> Result<Vec<SchemaMigration>, MigrationError> {
let current = self.current_version.load(Ordering::Acquire);
if current == target_version {
return Err(MigrationError::AlreadyAtVersion {
version: target_version,
});
}
if target_version < current {
return Err(MigrationError::Downgrade {
from: current,
to: target_version,
});
}
let path = self
.migration_path(current, target_version)
.ok_or(MigrationError::NoPath {
from: current,
to: target_version,
})?;
let steps = path.len() as u64;
self.current_version
.store(target_version, Ordering::Release);
self.applied_count.fetch_add(steps, Ordering::Relaxed);
Ok(path)
}
pub fn current_version(&self) -> u64 {
self.current_version.load(Ordering::Acquire)
}
pub fn applied_count(&self) -> u64 {
self.applied_count.load(Ordering::Relaxed)
}
pub fn can_migrate(&self, from_version: u64, to_version: u64) -> bool {
self.migration_path(from_version, to_version).is_some()
}
pub fn registered_versions(&self) -> Vec<u64> {
let map = self.migrations.read();
let mut versions: Vec<u64> = map.keys().copied().collect();
versions.sort_unstable();
versions
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_add_op(name: &str) -> FieldMigration {
FieldMigration::AddField {
name: name.to_owned(),
data_type: "Int64".to_owned(),
nullable: true,
default_value: FieldDefault::Null,
}
}
fn make_drop_op(name: &str) -> FieldMigration {
FieldMigration::DropField {
name: name.to_owned(),
}
}
fn simple_migration(from: u64, to: u64, op: FieldMigration) -> SchemaMigration {
SchemaMigration::new(from, to, format!("v{from}->v{to}"), vec![op])
}
#[test]
fn test_register_migration() {
let mgr = SchemaEvolutionManager::new(1);
let m = simple_migration(1, 2, make_add_op("score"));
assert!(mgr.register_migration(m));
assert_eq!(mgr.registered_versions(), vec![1]);
}
#[test]
fn test_register_duplicate_returns_false() {
let mgr = SchemaEvolutionManager::new(1);
let m1 = simple_migration(1, 2, make_add_op("a"));
let m2 = simple_migration(1, 3, make_add_op("b"));
assert!(mgr.register_migration(m1));
assert!(!mgr.register_migration(m2));
}
#[test]
fn test_migration_path_direct() {
let mgr = SchemaEvolutionManager::new(1);
mgr.register_migration(simple_migration(1, 2, make_add_op("x")));
let path = mgr.migration_path(1, 2).expect("path should exist");
assert_eq!(path.len(), 1);
assert_eq!(path[0].from_version, 1);
assert_eq!(path[0].to_version, 2);
}
#[test]
fn test_migration_path_chained() {
let mgr = SchemaEvolutionManager::new(1);
mgr.register_migration(simple_migration(1, 2, make_add_op("a")));
mgr.register_migration(simple_migration(2, 3, make_add_op("b")));
let path = mgr.migration_path(1, 3).expect("chained path should exist");
assert_eq!(path.len(), 2);
assert_eq!(path[0].from_version, 1);
assert_eq!(path[1].from_version, 2);
}
#[test]
fn test_migration_path_missing() {
let mgr = SchemaEvolutionManager::new(1);
mgr.register_migration(simple_migration(1, 2, make_add_op("a")));
assert!(mgr.migration_path(1, 3).is_none());
}
#[test]
fn test_migrate_to_success() {
let mgr = SchemaEvolutionManager::new(1);
mgr.register_migration(simple_migration(1, 2, make_add_op("score")));
let applied = mgr.migrate_to(2).expect("migration should succeed");
assert_eq!(applied.len(), 1);
assert_eq!(mgr.current_version(), 2);
}
#[test]
fn test_migrate_to_already_at_version() {
let mgr = SchemaEvolutionManager::new(2);
let err = mgr.migrate_to(2).expect_err("should fail");
assert!(matches!(
err,
MigrationError::AlreadyAtVersion { version: 2 }
));
}
#[test]
fn test_migrate_to_downgrade() {
let mgr = SchemaEvolutionManager::new(5);
let err = mgr.migrate_to(3).expect_err("downgrade should fail");
assert!(matches!(err, MigrationError::Downgrade { from: 5, to: 3 }));
}
#[test]
fn test_migrate_increments_applied_count() {
let mgr = SchemaEvolutionManager::new(1);
mgr.register_migration(simple_migration(1, 2, make_add_op("a")));
mgr.register_migration(simple_migration(2, 3, make_add_op("b")));
assert_eq!(mgr.applied_count(), 0);
mgr.migrate_to(3).expect("ok");
assert_eq!(mgr.applied_count(), 2);
}
#[test]
fn test_can_migrate() {
let mgr = SchemaEvolutionManager::new(1);
mgr.register_migration(simple_migration(1, 2, make_add_op("x")));
assert!(mgr.can_migrate(1, 2));
assert!(!mgr.can_migrate(1, 3));
assert!(!mgr.can_migrate(2, 1)); }
#[test]
fn test_field_names_added_removed() {
let ops = vec![
make_add_op("new_col"),
make_drop_op("old_col"),
FieldMigration::RenameField {
from: "alpha".to_owned(),
to: "beta".to_owned(),
},
];
let m = SchemaMigration::new(1, 2, "mixed", ops);
assert_eq!(m.field_names_added(), vec!["new_col"]);
assert_eq!(m.field_names_removed(), vec!["old_col"]);
}
#[test]
fn test_registered_versions() {
let mgr = SchemaEvolutionManager::new(1);
mgr.register_migration(simple_migration(3, 4, make_add_op("z")));
mgr.register_migration(simple_migration(1, 2, make_add_op("a")));
mgr.register_migration(simple_migration(2, 3, make_add_op("b")));
assert_eq!(mgr.registered_versions(), vec![1, 2, 3]);
}
#[test]
fn test_is_forward() {
let forward = SchemaMigration::new(1, 2, "fwd", vec![]);
assert!(forward.is_forward());
let same = SchemaMigration::new(2, 2, "same", vec![]);
assert!(!same.is_forward());
}
}