1pub const VALUE_DISTRIBUTION_BUCKETS: usize = 64;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct Histogram {
13 pub min: Vec<u8>,
15 pub max: Vec<u8>,
17 pub distinct: u64,
19 pub buckets: [u32; VALUE_DISTRIBUTION_BUCKETS],
21 pub partial: bool,
24}
25
26impl Default for Histogram {
27 fn default() -> Self {
28 Self {
29 min: Vec::new(),
30 max: Vec::new(),
31 distinct: 0,
32 buckets: [0; VALUE_DISTRIBUTION_BUCKETS],
33 partial: false,
34 }
35 }
36}
37
38impl Histogram {
39 pub fn add(&mut self, payload: &[u8], count: u64) {
41 if self.min.is_empty() || payload < self.min.as_slice() {
42 self.min = payload.to_vec();
43 }
44 if payload > self.max.as_slice() {
45 self.max = payload.to_vec();
46 }
47 self.distinct += 1;
48 let bucket = &mut self.buckets[fixed_bucket_index(payload)];
49 *bucket = bucket.saturating_add(u32::try_from(count).unwrap_or(u32::MAX));
50 }
51
52 #[must_use]
54 pub fn resident_bytes(&self) -> usize {
55 std::mem::size_of::<Self>() + self.min.capacity() + self.max.capacity()
56 }
57
58 #[must_use]
60 pub fn total(&self) -> u64 {
61 self.buckets.iter().map(|&b| u64::from(b)).sum()
62 }
63
64 #[must_use]
67 #[expect(
68 clippy::cast_possible_truncation,
69 clippy::cast_precision_loss,
70 clippy::cast_sign_loss,
71 reason = "selectivity estimate: a non-negative fraction of a u32 bucket count"
72 )]
73 pub fn estimate(&self, bound: &[u8], above: bool) -> u64 {
74 let total = self.total();
75 if total == 0 || self.max.is_empty() {
76 return 0;
77 }
78 let (min_v, max_v, bound_v) = (
79 encoded_to_u128(&self.min),
80 encoded_to_u128(&self.max),
81 encoded_to_u128(bound),
82 );
83 if above {
84 if bound_v <= min_v {
85 return total;
86 }
87 if bound_v > max_v {
88 return 0;
89 }
90 } else {
91 if bound_v > max_v {
92 return total;
93 }
94 if bound_v <= min_v {
95 return 0;
96 }
97 }
98 let width = self.min.len().max(self.max.len()).max(bound.len());
99 let bidx = fixed_bucket_index(bound);
100 let (lo, hi) = fixed_bucket_bounds(width, bidx);
101 let bucket_width = hi.saturating_sub(lo).max(1);
102 let within = if above {
103 hi.saturating_sub(bound_v)
104 } else {
105 bound_v.saturating_sub(lo)
106 };
107 let fraction = (within as f64 / bucket_width as f64).clamp(0.0, 1.0);
108 let partial = (f64::from(self.buckets[bidx]) * fraction).round() as u32;
109 let rest: u64 = if above {
110 self.buckets[bidx + 1..].iter().map(|&b| u64::from(b)).sum()
111 } else {
112 self.buckets[..bidx].iter().map(|&b| u64::from(b)).sum()
113 };
114 rest.saturating_add(u64::from(partial)).min(total)
115 }
116}
117
118fn encoded_to_u128(bytes: &[u8]) -> u128 {
119 let mut buf = [0u8; 16];
120 let take = bytes.len().min(16);
121 buf[16 - take..].copy_from_slice(&bytes[bytes.len() - take..]);
122 u128::from_be_bytes(buf)
123}
124
125fn fixed_bucket_index(encoded: &[u8]) -> usize {
126 let bits = encoded.len().saturating_mul(8);
127 let shift = bits.saturating_sub(6);
128 ((encoded_to_u128(encoded) >> shift) & 0x3F) as usize
129}
130
131fn fixed_bucket_bounds(width: usize, idx: usize) -> (u128, u128) {
132 let bits = width.saturating_mul(8);
133 let shift = bits.saturating_sub(6);
134 let lo = (idx as u128) << shift;
135 let hi = if idx + 1 < VALUE_DISTRIBUTION_BUCKETS {
136 (idx as u128 + 1) << shift
137 } else if bits >= 128 {
138 u128::MAX
139 } else {
140 (1u128 << bits) - 1
141 };
142 (lo, hi)
143}