1#[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#[derive(Debug, Clone, PartialEq)]
52pub enum RouteResult {
53 Routed(String),
55 NeedsExpansion {
57 new_table: String,
58 new_bounds: (i64, i64),
59 },
60}
61
62#[derive(Debug, Clone)]
64pub enum PartitionType {
65 Range {
67 column: String,
68 bounds: Vec<(i64, i64)>,
69 auto_expand_interval: Option<(i64, usize)>,
71 },
72 Hash {
74 column: String,
75 num_partitions: usize,
76 },
77 List {
79 column: String,
80 values: Vec<Vec<String>>,
81 },
82}
83
84#[derive(Debug, Clone)]
86pub struct PartitionMap {
87 pub table: String,
89 pub partition_type: PartitionType,
91 pub num_partitions: usize,
93}
94
95impl PartitionMap {
96 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 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 pub fn all_partitions(&self) -> Vec<String> {
143 self.pruned_partitions(None)
144 }
145
146 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 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 let last_hi = bounds.last().map(|(_, hi)| *hi).unwrap_or(0);
166 if v >= last_hi {
167 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 return RouteResult::Routed(format!("{}__p_part_0", self.table));
181 }
182 }
183
184 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
193fn 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 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 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 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#[derive(Debug, Clone, Default)]
355pub struct PartitionStats {
356 pub row_count: usize,
358 pub min_value: i64,
360 pub max_value: i64,
362 pub null_count: usize,
364 pub distinct_count: usize,
366}
367
368#[derive(Debug, Clone)]
377pub struct PartitionLifecycle {
378 pub archive_after_days: u32,
380 pub delete_after_days: u32,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
392pub enum PartitionTierHint {
393 #[default]
395 Hot,
396 Warm,
398 Cold,
400}