1use crate::error::{Error, Result};
4use crate::handle::Tx;
5
6type UpFn = Box<dyn Fn(&Tx<'_>) -> Result<()> + Send + Sync>;
8
9pub trait MigrationStep: Send + Sync + 'static {
11 #[allow(clippy::wrong_self_convention)]
13 fn from_version(&self) -> u32;
14 fn to_version(&self) -> u32;
16 fn up(&self, tx: &Tx<'_>) -> Result<()>;
18 fn down(&self, _tx: &Tx<'_>) -> Result<()> {
20 Err(Error::Migration(
21 "down 마이그레이션이 구현되지 않았습니다".into(),
22 ))
23 }
24}
25
26pub struct Migration {
28 from: u32,
29 to: u32,
30 up: UpFn,
31}
32
33impl Migration {
34 pub fn sql(from: u32, to: u32, sql: impl Into<String>) -> Self {
36 let sql = sql.into();
37 Self::code(from, to, move |tx| tx.execute_batch(&sql))
38 }
39
40 pub fn sql_batch(from: u32, to: u32, sql: impl Into<String>) -> Self {
42 Self::sql(from, to, sql)
43 }
44
45 pub fn code(
47 from: u32,
48 to: u32,
49 f: impl Fn(&Tx<'_>) -> Result<()> + Send + Sync + 'static,
50 ) -> Self {
51 Self {
52 from,
53 to,
54 up: Box::new(f),
55 }
56 }
57
58 #[allow(clippy::self_named_constructors, clippy::wrong_self_convention)]
60 pub fn from_step(step: impl MigrationStep) -> Self {
61 let step = std::sync::Arc::new(step);
62 Self {
63 from: step.from_version(),
64 to: step.to_version(),
65 up: Box::new(move |tx| step.up(tx)),
66 }
67 }
68
69 #[allow(clippy::wrong_self_convention)]
71 pub fn from_version(&self) -> u32 {
72 self.from
73 }
74
75 pub fn to_version(&self) -> u32 {
77 self.to
78 }
79
80 pub(crate) fn run_up(&self, tx: &Tx<'_>) -> Result<()> {
82 (self.up)(tx)
83 }
84}
85
86pub(crate) fn plan_chain<'a>(
90 steps: &[&'a Migration],
91 current: u32,
92 target: u32,
93) -> Result<Vec<&'a Migration>> {
94 for s in steps {
96 if s.to <= s.from {
97 return Err(Error::Migration(format!(
98 "잘못된 마이그레이션 구간: {} -> {} (to는 from보다 커야 함)",
99 s.from, s.to
100 )));
101 }
102 }
103 let mut froms: Vec<u32> = steps.iter().map(|s| s.from).collect();
105 froms.sort_unstable();
106 if let Some(w) = froms.windows(2).find(|w| w[0] == w[1]) {
107 return Err(Error::Migration(format!(
108 "중복 마이그레이션 구간: from={} 스텝이 2개 이상",
109 w[0]
110 )));
111 }
112
113 if current > target {
114 return Err(Error::Migration(format!(
115 "다운그레이드는 지원하지 않습니다 (DB={current}, 코드={target}) — down은 수동 실행"
116 )));
117 }
118
119 let mut plan = Vec::new();
121 let mut v = current;
122 while v < target {
123 let Some(step) = steps.iter().copied().find(|s| s.from == v) else {
124 return Err(Error::Migration(format!(
125 "마이그레이션 체인 갭: v{v} -> v{target} 구간을 잇는 스텝이 없습니다"
126 )));
127 };
128 if step.to > target {
129 return Err(Error::Migration(format!(
130 "마이그레이션 스텝이 목표를 지나칩니다: {} -> {} (목표 {target})",
131 step.from, step.to
132 )));
133 }
134 v = step.to;
135 plan.push(step);
136 }
137 Ok(plan)
138}
139
140#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
148 fn chain_plan() {
149 let owned = [
150 Migration::sql(1, 2, "SELECT 1"),
151 Migration::sql(2, 3, "SELECT 1"),
152 ];
153 let steps: Vec<&Migration> = owned.iter().collect();
154 assert_eq!(plan_chain(&steps, 1, 3).unwrap().len(), 2);
155 assert_eq!(plan_chain(&steps, 2, 3).unwrap().len(), 1);
156 assert_eq!(plan_chain(&steps, 3, 3).unwrap().len(), 0);
157 assert!(plan_chain(&steps, 0, 3).is_err(), "갭(0->1)");
158 assert!(plan_chain(&steps, 3, 1).is_err(), "다운그레이드");
159
160 let dup_owned = [Migration::sql(1, 2, ""), Migration::sql(1, 3, "")];
161 let dup: Vec<&Migration> = dup_owned.iter().collect();
162 assert!(plan_chain(&dup, 1, 3).is_err(), "중복 from");
163 }
164}