1use std::collections::{BTreeMap, HashMap};
9
10use crate::IndexValue;
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct GroupStats {
15 pub count: u64,
17 pub sum: f64,
20 pub min: Option<IndexValue>,
22 pub max: Option<IndexValue>,
24}
25
26impl GroupStats {
27 pub fn avg(&self) -> Option<f64> {
29 (self.count > 0).then(|| self.sum / self.count as f64)
30 }
31}
32
33#[derive(Debug)]
34struct Group {
35 count: u64,
36 sum: f64,
37 values: BTreeMap<IndexValue, u32>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum AggBy {
44 #[default]
46 Count,
47 Sum,
49 Min,
51 Max,
53}
54
55impl AggBy {
56 pub fn parse(raw: &[u8]) -> Option<AggBy> {
58 if raw.eq_ignore_ascii_case(b"count") {
59 Some(AggBy::Count)
60 } else if raw.eq_ignore_ascii_case(b"sum") {
61 Some(AggBy::Sum)
62 } else if raw.eq_ignore_ascii_case(b"min") {
63 Some(AggBy::Min)
64 } else if raw.eq_ignore_ascii_case(b"max") {
65 Some(AggBy::Max)
66 } else {
67 None
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
74pub struct AggStats {
75 pub groups: u64,
77 pub rows: u64,
79 pub excluded: u64,
81 pub approx_bytes: u64,
84}
85
86#[derive(Debug, Default)]
88pub struct AggSegment {
89 groups: HashMap<Vec<u8>, Group>,
90 rows: HashMap<Vec<u8>, (Vec<u8>, IndexValue)>,
92 excluded: u64,
93 distinct_total: u64,
98 gkey_bytes: u64,
99 row_key_bytes: u64,
100}
101
102impl AggSegment {
103 pub fn new() -> Self {
105 Self::default()
106 }
107
108 #[allow(clippy::missing_panics_doc)]
115 pub fn apply(&mut self, key: &[u8], entry: Option<(Vec<u8>, IndexValue)>, excluded_row: bool) {
116 if let Some((group, val)) = &entry
117 && self.fast_path_same_group(key, group, val)
118 {
119 return;
120 }
121 self.retract_row(key);
122 match entry {
123 Some((group, val)) => {
124 let g = self.groups.entry(group.clone()).or_insert(Group {
125 count: 0,
126 sum: 0.0,
127 values: BTreeMap::new(),
128 });
129 if g.count == 0 {
130 self.gkey_bytes += group.len() as u64;
131 }
132 g.count += 1;
133 g.sum += val.as_f64();
134 let slot = g.values.entry(val.clone()).or_insert(0);
135 *slot += 1;
136 if *slot == 1 {
137 self.distinct_total += 1;
138 }
139 self.row_key_bytes += key.len() as u64 + 10;
140 self.rows.insert(key.to_vec(), (group, val));
141 }
142 None if excluded_row => self.excluded += 1,
143 None => {}
144 }
145 }
146
147 fn fast_path_same_group(&mut self, key: &[u8], group: &[u8], val: &IndexValue) -> bool {
152 let Some((old_group, old_val)) = self.rows.get_mut(key) else { return false };
153 if old_group != group {
154 return false;
155 }
156 if old_val == val {
157 return true; }
159 let g = self.groups.get_mut(group).expect("group of live row");
160 g.sum += val.as_f64() - old_val.as_f64();
161 match g.values.get_mut(old_val) {
162 Some(m) if *m > 1 => *m -= 1,
163 _ => {
164 g.values.remove(old_val);
165 self.distinct_total -= 1;
166 }
167 }
168 let slot = g.values.entry(val.clone()).or_insert(0);
169 *slot += 1;
170 if *slot == 1 {
171 self.distinct_total += 1;
172 }
173 *old_val = val.clone();
174 true
175 }
176
177 fn retract_row(&mut self, key: &[u8]) {
180 if let Some((old_group, old_val)) = self.rows.remove(key) {
181 self.row_key_bytes -= key.len() as u64 + 10;
182 let empty = {
183 let g = self.groups.get_mut(&old_group).expect("group of live row");
184 g.count -= 1;
185 g.sum -= old_val.as_f64();
186 match g.values.get_mut(&old_val) {
187 Some(m) if *m > 1 => *m -= 1,
188 _ => {
189 g.values.remove(&old_val);
190 self.distinct_total -= 1;
191 }
192 }
193 g.count == 0
194 };
195 if empty {
196 self.groups.remove(&old_group);
197 self.gkey_bytes -= old_group.len() as u64;
198 }
199 }
200 }
201
202 pub fn group(&self, group: &[u8]) -> GroupStats {
204 match self.groups.get(group) {
205 Some(g) => GroupStats {
206 count: g.count,
207 sum: g.sum,
208 min: g.values.keys().next().cloned(),
209 max: g.values.keys().next_back().cloned(),
210 },
211 None => GroupStats { count: 0, sum: 0.0, min: None, max: None },
212 }
213 }
214
215 pub fn top_groups(&self, by: AggBy, limit: usize) -> Vec<(Vec<u8>, GroupStats)> {
222 let score_of = |g: &Group| -> f64 {
223 match by {
224 AggBy::Count => g.count as f64,
225 AggBy::Sum => g.sum,
226 AggBy::Max => {
227 g.values.keys().next_back().map_or(f64::NEG_INFINITY, IndexValue::as_f64)
228 }
229 AggBy::Min => g.values.keys().next().map_or(f64::NEG_INFINITY, |v| -v.as_f64()),
230 }
231 };
232 #[allow(clippy::float_cmp)]
235 let better = |a: (f64, &[u8]), b: (f64, &[u8])| a.0 > b.0 || (a.0 == b.0 && a.1 < b.1);
236 let mut top: Vec<(f64, &Vec<u8>)> = Vec::with_capacity(limit.min(1024) + 1);
237 for (k, g) in &self.groups {
238 let cand = (score_of(g), k);
239 if top.len() < limit {
240 top.push(cand);
241 if top.len() == limit {
242 top.sort_by(|a, b| b.0.total_cmp(&a.0).then_with(|| a.1.cmp(b.1)));
243 }
244 } else if let Some(last) = top.last()
245 && better((cand.0, cand.1), (last.0, last.1))
246 {
247 let pos = top.partition_point(|e| better((e.0, e.1), (cand.0, cand.1)));
248 top.insert(pos, cand);
249 top.pop();
250 }
251 }
252 if top.len() < limit {
253 top.sort_by(|a, b| b.0.total_cmp(&a.0).then_with(|| a.1.cmp(b.1)));
254 }
255 top.into_iter().map(|(_, k)| (k.clone(), self.group(k))).collect()
256 }
257
258 pub fn all_groups(&self) -> Vec<(Vec<u8>, GroupStats)> {
262 self.groups.keys().map(|k| (k.clone(), self.group(k))).collect()
263 }
264
265 pub fn contains(&self, key: &[u8]) -> bool {
267 self.rows.contains_key(key)
268 }
269
270 pub fn rows(&self) -> u64 {
275 self.rows.len() as u64
276 }
277
278 pub fn stats(&self) -> AggStats {
285 AggStats {
286 groups: self.groups.len() as u64,
287 rows: self.rows.len() as u64,
288 excluded: self.excluded,
289 approx_bytes: self.gkey_bytes
290 + self.groups.len() as u64 * 64
291 + self.distinct_total * 18
292 + self.row_key_bytes,
293 }
294 }
295
296 #[cfg(test)]
299 pub(crate) fn recompute_stats(&self) -> AggStats {
300 let distinct: u64 = self.groups.values().map(|g| g.values.len() as u64).sum();
301 let gkey: u64 = self.groups.keys().map(|k| k.len() as u64).sum();
302 let rowbytes: u64 = self.rows.keys().map(|k| (k.len() + 10) as u64).sum();
303 AggStats {
304 groups: self.groups.len() as u64,
305 rows: self.rows.len() as u64,
306 excluded: self.excluded,
307 approx_bytes: gkey + self.groups.len() as u64 * 64 + distinct * 18 + rowbytes,
308 }
309 }
310}
311
312pub fn sort_groups(all: &mut [(Vec<u8>, GroupStats)], by: AggBy) {
315 match by {
316 AggBy::Count => all.sort_by(|a, b| b.1.count.cmp(&a.1.count).then_with(|| a.0.cmp(&b.0))),
317 AggBy::Sum => all.sort_by(|a, b| b.1.sum.total_cmp(&a.1.sum).then_with(|| a.0.cmp(&b.0))),
318 AggBy::Min => all.sort_by(|a, b| {
319 match (&a.1.min, &b.1.min) {
320 (Some(x), Some(y)) => x.cmp(y),
321 (Some(_), None) => std::cmp::Ordering::Less,
322 (None, Some(_)) => std::cmp::Ordering::Greater,
323 (None, None) => std::cmp::Ordering::Equal,
324 }
325 .then_with(|| a.0.cmp(&b.0))
326 }),
327 AggBy::Max => all.sort_by(|a, b| {
328 match (&b.1.max, &a.1.max) {
329 (Some(x), Some(y)) => x.cmp(y),
330 (Some(_), None) => std::cmp::Ordering::Less,
331 (None, Some(_)) => std::cmp::Ordering::Greater,
332 (None, None) => std::cmp::Ordering::Equal,
333 }
334 .then_with(|| a.0.cmp(&b.0))
335 }),
336 }
337}
338
339pub fn merge_group(into: &mut GroupStats, part: &GroupStats) {
342 into.count += part.count;
343 into.sum += part.sum;
344 into.min = match (into.min.take(), part.min.clone()) {
345 (Some(a), Some(b)) => Some(if b < a { b } else { a }),
346 (a, b) => a.or(b),
347 };
348 into.max = match (into.max.take(), part.max.clone()) {
349 (Some(a), Some(b)) => Some(if b > a { b } else { a }),
350 (a, b) => a.or(b),
351 };
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 fn seg() -> AggSegment {
359 let mut s = AggSegment::new();
360 for (k, g, v) in [
362 ("o1", "paid", 100),
363 ("o2", "paid", 250),
364 ("o3", "open", 40),
365 ("o4", "paid", 100),
366 ("o5", "open", 999),
367 ] {
368 s.apply(k.as_bytes(), Some((g.as_bytes().to_vec(), IndexValue::I64(v))), false);
369 }
370 s
371 }
372
373 #[test]
374 fn group_stats_exact() {
375 let s = seg();
376 let g = s.group(b"paid");
377 assert_eq!((g.count, g.sum), (3, 450.0));
378 assert_eq!(g.min, Some(IndexValue::I64(100)));
379 assert_eq!(g.max, Some(IndexValue::I64(250)));
380 assert_eq!(g.avg(), Some(150.0));
381 let none = s.group(b"nope");
382 assert_eq!(none.count, 0);
383 assert!(none.min.is_none() && none.avg().is_none());
384 }
385
386 #[test]
387 fn min_max_exact_under_delete_and_update() {
388 let mut s = seg();
389 s.apply(b"o2", None, false);
391 let g = s.group(b"paid");
392 assert_eq!((g.count, g.max.clone()), (2, Some(IndexValue::I64(100))));
393 s.apply(b"o1", None, false);
395 let g = s.group(b"paid");
396 assert_eq!((g.count, g.min.clone()), (1, Some(IndexValue::I64(100))));
397 s.apply(b"o3", Some((b"paid".to_vec(), IndexValue::I64(40))), false);
399 assert_eq!(s.group(b"paid").count, 2);
400 assert_eq!(s.group(b"open").count, 1);
401 assert_eq!(s.group(b"paid").min, Some(IndexValue::I64(40)));
402 s.apply(b"o5", None, false);
404 assert_eq!(s.group(b"open").count, 0);
405 assert_eq!(s.stats().groups, 1);
406 }
407
408 #[test]
409 fn top_groups_all_metrics() {
410 let s = seg();
411 let top = s.top_groups(AggBy::Count, 10);
412 assert_eq!(top[0].0, b"paid".to_vec());
413 let top = s.top_groups(AggBy::Sum, 10);
414 assert_eq!(top[0].0, b"open".to_vec(), "open sum 1039 > paid 450");
415 let top = s.top_groups(AggBy::Min, 10);
416 assert_eq!(top[0].0, b"open".to_vec(), "min ascending: 40 first");
417 let top = s.top_groups(AggBy::Max, 1);
418 assert_eq!(top.len(), 1);
419 assert_eq!(top[0].0, b"open".to_vec(), "max 999");
420 }
421
422 #[test]
423 fn excluded_counted_and_merge() {
424 let mut s = seg();
425 s.apply(b"bad1", None, true);
426 s.apply(b"bad2", None, true);
427 assert_eq!(s.stats().excluded, 2);
428 assert!(s.contains(b"o1") && !s.contains(b"bad1"));
429 let mut a = s.group(b"paid");
431 let b = seg().group(b"paid");
432 merge_group(&mut a, &b);
433 assert_eq!((a.count, a.sum), (6, 900.0));
434 assert_eq!(a.min, Some(IndexValue::I64(100)));
435 assert_eq!(a.max, Some(IndexValue::I64(250)));
436 let mut e = GroupStats { count: 0, sum: 0.0, min: None, max: None };
438 merge_group(&mut e, &a);
439 assert_eq!(e.max, Some(IndexValue::I64(250)));
440 }
441
442 #[test]
443 fn stats_bytes_nonzero() {
444 let s = seg();
445 let st = s.stats();
446 assert_eq!((st.groups, st.rows), (2, 5));
447 assert!(st.approx_bytes > 0);
448 }
449
450 #[test]
455 fn running_stats_never_drift_from_the_walking_reference() {
456 let mut s = AggSegment::new();
457 let check = |s: &AggSegment, at: &str| {
458 assert_eq!(s.stats(), s.recompute_stats(), "counter drift after {at}");
459 };
460 let mut x = 0x2545F491u64;
461 let mut next = move || {
462 x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
463 (x >> 33) as u32
464 };
465 let groups = [b"eng".as_slice(), b"sales", b"ops"];
466 for round in 0..300u32 {
467 let key = format!("r:{}", next() % 30);
468 match next() % 6 {
469 0 => s.apply(key.as_bytes(), None, false),
470 1 => s.apply(key.as_bytes(), None, true), _ => {
472 let g = groups[(next() % 3) as usize].to_vec();
473 let v = IndexValue::I64(i64::from(next() % 7));
475 s.apply(key.as_bytes(), Some((g, v)), false);
476 }
477 }
478 check(&s, &format!("round {round}"));
479 }
480 for i in 0..30u32 {
481 s.apply(format!("r:{i}").as_bytes(), None, false);
482 }
483 check(&s, "full drain");
484 let end = s.stats();
485 assert_eq!((end.groups, end.rows), (0, 0));
486 }
487}