Skip to main content

roomrs_core/
migration.rs

1//! 마이그레이션 스텝 · 체인 러너 (명세 §8, 결정 로그 6)
2
3use crate::error::{Error, Result};
4use crate::handle::Tx;
5
6/// 마이그레이션 실행 클로저 타입
7type UpFn = Box<dyn Fn(&Tx<'_>) -> Result<()> + Send + Sync>;
8
9/// 수동 코드 스텝 trait — (from,to) 쌍 모델 [C-3] (명세 §8.3)
10pub trait MigrationStep: Send + Sync + 'static {
11    /// 시작 버전 (명세 [C-3] 확정 이름 — 관례 린트 예외)
12    #[allow(clippy::wrong_self_convention)]
13    fn from_version(&self) -> u32;
14    /// 도착 버전
15    fn to_version(&self) -> u32;
16    /// 정방향 실행 — 트랜잭션 안에서 호출된다
17    fn up(&self, tx: &Tx<'_>) -> Result<()>;
18    /// 역방향(선택) — v1 러너는 up만 실행. down은 사용자 수동 실행용.
19    fn down(&self, _tx: &Tx<'_>) -> Result<()> {
20        Err(Error::Migration(
21            "down 마이그레이션이 구현되지 않았습니다".into(),
22        ))
23    }
24}
25
26/// 마이그레이션 스텝 — 세 소스(SQL 파일/인라인 SQL/코드)의 공통 표현 (명세 §8.2)
27pub struct Migration {
28    from: u32,
29    to: u32,
30    up: UpFn,
31}
32
33impl Migration {
34    /// 인라인 SQL 스텝 — `Migration::sql(1, 2, "ALTER TABLE …")` (명세 §8.2)
35    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    /// 여러 문장 SQL 스텝 — execute_batch와 동일(별칭, 의도 명시용)
41    pub fn sql_batch(from: u32, to: u32, sql: impl Into<String>) -> Self {
42        Self::sql(from, to, sql)
43    }
44
45    /// 코드 스텝
46    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    /// trait 구현체 래핑 (명세 §8.3)
59    #[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    /// 시작 버전 (명세 [C-3] 확정 이름)
70    #[allow(clippy::wrong_self_convention)]
71    pub fn from_version(&self) -> u32 {
72        self.from
73    }
74
75    /// 도착 버전
76    pub fn to_version(&self) -> u32 {
77        self.to
78    }
79
80    /// 정방향 실행 — 러너 전용
81    pub(crate) fn run_up(&self, tx: &Tx<'_>) -> Result<()> {
82        (self.up)(tx)
83    }
84}
85
86/// 체인 검증 + 실행 계획 — current에서 target까지의 스텝 나열.
87/// 중복 구간 = 에러, 갭 = `Err(None 계획)` 대신 명확한 에러 메시지 반환.
88/// 참조 슬라이스를 받는다 — 등록 스텝 + 합성 스텝(명세 §8.4)을 복제 없이 합친다.
89pub(crate) fn plan_chain<'a>(
90    steps: &[&'a Migration],
91    current: u32,
92    target: u32,
93) -> Result<Vec<&'a Migration>> {
94    // 유효성: to > from
95    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    // 같은 from 중복 = 에러 (명세 §8.3 같은 구간 중복)
104    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    // 그리디 체인 — from==v 스텝 순차 적용
120    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// Tx::execute_batch는 handle.rs로 이동 (M-2) — 배치 write 무효화 수집 경로 공유
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    /// 체인 계획 — 정상/갭/중복/다운그레이드
147    #[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}