Skip to main content

dbx_core/storage/
partition.rs

1//! 파티셔닝 — Range / Hash / List 파티션 키 기반 물리적 분할
2//!
3//! 파티션된 테이블은 N개의 sub-테이블 (`table__p_part_0`, ...) 로 저장됩니다.
4//! 라우팅 함수가 key 값 → sub-table 이름을 결정합니다.
5//!
6//! # 사용 예
7//!
8//! ```rust
9//! use dbx_core::storage::partition::{PartitionMap, PartitionType, PartitionValue};
10//!
11//! let map = PartitionMap {
12//!     table: "users".into(),
13//!     partition_type: PartitionType::Hash {
14//!         column: "id".into(),
15//!         num_partitions: 4,
16//!     },
17//!     num_partitions: 4,
18//! };
19//!
20//! let sub_table = map.route_key(&PartitionValue::Int(42));
21//! assert!(sub_table.starts_with("users__p_part_"));
22//! ```
23
24/// 파티션 키 값 — i64 정수, 문자열, 부동소수점 지원
25#[derive(Debug, Clone, PartialEq)]
26pub enum PartitionValue {
27    Int(i64),
28    Float(f64),
29    Text(String),
30}
31
32impl PartitionValue {
33    fn as_i64(&self) -> i64 {
34        match self {
35            PartitionValue::Int(v) => *v,
36            PartitionValue::Float(v) => *v as i64,
37            PartitionValue::Text(s) => s.parse::<i64>().unwrap_or(0),
38        }
39    }
40
41    fn to_string_repr(&self) -> String {
42        match self {
43            PartitionValue::Int(v) => v.to_string(),
44            PartitionValue::Float(v) => v.to_string(),
45            PartitionValue::Text(s) => s.clone(),
46        }
47    }
48}
49
50/// 파티션 라우팅 결과
51#[derive(Debug, Clone, PartialEq)]
52pub enum RouteResult {
53    /// 기존 파티션으로 정상 라우팅됨 (서브테이블 이름 반환)
54    Routed(String),
55    /// 자동 확장이 필요함 (새로운 서브테이블 이름, 추가될 범위 (low, high))
56    NeedsExpansion {
57        new_table: String,
58        new_bounds: (i64, i64),
59    },
60}
61
62/// 파티션 타입
63#[derive(Debug, Clone)]
64pub enum PartitionType {
65    /// 범위 파티션: 각 (low, high) 범위가 하나의 파티션 [low, high)
66    Range {
67        column: String,
68        bounds: Vec<(i64, i64)>,
69        /// 자동 확장 설정: (간격, 최대 파티션 개수)
70        auto_expand_interval: Option<(i64, usize)>,
71    },
72    /// 해시 파티션: FNV1a 해시 후 num_partitions로 모듈러
73    Hash {
74        column: String,
75        num_partitions: usize,
76    },
77    /// 리스트 파티션: 각 파티션이 특정 값 목록 소유
78    List {
79        column: String,
80        values: Vec<Vec<String>>,
81    },
82}
83
84/// 파티션 맵 — 테이블 하나의 파티션 구성 전체를 담음
85#[derive(Debug, Clone)]
86pub struct PartitionMap {
87    /// 원본 테이블 이름
88    pub table: String,
89    /// 파티션 타입
90    pub partition_type: PartitionType,
91    /// 총 파티션 수
92    pub num_partitions: usize,
93}
94
95impl PartitionMap {
96    /// key 값을 받아 sub-table 이름 반환
97    ///
98    /// 반환 형식: `{table}__p_part_{idx}`
99    pub fn route_key(&self, key_value: &PartitionValue) -> String {
100        let idx = self.partition_index(key_value);
101        format!("{}__p_part_{}", self.table, idx)
102    }
103
104    fn partition_index(&self, key_value: &PartitionValue) -> usize {
105        match &self.partition_type {
106            PartitionType::Hash { num_partitions, .. } => {
107                let s = key_value.to_string_repr();
108                let h = fnv1a_hash(s.as_bytes());
109                h % num_partitions
110            }
111            PartitionType::Range { bounds, .. } => {
112                let v = key_value.as_i64();
113                bounds
114                    .iter()
115                    .position(|(lo, hi)| v >= *lo && v < *hi)
116                    .unwrap_or(self.num_partitions.saturating_sub(1))
117            }
118            PartitionType::List { values, .. } => {
119                let s = key_value.to_string_repr();
120                values
121                    .iter()
122                    .position(|group| group.iter().any(|v| v == &s))
123                    .unwrap_or(0)
124            }
125        }
126    }
127
128    /// WHERE 조건값으로 스캔할 파티션 목록 반환 (Pruning)
129    ///
130    /// - `filter_value = None` → 모든 파티션 반환 (full scan)
131    /// - `filter_value = Some(v)` → 해당 v가 속한 파티션만 반환
132    pub fn pruned_partitions(&self, filter_value: Option<&PartitionValue>) -> Vec<String> {
133        match filter_value {
134            None => (0..self.num_partitions)
135                .map(|i| format!("{}__p_part_{}", self.table, i))
136                .collect(),
137            Some(v) => vec![self.route_key(v)],
138        }
139    }
140
141    /// 모든 파티션의 sub-table 이름 반환
142    pub fn all_partitions(&self) -> Vec<String> {
143        self.pruned_partitions(None)
144    }
145
146    /// 자동 확장이 필요한지 파악하는 함수
147    pub fn route_or_expand(&self, key_value: &PartitionValue) -> RouteResult {
148        match &self.partition_type {
149            PartitionType::Range {
150                bounds,
151                auto_expand_interval,
152                ..
153            } => {
154                let v = key_value.as_i64();
155
156                // 기존 bounds 내에 있는지 확인
157                if let Some(pos) = bounds.iter().position(|(lo, hi)| v >= *lo && v < *hi) {
158                    return RouteResult::Routed(format!("{}__p_part_{}", self.table, pos));
159                }
160
161                if let Some((interval, max_parts)) = auto_expand_interval
162                    && self.num_partitions < *max_parts
163                {
164                    // 범위를 벗어났고 확장 가능함
165                    let last_hi = bounds.last().map(|(_, hi)| *hi).unwrap_or(0);
166                    if v >= last_hi {
167                        // 현재 v를 포함할 수 있는 범위 계산
168                        let diff = v - last_hi;
169                        let steps = (diff / interval) + 1;
170                        let new_hi = last_hi + steps * interval;
171
172                        return RouteResult::NeedsExpansion {
173                            new_table: format!("{}__p_part_{}", self.table, self.num_partitions),
174                            new_bounds: (last_hi, new_hi),
175                        };
176                    } else {
177                        // v < lo (첫 파티션보다 작은 경우 -> 과거 데이터)
178                        // 현재는 가장 과거 구간 확장은 복잡하므로 MVP에서는 그대로 `Routed` 처리하거나, 확장 안됨.
179                        // 가장 가까운 0번 파티션 반환
180                        return RouteResult::Routed(format!("{}__p_part_0", self.table));
181                    }
182                }
183
184                // 자동 확장이 켜져있지 않거나 최대 파티션에 도달한 경우 = 마지막 파티션 반환
185                let idx = self.num_partitions.saturating_sub(1);
186                RouteResult::Routed(format!("{}__p_part_{}", self.table, idx))
187            }
188            _ => RouteResult::Routed(self.route_key(key_value)),
189        }
190    }
191}
192
193/// FNV-1a 해시 (32-bit) — 결정론적, 가벼운 비암호학적 해시
194fn fnv1a_hash(data: &[u8]) -> usize {
195    const FNV_OFFSET: u32 = 2_166_136_261;
196    const FNV_PRIME: u32 = 16_777_619;
197    let mut hash = FNV_OFFSET;
198    for &byte in data {
199        hash ^= byte as u32;
200        hash = hash.wrapping_mul(FNV_PRIME);
201    }
202    hash as usize
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_hash_partition_routing() {
211        let map = PartitionMap {
212            table: "users".into(),
213            partition_type: PartitionType::Hash {
214                column: "id".into(),
215                num_partitions: 4,
216            },
217            num_partitions: 4,
218        };
219        let t0 = map.route_key(&PartitionValue::Int(0));
220        let t3 = map.route_key(&PartitionValue::Int(3));
221        assert!(
222            t0.contains("part_"),
223            "서브테이블 이름에 part_ 포함되어야 함"
224        );
225        // 다른 값이 다른 파티션에 라우팅된다 (항상 참은 아니지만 0,3은 다름)
226        // 적어도 테이블 이름이 올바른지 확인
227        assert!(t0.starts_with("users__p_part_"));
228        assert!(t3.starts_with("users__p_part_"));
229    }
230
231    #[test]
232    fn test_hash_partition_all_valid_indices() {
233        let n = 8usize;
234        let map = PartitionMap {
235            table: "logs".into(),
236            partition_type: PartitionType::Hash {
237                column: "id".into(),
238                num_partitions: n,
239            },
240            num_partitions: n,
241        };
242        for i in 0i64..100 {
243            let sub = map.route_key(&PartitionValue::Int(i));
244            let idx: usize = sub.split('_').last().unwrap().parse().unwrap();
245            assert!(idx < n, "인덱스 {}가 범위 이상", idx);
246        }
247    }
248
249    #[test]
250    fn test_range_partition_routing() {
251        let map = PartitionMap {
252            table: "orders".into(),
253            partition_type: PartitionType::Range {
254                column: "amount".into(),
255                bounds: vec![(0, 100), (100, 500), (500, 10000)],
256                auto_expand_interval: None,
257            },
258            num_partitions: 3,
259        };
260        // 각 범위에 맞게 라우팅
261        let p0 = map.route_key(&PartitionValue::Int(50));
262        let p1 = map.route_key(&PartitionValue::Int(200));
263        let p2 = map.route_key(&PartitionValue::Int(1000));
264        assert!(p0.ends_with("_0"));
265        assert!(p1.ends_with("_1"));
266        assert!(p2.ends_with("_2"));
267    }
268
269    #[test]
270    fn test_range_partition_pruning() {
271        let map = PartitionMap {
272            table: "logs".into(),
273            partition_type: PartitionType::Range {
274                column: "ts".into(),
275                bounds: vec![(0, 1000), (1000, 2000), (2000, 3000)],
276                auto_expand_interval: None,
277            },
278            num_partitions: 3,
279        };
280        let partitions = map.pruned_partitions(Some(&PartitionValue::Int(1500)));
281        assert_eq!(partitions.len(), 1, "단일 파티션만 스캔해야 함");
282        assert!(partitions[0].ends_with("_1"), "1000-2000 범위는 파티션 1");
283
284        // 필터 없으면 전체 스캔
285        let all = map.pruned_partitions(None);
286        assert_eq!(all.len(), 3);
287    }
288
289    #[test]
290    fn test_list_partition_routing() {
291        let map = PartitionMap {
292            table: "regions".into(),
293            partition_type: PartitionType::List {
294                column: "country".into(),
295                values: vec![
296                    vec!["KR".into(), "JP".into()],
297                    vec!["US".into(), "CA".into()],
298                ],
299            },
300            num_partitions: 2,
301        };
302        let kr = map.route_key(&PartitionValue::Text("KR".into()));
303        let us = map.route_key(&PartitionValue::Text("US".into()));
304        assert!(kr.ends_with("_0"));
305        assert!(us.ends_with("_1"));
306    }
307
308    #[test]
309    fn test_all_partitions() {
310        let map = PartitionMap {
311            table: "data".into(),
312            partition_type: PartitionType::Hash {
313                column: "id".into(),
314                num_partitions: 5,
315            },
316            num_partitions: 5,
317        };
318        let all = map.all_partitions();
319        assert_eq!(all.len(), 5);
320        for (i, name) in all.iter().enumerate() {
321            assert_eq!(*name, format!("data__p_part_{}", i));
322        }
323    }
324
325    #[test]
326    fn test_fnv1a_hash_deterministic() {
327        let h1 = fnv1a_hash(b"hello");
328        let h2 = fnv1a_hash(b"hello");
329        assert_eq!(h1, h2, "FNV1a는 결정론적이어야 함");
330
331        let h3 = fnv1a_hash(b"world");
332        assert_ne!(h1, h3, "다른 입력은 다른 해시");
333    }
334}
335
336// ═══════════════════════════════════════════
337// Phase 3 Synergy: Stats / Lifecycle / Tier
338// ═══════════════════════════════════════════
339
340/// 파티션별 통계 정보 — 쿼리 옵티마이저에 활용
341///
342/// # 사용 예
343/// ```rust
344/// use dbx_core::storage::partition::PartitionStats;
345/// let stats = PartitionStats {
346///     row_count: 1000,
347///     min_value: 0,
348///     max_value: 999,
349///     null_count: 5,
350///     distinct_count: 990,
351/// };
352/// assert_eq!(stats.row_count, 1000);
353/// ```
354#[derive(Debug, Clone, Default)]
355pub struct PartitionStats {
356    /// 파티션 내 총 행 수
357    pub row_count: usize,
358    /// 파티션 키의 최솟값
359    pub min_value: i64,
360    /// 파티션 키의 최댓값
361    pub max_value: i64,
362    /// 파티션 키의 NULL 수
363    pub null_count: usize,
364    /// 파티션 키의 고유값 수 (Distinct count)
365    pub distinct_count: usize,
366}
367
368/// 파티션 수명 주기 정책 — 자동 아카이빙 및 삭제
369///
370/// # 사용 예
371/// ```rust
372/// use dbx_core::storage::partition::PartitionLifecycle;
373/// let lc = PartitionLifecycle { archive_after_days: 90, delete_after_days: 365 };
374/// assert_eq!(lc.archive_after_days, 90);
375/// ```
376#[derive(Debug, Clone)]
377pub struct PartitionLifecycle {
378    /// N일 이상 지난 파티션을 고압축(아카이브) 상태로 전환
379    pub archive_after_days: u32,
380    /// N일 이상 지난 파티션을 삭제
381    pub delete_after_days: u32,
382}
383
384/// 파티션의 스토리지 티어 힌트 — Hot / Warm / Cold 분류
385///
386/// # 사용 예
387/// ```rust
388/// use dbx_core::storage::partition::PartitionTierHint;
389/// assert_eq!(PartitionTierHint::default(), PartitionTierHint::Hot);
390/// ```
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
392pub enum PartitionTierHint {
393    /// 최근 데이터 — Delta/Cache (Tier 1-2)에 우선 배치
394    #[default]
395    Hot,
396    /// 중간 데이터 — WOS (Tier 3)에 배치
397    Warm,
398    /// 오래된 데이터 — ROS, 고압축 (Tier 5)에 배치
399    Cold,
400}