1use chronix_engine::index::{SegmentCatalogEntry, SeriesBloomFilter};
26
27#[derive(Debug, Clone, Default)]
29pub struct PruningStats {
30 pub segments_total: usize,
32 pub pruned_by_time: usize,
34 pub pruned_by_bloom: usize,
36 pub pruned_by_stats: usize,
38 pub segments_remaining: usize,
40}
41
42impl PruningStats {
43 #[must_use]
45 pub fn total_pruned(&self) -> usize {
46 self.pruned_by_time + self.pruned_by_bloom + self.pruned_by_stats
47 }
48}
49
50#[derive(Debug)]
52pub struct PruningResult {
53 pub segments: Vec<PrunedSegment>,
55 pub stats: PruningStats,
57}
58
59#[derive(Debug, Clone)]
65pub struct PrunedSegment {
66 pub entry: SegmentCatalogEntry,
68}
69
70#[must_use]
83pub fn prune_entries<'a, F>(
84 entries: Vec<&'a SegmentCatalogEntry>,
85 bloom_lookup: F,
86 series_key: Option<&chronix_core::SeriesKey>,
87 tag_filter_keys: &[&str],
88) -> PruningResult
89where
90 F: Fn(u64) -> Option<&'a SeriesBloomFilter>,
91{
92 let input_count = entries.len();
93 let mut stats = PruningStats::default();
94
95 let mut surviving: Vec<PrunedSegment> = Vec::with_capacity(input_count);
97
98 for entry in entries {
99 if let Some(series_key) = series_key
100 && let Some(bloom) = bloom_lookup(entry.segment_id.0)
101 && !bloom.may_contain(series_key)
102 {
103 stats.pruned_by_bloom += 1;
104 continue;
105 }
106
107 surviving.push(PrunedSegment {
108 entry: entry.clone(),
109 });
110 }
111
112 if !tag_filter_keys.is_empty() {
116 surviving.retain(|ps| {
117 for tag_key in tag_filter_keys {
118 if let Some(cs) = ps.entry.column_stats.iter().find(|c| c.name == *tag_key)
119 && cs.stats.distinct_count == 0
120 && cs.stats.value_count == 0
121 {
122 stats.pruned_by_stats += 1;
123 return false;
124 }
125 }
126 true
127 });
128 }
129
130 stats.segments_remaining = surviving.len();
131
132 PruningResult {
133 segments: surviving,
134 stats,
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use chronix_core::{SegmentFile, SegmentId, SegmentState, ShardId};
142
143 fn make_entry(id: u64, shard: i64, min_ts: i64, max_ts: i64) -> SegmentCatalogEntry {
144 SegmentCatalogEntry {
145 segment_id: SegmentId(id),
146 shard_id: ShardId(shard),
147 measurement: "cpu".to_string(),
148 file: SegmentFile::new(ShardId(shard), &format!("seg_{id}.csx")).unwrap(),
149 min_timestamp: min_ts,
150 max_timestamp: max_ts,
151 row_count: 1000,
152 series_count: 10,
153 byte_size: 4096,
154 row_group_count: 1,
155 column_count: 5,
156 column_stats: Vec::new(),
157 state: SegmentState::default(),
158 }
159 }
160
161 fn tag_stats(
162 name: &str,
163 distinct: u32,
164 values: u64,
165 ) -> chronix_engine::index::CatalogColumnStats {
166 chronix_engine::index::CatalogColumnStats {
167 name: name.to_string(),
168 data_type: 1, role: 1, decimal_scale: None,
171 stats: chronix_engine::segment::stats::ColumnStats {
172 min_value: 0,
173 max_value: 0,
174 min_value_u64: 0,
175 max_value_u64: 0,
176 null_count: 0,
177 value_count: values,
178 sum: 0.0,
179 sum_i128: 0,
180 distinct_count: distinct,
181 },
182 }
183 }
184
185 #[test]
186 fn nothing_is_pruned_without_a_series_key_or_a_tag_filter() {
187 let entries = [make_entry(1, 0, 100, 200), make_entry(2, 0, 300, 400)];
188 let refs: Vec<&SegmentCatalogEntry> = entries.iter().collect();
189 let result = prune_entries(refs, |_| None, None, &[]);
190 assert_eq!(result.segments.len(), 2);
191 assert_eq!(result.stats.total_pruned(), 0);
192 }
193
194 #[test]
200 fn a_segment_without_a_bloom_is_kept() {
201 use chronix_engine::index::SeriesBloomFilter;
202 let entries = [make_entry(1, 0, 100, 200), make_entry(2, 0, 300, 400)];
203 let refs: Vec<&SegmentCatalogEntry> = entries.iter().collect();
204
205 let key = chronix_core::SeriesKey::new(
206 "cpu",
207 [("host".to_string(), "a".to_string())]
208 .into_iter()
209 .collect::<std::collections::BTreeMap<_, _>>(),
210 )
211 .unwrap();
212
213 let mut bloom = SeriesBloomFilter::new(8, 0.01);
215 bloom.insert(
216 &chronix_core::SeriesKey::new(
217 "cpu",
218 [("host".to_string(), "z".to_string())]
219 .into_iter()
220 .collect::<std::collections::BTreeMap<_, _>>(),
221 )
222 .unwrap(),
223 );
224
225 let result = prune_entries(refs, |id| (id == 1).then_some(&bloom), Some(&key), &[]);
226 assert_eq!(result.stats.pruned_by_bloom, 1);
227 assert_eq!(result.segments.len(), 1);
228 assert_eq!(result.segments[0].entry.segment_id, SegmentId(2));
229 }
230
231 #[test]
234 fn an_all_null_tag_column_is_pruned_by_its_statistics() {
235 let mut with_values = make_entry(1, 0, 100, 200);
236 with_values.column_stats = vec![tag_stats("host", 2, 10)];
237 let mut all_null = make_entry(2, 0, 300, 400);
238 all_null.column_stats = vec![tag_stats("host", 0, 0)];
239
240 let entries = [with_values, all_null];
241 let refs: Vec<&SegmentCatalogEntry> = entries.iter().collect();
242 let result = prune_entries(refs, |_| None, None, &["host"]);
243
244 assert_eq!(result.stats.pruned_by_stats, 1);
245 assert_eq!(result.segments.len(), 1);
246 assert_eq!(result.segments[0].entry.segment_id, SegmentId(1));
247 }
248
249 #[test]
251 fn a_segment_with_no_statistics_for_the_tag_is_kept() {
252 let entries = [make_entry(1, 0, 100, 200)];
253 let refs: Vec<&SegmentCatalogEntry> = entries.iter().collect();
254 let result = prune_entries(refs, |_| None, None, &["host"]);
255 assert_eq!(result.segments.len(), 1);
256 assert_eq!(result.stats.pruned_by_stats, 0);
257 }
258}