use crate::{error::OxenError, model::LocalRepository};
use colored::Colorize;
pub mod m20260408_add_workspace_name_index;
pub use m20260408_add_workspace_name_index::AddWorkspaceNameIndexMigration;
pub mod m20260626_migrate_merkle_nodes_to_lmdb;
pub use m20260626_migrate_merkle_nodes_to_lmdb::MerkleNodesToLmdbMigration;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr, VariantNames};
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
EnumString,
VariantNames,
Display,
IntoStaticStr,
Serialize,
Deserialize,
utoipa::ToSchema,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")] pub enum Direction {
Up,
Down,
}
impl Default for Direction {
fn default() -> Self {
Direction::Up
}
}
pub trait Migrate: Send + Sync {
fn up(&self, repo: LocalRepository) -> Result<(), OxenError>;
fn down(&self, repo: LocalRepository) -> Result<(), OxenError>;
fn is_needed(&self, repo: &LocalRepository) -> Result<bool, OxenError>;
fn is_applicable(
&self,
direction: Direction,
repo: &LocalRepository,
) -> Result<bool, OxenError> {
match direction {
Direction::Up => self.is_needed(repo),
Direction::Down => Ok(true),
}
}
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
}
pub const ALL_MIGRATIONS: [&dyn Migrate; 2] =
[&AddWorkspaceNameIndexMigration, &MerkleNodesToLmdbMigration];
pub fn all_migrations(migration_name: &str) -> Option<&'static dyn Migrate> {
for migration in ALL_MIGRATIONS.iter() {
if migration_name == migration.name() {
return Some(*migration);
}
}
None
}
#[derive(Debug, thiserror::Error)]
pub enum MigrationResult {
#[error("Nothing to do: '{migration_name}' ({direction}) is applicable, but not needed.")]
IsApplicableButOptional {
direction: Direction,
migration_name: &'static str,
},
#[error("Nothing to do: '{migration_name}' ({direction}) is not needed nor is it applicable.")]
NotNeededNorApplicable {
direction: Direction,
migration_name: &'static str,
},
#[error("Successfully applied migration {direction} {migration_name}")]
Success {
direction: Direction,
migration_name: &'static str,
},
}
impl MigrationResult {
pub fn did_run(&self) -> bool {
matches!(self, Self::Success { .. })
}
pub fn as_hint(&self, is_server: bool) -> String {
let msg = self.to_string();
match self {
Self::IsApplicableButOptional { .. } => format!(
"{msg} You must run with {} to apply it.",
(if is_server {
"run_optional=true"
} else {
"--run-optional"
})
.yellow()
),
_ => msg,
}
}
}
pub fn try_apply_migration(
migration: &dyn Migrate,
direction: Direction,
run_optional: bool,
repo: LocalRepository,
) -> Result<MigrationResult, OxenError> {
let migration_name = migration.name();
if matches!(direction, Direction::Up) && migration.is_needed(&repo)? {
migration.up(repo)?;
return Ok(MigrationResult::Success {
direction,
migration_name,
});
}
if migration.is_applicable(direction, &repo)? {
match direction {
Direction::Down => {
migration.down(repo)?;
Ok(MigrationResult::Success {
direction,
migration_name,
})
}
Direction::Up if run_optional => {
migration.up(repo)?;
Ok(MigrationResult::Success {
direction,
migration_name,
})
}
_ => Ok(MigrationResult::IsApplicableButOptional {
direction,
migration_name,
}),
}
} else {
Ok(MigrationResult::NotNeededNorApplicable {
direction,
migration_name,
})
}
}