Skip to main content

dbx_core/engine/
index_versioning.rs

1//! Index Versioning — Phase 2: Section 6.2
2//!
3//! 무중단 REINDEX 지원: 인덱스 메타데이터 버전 관리
4
5use crate::error::{DbxError, DbxResult};
6use std::collections::HashMap;
7use std::sync::RwLock;
8
9/// 인덱스 메타데이터
10#[derive(Debug, Clone)]
11pub struct IndexMeta {
12    /// 인덱스 이름
13    pub name: String,
14    /// 대상 테이블
15    pub table: String,
16    /// 인덱스 컬럼
17    pub columns: Vec<String>,
18    /// 인덱스 종류
19    pub index_type: IndexType,
20    /// 버전 번호
21    pub version: u64,
22    /// 빌드 상태
23    pub status: IndexStatus,
24}
25
26/// 인덱스 종류
27#[derive(Debug, Clone, PartialEq)]
28pub enum IndexType {
29    Hash,
30    BTree,
31    Bitmap,
32}
33
34/// 인덱스 빌드 상태
35#[derive(Debug, Clone, PartialEq)]
36pub enum IndexStatus {
37    /// 빌드 중 (이전 버전 사용 가능)
38    Building,
39    /// 사용 가능
40    Ready,
41    /// 비활성화
42    Disabled,
43}
44
45/// 인덱스 버전 관리자
46///
47/// 인덱스를 무중단으로 재구축합니다:
48/// 1. 새 버전 생성 (Building 상태)
49/// 2. 백그라운드에서 인덱스 빌드
50/// 3. Ready 상태로 전환 → 이전 버전 제거
51pub struct IndexVersionManager {
52    /// index_name → 버전 히스토리
53    versions: RwLock<HashMap<String, Vec<IndexMeta>>>,
54    /// index_name → 현재 활성 버전
55    active: RwLock<HashMap<String, u64>>,
56}
57
58impl IndexVersionManager {
59    pub fn new() -> Self {
60        Self {
61            versions: RwLock::new(HashMap::new()),
62            active: RwLock::new(HashMap::new()),
63        }
64    }
65
66    /// 인덱스 생성
67    pub fn create_index(
68        &self,
69        name: &str,
70        table: &str,
71        columns: Vec<String>,
72        index_type: IndexType,
73    ) -> DbxResult<u64> {
74        let meta = IndexMeta {
75            name: name.to_string(),
76            table: table.to_string(),
77            columns,
78            index_type,
79            version: 1,
80            status: IndexStatus::Ready,
81        };
82
83        let mut versions = self
84            .versions
85            .write()
86            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
87        let mut active = self
88            .active
89            .write()
90            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
91
92        versions.insert(name.to_string(), vec![meta]);
93        active.insert(name.to_string(), 1);
94
95        Ok(1)
96    }
97
98    /// 무중단 REINDEX 시작 (새 버전을 Building 상태로 생성)
99    pub fn start_reindex(
100        &self,
101        name: &str,
102        columns: Vec<String>,
103        index_type: IndexType,
104    ) -> DbxResult<u64> {
105        let mut versions = self
106            .versions
107            .write()
108            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
109        let history = versions
110            .get_mut(name)
111            .ok_or_else(|| DbxError::Serialization(format!("Index {name} not found")))?;
112
113        let last = history
114            .last()
115            .ok_or_else(|| DbxError::Serialization("Empty history".into()))?;
116        let new_version = last.version + 1;
117
118        history.push(IndexMeta {
119            name: name.to_string(),
120            table: last.table.clone(),
121            columns,
122            index_type,
123            version: new_version,
124            status: IndexStatus::Building,
125        });
126
127        Ok(new_version)
128    }
129
130    /// REINDEX 완료 (Building → Ready, 이전 버전 비활성화)
131    pub fn complete_reindex(&self, name: &str, version: u64) -> DbxResult<()> {
132        let mut versions = self
133            .versions
134            .write()
135            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
136        let mut active = self
137            .active
138            .write()
139            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
140
141        let history = versions
142            .get_mut(name)
143            .ok_or_else(|| DbxError::Serialization(format!("Index {name} not found")))?;
144
145        for meta in history.iter_mut() {
146            if meta.version == version {
147                meta.status = IndexStatus::Ready;
148            } else if meta.status == IndexStatus::Ready {
149                meta.status = IndexStatus::Disabled;
150            }
151        }
152
153        active.insert(name.to_string(), version);
154        Ok(())
155    }
156
157    /// 현재 활성 인덱스 메타데이터 조회
158    pub fn get_active(&self, name: &str) -> DbxResult<IndexMeta> {
159        let versions = self
160            .versions
161            .read()
162            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
163        let active = self
164            .active
165            .read()
166            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
167
168        let active_ver = active
169            .get(name)
170            .ok_or_else(|| DbxError::Serialization(format!("Index {name} not found")))?;
171        let history = versions
172            .get(name)
173            .ok_or_else(|| DbxError::Serialization(format!("Index {name} not found")))?;
174
175        history
176            .iter()
177            .find(|m| m.version == *active_ver)
178            .cloned()
179            .ok_or_else(|| DbxError::Serialization(format!("Version {active_ver} not found")))
180    }
181
182    /// 인덱스 삭제
183    pub fn drop_index(&self, name: &str) -> DbxResult<()> {
184        let mut versions = self
185            .versions
186            .write()
187            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
188        let mut active = self
189            .active
190            .write()
191            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
192
193        versions.remove(name);
194        active.remove(name);
195        Ok(())
196    }
197
198    /// 테이블의 모든 인덱스 조회
199    pub fn list_indexes(&self, table: &str) -> DbxResult<Vec<IndexMeta>> {
200        let versions = self
201            .versions
202            .read()
203            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
204        let active = self
205            .active
206            .read()
207            .map_err(|_| DbxError::Serialization("Lock poisoned".into()))?;
208
209        let mut result = Vec::new();
210        for (name, history) in versions.iter() {
211            if let Some(&active_ver) = active.get(name)
212                && let Some(meta) = history
213                    .iter()
214                    .find(|m| m.version == active_ver && m.table == table)
215            {
216                result.push(meta.clone());
217            }
218        }
219        Ok(result)
220    }
221}
222
223impl Default for IndexVersionManager {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_create_index() {
235        let mgr = IndexVersionManager::new();
236        let ver = mgr
237            .create_index(
238                "idx_users_email",
239                "users",
240                vec!["email".into()],
241                IndexType::Hash,
242            )
243            .unwrap();
244        assert_eq!(ver, 1);
245
246        let meta = mgr.get_active("idx_users_email").unwrap();
247        assert_eq!(meta.table, "users");
248        assert_eq!(meta.status, IndexStatus::Ready);
249    }
250
251    #[test]
252    fn test_reindex_zero_downtime() {
253        let mgr = IndexVersionManager::new();
254        mgr.create_index("idx1", "users", vec!["name".into()], IndexType::Hash)
255            .unwrap();
256
257        // Start reindex (v1 still active)
258        let v2 = mgr
259            .start_reindex(
260                "idx1",
261                vec!["name".into(), "email".into()],
262                IndexType::BTree,
263            )
264            .unwrap();
265        assert_eq!(v2, 2);
266
267        // v1 is still active during build
268        let active = mgr.get_active("idx1").unwrap();
269        assert_eq!(active.version, 1);
270
271        // Complete reindex → v2 becomes active
272        mgr.complete_reindex("idx1", 2).unwrap();
273        let active = mgr.get_active("idx1").unwrap();
274        assert_eq!(active.version, 2);
275        assert_eq!(active.columns.len(), 2);
276        assert_eq!(active.index_type, IndexType::BTree);
277    }
278
279    #[test]
280    fn test_drop_index() {
281        let mgr = IndexVersionManager::new();
282        mgr.create_index("idx1", "users", vec!["name".into()], IndexType::Hash)
283            .unwrap();
284        mgr.drop_index("idx1").unwrap();
285
286        assert!(mgr.get_active("idx1").is_err());
287    }
288
289    #[test]
290    fn test_list_indexes() {
291        let mgr = IndexVersionManager::new();
292        mgr.create_index("idx1", "users", vec!["name".into()], IndexType::Hash)
293            .unwrap();
294        mgr.create_index("idx2", "users", vec!["email".into()], IndexType::BTree)
295            .unwrap();
296        mgr.create_index("idx3", "orders", vec!["id".into()], IndexType::Hash)
297            .unwrap();
298
299        let user_indexes = mgr.list_indexes("users").unwrap();
300        assert_eq!(user_indexes.len(), 2);
301
302        let order_indexes = mgr.list_indexes("orders").unwrap();
303        assert_eq!(order_indexes.len(), 1);
304    }
305}