use crate::error::{Error, Result};
use crate::handle::Tx;
type UpFn = Box<dyn Fn(&Tx<'_>) -> Result<()> + Send + Sync>;
pub trait MigrationStep: Send + Sync + 'static {
#[allow(clippy::wrong_self_convention)]
fn from_version(&self) -> u32;
fn to_version(&self) -> u32;
fn up(&self, tx: &Tx<'_>) -> Result<()>;
fn down(&self, _tx: &Tx<'_>) -> Result<()> {
Err(Error::Migration(
"down 마이그레이션이 구현되지 않았습니다".into(),
))
}
}
pub struct Migration {
from: u32,
to: u32,
up: UpFn,
}
impl Migration {
pub fn sql(from: u32, to: u32, sql: impl Into<String>) -> Self {
let sql = sql.into();
Self::code(from, to, move |tx| tx.execute_batch(&sql))
}
pub fn sql_batch(from: u32, to: u32, sql: impl Into<String>) -> Self {
Self::sql(from, to, sql)
}
pub fn code(
from: u32,
to: u32,
f: impl Fn(&Tx<'_>) -> Result<()> + Send + Sync + 'static,
) -> Self {
Self {
from,
to,
up: Box::new(f),
}
}
#[allow(clippy::self_named_constructors, clippy::wrong_self_convention)]
pub fn from_step(step: impl MigrationStep) -> Self {
let step = std::sync::Arc::new(step);
Self {
from: step.from_version(),
to: step.to_version(),
up: Box::new(move |tx| step.up(tx)),
}
}
#[allow(clippy::wrong_self_convention)]
pub fn from_version(&self) -> u32 {
self.from
}
pub fn to_version(&self) -> u32 {
self.to
}
pub(crate) fn run_up(&self, tx: &Tx<'_>) -> Result<()> {
(self.up)(tx)
}
}
pub(crate) fn plan_chain<'a>(
steps: &[&'a Migration],
current: u32,
target: u32,
) -> Result<Vec<&'a Migration>> {
for s in steps {
if s.to <= s.from {
return Err(Error::Migration(format!(
"잘못된 마이그레이션 구간: {} -> {} (to는 from보다 커야 함)",
s.from, s.to
)));
}
}
let mut froms: Vec<u32> = steps.iter().map(|s| s.from).collect();
froms.sort_unstable();
if let Some(w) = froms.windows(2).find(|w| w[0] == w[1]) {
return Err(Error::Migration(format!(
"중복 마이그레이션 구간: from={} 스텝이 2개 이상",
w[0]
)));
}
if current > target {
return Err(Error::Migration(format!(
"다운그레이드는 지원하지 않습니다 (DB={current}, 코드={target}) — down은 수동 실행"
)));
}
let mut plan = Vec::new();
let mut v = current;
while v < target {
let Some(step) = steps.iter().copied().find(|s| s.from == v) else {
return Err(Error::Migration(format!(
"마이그레이션 체인 갭: v{v} -> v{target} 구간을 잇는 스텝이 없습니다"
)));
};
if step.to > target {
return Err(Error::Migration(format!(
"마이그레이션 스텝이 목표를 지나칩니다: {} -> {} (목표 {target})",
step.from, step.to
)));
}
v = step.to;
plan.push(step);
}
Ok(plan)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chain_plan() {
let owned = [
Migration::sql(1, 2, "SELECT 1"),
Migration::sql(2, 3, "SELECT 1"),
];
let steps: Vec<&Migration> = owned.iter().collect();
assert_eq!(plan_chain(&steps, 1, 3).unwrap().len(), 2);
assert_eq!(plan_chain(&steps, 2, 3).unwrap().len(), 1);
assert_eq!(plan_chain(&steps, 3, 3).unwrap().len(), 0);
assert!(plan_chain(&steps, 0, 3).is_err(), "갭(0->1)");
assert!(plan_chain(&steps, 3, 1).is_err(), "다운그레이드");
let dup_owned = [Migration::sql(1, 2, ""), Migration::sql(1, 3, "")];
let dup: Vec<&Migration> = dup_owned.iter().collect();
assert!(plan_chain(&dup, 1, 3).is_err(), "중복 from");
}
}