1use std::collections::{HashMap, HashSet};
18use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
19use std::sync::{Arc, Mutex};
20use std::time::Instant;
21
22use common_telemetry::debug;
23use smallvec::SmallVec;
24use snafu::ResultExt;
25use store_api::region_engine::PartitionRange;
26use store_api::storage::FileId;
27use tokio::sync::{mpsc, oneshot};
28use uuid::Uuid;
29
30use crate::error::{PruneFileSnafu, Result};
31use crate::metrics::PRUNER_ACTIVE_BUILDERS;
32use crate::read::range::{FileRangeBuilder, RowGroupIndex};
33use crate::read::scan_region::StreamContext;
34use crate::read::scan_util::{FileScanMetrics, PartitionMetrics, new_filter_metrics};
35use crate::sst::parquet::file_range::{FileRange, PreFilterMode};
36use crate::sst::parquet::reader::ReaderMetrics;
37
38const PREFETCH_COUNT: usize = 8;
40
41pub struct PartitionPruner {
43 pruner: Arc<Pruner>,
44 file_indices: Vec<usize>,
46 pre_filter_modes: Vec<PreFilterMode>,
48 current_position: AtomicUsize,
50}
51
52impl PartitionPruner {
53 pub fn new(pruner: Arc<Pruner>, partition_ranges: &[PartitionRange]) -> Self {
55 let num_files = pruner.inner.stream_ctx.input.num_files();
56 let mut file_indices = Vec::with_capacity(num_files);
57 let mut pre_filter_modes = vec![PreFilterMode::SkipFields; num_files];
58 let mut dedup_set = HashSet::with_capacity(pruner.inner.stream_ctx.input.num_files());
59
60 let num_memtables = pruner.inner.stream_ctx.input.num_memtables();
61 for part_range in partition_ranges {
62 let range_meta = &pruner.inner.stream_ctx.ranges[part_range.identifier];
63 let pre_filter_mode = pruner.inner.stream_ctx.range_pre_filter_mode(part_range);
64 for row_group_index in &range_meta.row_group_indices {
65 if pruner
66 .inner
67 .stream_ctx
68 .is_file_range_index(*row_group_index)
69 {
70 let file_index = row_group_index.index - num_memtables;
71 if dedup_set.contains(&file_index) {
72 continue;
73 } else {
74 file_indices.push(file_index);
75 pre_filter_modes[file_index] = pre_filter_mode;
76 dedup_set.insert(file_index);
77 }
78 }
79 }
80 }
81
82 Self {
83 pruner,
84 file_indices,
85 pre_filter_modes,
86 current_position: AtomicUsize::new(0),
87 }
88 }
89
90 pub(crate) fn excluding_files(mut self, excluded: &HashSet<usize>) -> Self {
92 self.file_indices.retain(|index| !excluded.contains(index));
93 self
94 }
95
96 pub async fn build_file_ranges(
101 &self,
102 index: RowGroupIndex,
103 partition_metrics: &PartitionMetrics,
104 reader_metrics: &mut ReaderMetrics,
105 ) -> Result<SmallVec<[FileRange; 2]>> {
106 let file_index = index.index - self.pruner.inner.stream_ctx.input.num_memtables();
107 let pre_filter_mode = self.pre_filter_mode(file_index);
108
109 let ranges = self
111 .pruner
112 .build_file_ranges(index, pre_filter_mode, partition_metrics, reader_metrics)
113 .await?;
114
115 if let Some(pos) = self.file_indices.iter().position(|&idx| idx == file_index) {
117 let prev_pos = self.current_position.fetch_max(pos, Ordering::Relaxed);
118 if pos > prev_pos || prev_pos == 0 {
119 self.prefetch_upcoming_files(pos, partition_metrics);
120 }
121 }
122
123 Ok(ranges)
124 }
125
126 pub fn try_skip_manifest_pruned_file_range(
136 &self,
137 index: RowGroupIndex,
138 part_metrics: &PartitionMetrics,
139 ) -> bool {
140 let Some(file_index) = self.file_index(index) else {
141 return false;
142 };
143 let mut reader_metrics = ReaderMetrics::default();
144 let pruned = self
145 .pruner
146 .inner
147 .try_mark_manifest_pruned(file_index, &mut reader_metrics);
148 if pruned {
149 self.pruner.skip_file_range(index, &mut reader_metrics);
150 part_metrics.merge_reader_metrics(&reader_metrics, None);
151 }
152 pruned
153 }
154
155 pub(crate) fn pre_filter_mode(&self, file_index: usize) -> PreFilterMode {
158 self.pre_filter_modes
159 .get(file_index)
160 .copied()
161 .unwrap_or(PreFilterMode::SkipFields)
162 }
163
164 fn prefetch_upcoming_files(&self, current_pos: usize, partition_metrics: &PartitionMetrics) {
166 let start = current_pos + 1;
167 let end = (start + PREFETCH_COUNT).min(self.file_indices.len());
168
169 for i in start..end {
170 let file_index = self.file_indices[i];
171 let pre_filter_mode = self.pre_filter_mode(file_index);
172 self.pruner.get_file_builder_background(
173 file_index,
174 pre_filter_mode,
175 Some(partition_metrics.clone()),
176 );
177 }
178 }
179
180 fn file_index(&self, index: RowGroupIndex) -> Option<usize> {
181 self.pruner
182 .inner
183 .stream_ctx
184 .is_file_range_index(index)
185 .then(|| index.index - self.pruner.inner.stream_ctx.input.num_memtables())
186 }
187}
188
189#[derive(Debug, Clone, Copy)]
191pub struct PrunerOptions {
192 pub retain_builders: bool,
194 pub enable_predicate_prefilter: bool,
202}
203
204impl Default for PrunerOptions {
205 fn default() -> Self {
206 Self {
207 retain_builders: false,
208 enable_predicate_prefilter: true,
209 }
210 }
211}
212
213pub struct Pruner {
215 worker_senders: Vec<mpsc::Sender<PruneRequest>>,
217 inner: Arc<PrunerInner>,
218}
219
220struct PrunerInner {
221 num_workers: usize,
223 file_entries: Vec<Mutex<FileBuilderEntry>>,
225 stream_ctx: Arc<StreamContext>,
227 manifest_pruned_files: Vec<AtomicBool>,
233 static_manifest_kept_files: Vec<AtomicBool>,
235 #[cfg(test)]
236 manifest_check_count: AtomicUsize,
237 retain_builders: bool,
239 enable_predicate_prefilter: bool,
241}
242
243impl Drop for PrunerInner {
244 fn drop(&mut self) {
245 let active_builders = self
246 .file_entries
247 .iter_mut()
248 .map(|entry| usize::from(entry.get_mut().is_ok_and(|entry| entry.builder.is_some())))
249 .sum::<usize>();
250 PRUNER_ACTIVE_BUILDERS.sub(active_builders as i64);
251 }
252}
253
254impl PrunerInner {
255 fn try_mark_manifest_pruned(
261 &self,
262 file_index: usize,
263 reader_metrics: &mut ReaderMetrics,
264 ) -> bool {
265 if self.manifest_pruned_files[file_index].load(Ordering::Relaxed) {
266 return true;
267 }
268 let file = &self.stream_ctx.input.files[file_index];
269 let has_dynamic_filters = self
271 .stream_ctx
272 .input
273 .predicate_for_file(file)
274 .is_some_and(|predicate| !predicate.dyn_filters().is_empty());
275 if !has_dynamic_filters
276 && self.static_manifest_kept_files[file_index].load(Ordering::Relaxed)
277 {
278 return false;
279 }
280 #[cfg(test)]
281 self.manifest_check_count.fetch_add(1, Ordering::Relaxed);
282 if !self.stream_ctx.input.can_manifest_prune_file(file) {
283 if !has_dynamic_filters {
284 self.static_manifest_kept_files[file_index].store(true, Ordering::Relaxed);
285 }
286 return false;
287 }
288 if self.manifest_pruned_files[file_index]
289 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
290 .is_ok()
291 {
292 reader_metrics.filter_metrics.files_time_range_pruned += 1;
293 }
294 true
295 }
296}
297
298struct FileBuilderEntry {
300 builder: Option<Arc<FileRangeBuilder>>,
306 remaining_ranges: usize,
310 waiters: Vec<oneshot::Sender<Result<Arc<FileRangeBuilder>>>>,
312}
313
314struct PruneRequest {
316 file_index: usize,
318 pre_filter_mode: PreFilterMode,
320 response_tx: Option<oneshot::Sender<Result<Arc<FileRangeBuilder>>>>,
322 partition_metrics: Option<PartitionMetrics>,
324}
325
326impl Pruner {
327 pub fn new(stream_ctx: Arc<StreamContext>, num_workers: usize) -> Self {
332 Self::new_with_options(stream_ctx, num_workers, PrunerOptions::default())
333 }
334
335 pub fn new_with_options(
337 stream_ctx: Arc<StreamContext>,
338 num_workers: usize,
339 options: PrunerOptions,
340 ) -> Self {
341 let PrunerOptions {
342 retain_builders,
343 enable_predicate_prefilter,
344 } = options;
345 let num_files = stream_ctx.input.num_files();
346 let file_entries: Vec<_> = (0..num_files)
347 .map(|_| {
348 Mutex::new(FileBuilderEntry {
349 builder: None,
350 remaining_ranges: 0,
351 waiters: Vec::new(),
352 })
353 })
354 .collect();
355 let manifest_pruned_files: Vec<AtomicBool> =
356 (0..num_files).map(|_| AtomicBool::new(false)).collect();
357 let mut worker_senders = Vec::with_capacity(num_workers);
359 let mut receivers = Vec::with_capacity(num_workers);
360 for _ in 0..num_workers {
361 let (tx, rx) = mpsc::channel::<PruneRequest>(64);
362 worker_senders.push(tx);
363 receivers.push(rx);
364 }
365
366 let inner = Arc::new(PrunerInner {
367 num_workers,
368 file_entries,
369 stream_ctx,
370 manifest_pruned_files,
371 static_manifest_kept_files: (0..num_files).map(|_| AtomicBool::new(false)).collect(),
372 #[cfg(test)]
373 manifest_check_count: AtomicUsize::new(0),
374 retain_builders,
375 enable_predicate_prefilter,
376 });
377
378 for (worker_id, rx) in receivers.into_iter().enumerate() {
380 let worker = Self::worker_loop(worker_id, rx, inner.clone());
381 if inner.stream_ctx.input.compaction {
382 common_runtime::spawn_compact(worker);
383 } else {
384 common_runtime::spawn_query(worker);
385 }
386 }
387
388 Self {
389 worker_senders,
390 inner,
391 }
392 }
393
394 pub fn add_partition_ranges(&self, partition_ranges: &[PartitionRange]) {
401 for pruned in &self.inner.manifest_pruned_files {
403 pruned.store(false, Ordering::Relaxed);
404 }
405
406 let num_memtables = self.inner.stream_ctx.input.num_memtables();
408 for part_range in partition_ranges {
409 let range_meta = &self.inner.stream_ctx.ranges[part_range.identifier];
410 for row_group_index in &range_meta.row_group_indices {
411 if self.inner.stream_ctx.is_file_range_index(*row_group_index) {
412 let file_index = row_group_index.index - num_memtables;
413 if file_index < self.inner.file_entries.len() {
414 let mut entry = self.inner.file_entries[file_index].lock().unwrap();
415 entry.remaining_ranges += 1;
416 }
417 }
418 }
419 }
420 }
421
422 pub async fn build_file_ranges(
428 &self,
429 index: RowGroupIndex,
430 pre_filter_mode: PreFilterMode,
431 partition_metrics: &PartitionMetrics,
432 reader_metrics: &mut ReaderMetrics,
433 ) -> Result<SmallVec<[FileRange; 2]>> {
434 let file_index = index.index - self.inner.stream_ctx.input.num_memtables();
435
436 let builder = self
438 .get_file_builder(
439 file_index,
440 pre_filter_mode,
441 partition_metrics,
442 reader_metrics,
443 )
444 .await?;
445
446 let mut ranges = SmallVec::new();
448 builder.build_ranges(index.row_group_index, &mut ranges);
449
450 self.decrement_and_maybe_clear(file_index, reader_metrics);
452
453 Ok(ranges)
454 }
455
456 pub fn skip_file_range(&self, index: RowGroupIndex, reader_metrics: &mut ReaderMetrics) {
462 if !self.inner.stream_ctx.is_file_range_index(index) {
463 return;
464 }
465 let file_index = index.index - self.inner.stream_ctx.input.num_memtables();
466 self.decrement_and_maybe_clear(file_index, reader_metrics);
467 }
468
469 async fn get_file_builder(
471 &self,
472 file_index: usize,
473 pre_filter_mode: PreFilterMode,
474 partition_metrics: &PartitionMetrics,
475 reader_metrics: &mut ReaderMetrics,
476 ) -> Result<Arc<FileRangeBuilder>> {
477 {
479 let entry = self.inner.file_entries[file_index].lock().unwrap();
480 if let Some(builder) = &entry.builder {
481 reader_metrics.filter_metrics.pruner_cache_hit += 1;
482 return Ok(builder.clone());
483 }
484 }
485
486 reader_metrics.filter_metrics.pruner_cache_miss += 1;
487 let prune_start = Instant::now();
488 let file = &self.inner.stream_ctx.input.files[file_index];
489 let file_id = file.file_id().file_id();
490 let worker_idx = self.get_worker_idx(file_id);
491
492 let (response_tx, response_rx) = oneshot::channel();
493 let request = PruneRequest {
494 file_index,
495 pre_filter_mode,
496 response_tx: Some(response_tx),
497 partition_metrics: Some(partition_metrics.clone()),
498 };
499
500 let result = if self.worker_senders[worker_idx].send(request).await.is_err() {
501 common_telemetry::warn!("Worker channel closed, falling back to direct pruning");
502 self.prune_file_directly(file_index, pre_filter_mode, reader_metrics)
504 .await
505 } else {
506 match response_rx.await {
508 Ok(result) => result,
509 Err(_) => {
510 common_telemetry::warn!(
511 "Response channel closed, falling back to direct pruning"
512 );
513 self.prune_file_directly(file_index, pre_filter_mode, reader_metrics)
515 .await
516 }
517 }
518 };
519 reader_metrics.filter_metrics.pruner_prune_cost += prune_start.elapsed();
520 result
521 }
522
523 pub fn get_file_builder_background(
525 &self,
526 file_index: usize,
527 pre_filter_mode: PreFilterMode,
528 partition_metrics: Option<PartitionMetrics>,
529 ) {
530 {
532 let entry = self.inner.file_entries[file_index].lock().unwrap();
533 if entry.builder.is_some() {
534 return;
535 }
536 }
537
538 let file = &self.inner.stream_ctx.input.files[file_index];
539 let file_id = file.file_id().file_id();
540 let worker_idx = self.get_worker_idx(file_id);
541
542 let request = PruneRequest {
543 file_index,
544 pre_filter_mode,
545 response_tx: None,
546 partition_metrics,
547 };
548
549 let _ = self.worker_senders[worker_idx].try_send(request);
551 }
552
553 pub fn predicate_prefilter_enabled(&self) -> bool {
556 self.inner.enable_predicate_prefilter
557 }
558
559 fn get_worker_idx(&self, file_id: FileId) -> usize {
560 let file_id_hash = Uuid::from(file_id).as_u128() as usize;
561 file_id_hash % self.inner.num_workers
562 }
563
564 async fn prune_file_directly(
567 &self,
568 file_index: usize,
569 pre_filter_mode: PreFilterMode,
570 reader_metrics: &mut ReaderMetrics,
571 ) -> Result<Arc<FileRangeBuilder>> {
572 if self
574 .inner
575 .try_mark_manifest_pruned(file_index, reader_metrics)
576 {
577 let arc_builder = Arc::new(FileRangeBuilder::default());
578 return Ok(arc_builder);
581 }
582
583 let file = &self.inner.stream_ctx.input.files[file_index];
584 let predicate = self.inner.stream_ctx.input.predicate_for_file(file);
585 let builder = self
586 .inner
587 .stream_ctx
588 .input
589 .prune_file_after_manifest_check(
590 file,
591 pre_filter_mode,
592 self.inner.enable_predicate_prefilter,
593 predicate,
594 reader_metrics,
595 )
596 .await?;
597
598 let arc_builder = Arc::new(builder);
599
600 {
603 let mut entry = self.inner.file_entries[file_index].lock().unwrap();
604 cache_builder_if_needed(&mut entry, &arc_builder, reader_metrics);
605 }
606
607 Ok(arc_builder)
608 }
609
610 fn decrement_and_maybe_clear(&self, file_index: usize, reader_metrics: &mut ReaderMetrics) {
612 let mut entry = self.inner.file_entries[file_index].lock().unwrap();
613 entry.remaining_ranges = entry.remaining_ranges.saturating_sub(1);
614
615 if !self.inner.retain_builders
616 && entry.remaining_ranges == 0
617 && let Some(builder) = entry.builder.take()
618 {
619 PRUNER_ACTIVE_BUILDERS.dec();
620 reader_metrics.metadata_mem_size -= builder.memory_size() as isize;
621 reader_metrics.num_range_builders -= 1;
622 }
623 }
624
625 async fn worker_loop(
627 worker_id: usize,
628 mut rx: mpsc::Receiver<PruneRequest>,
629 inner: Arc<PrunerInner>,
630 ) {
631 let mut worker_cache_hit = 0;
632 let mut worker_cache_miss = 0;
633 let mut pruned_files = Vec::new();
634
635 while let Some(request) = rx.recv().await {
636 let PruneRequest {
637 file_index,
638 pre_filter_mode,
639 response_tx,
640 partition_metrics,
641 } = request;
642
643 {
645 let entry = inner.file_entries[file_index].lock().unwrap();
646 if let Some(builder) = &entry.builder {
647 if let Some(response_tx) = response_tx {
649 let _ = response_tx.send(Ok(builder.clone()));
650 }
651 worker_cache_hit += 1;
652 continue;
653 }
654 }
655 worker_cache_miss += 1;
656
657 let file = &inner.stream_ctx.input.files[file_index];
658 pruned_files.push(file.file_id().file_id());
659 let explain_verbose = partition_metrics
660 .as_ref()
661 .map(|m| m.explain_verbose())
662 .unwrap_or(false);
663 let mut metrics = ReaderMetrics {
664 filter_metrics: new_filter_metrics(explain_verbose),
665 ..Default::default()
666 };
667
668 let result = if inner.try_mark_manifest_pruned(file_index, &mut metrics) {
670 Ok(FileRangeBuilder::default())
673 } else {
674 let predicate = inner.stream_ctx.input.predicate_for_file(file);
675 inner
676 .stream_ctx
677 .input
678 .prune_file_after_manifest_check(
679 file,
680 pre_filter_mode,
681 inner.enable_predicate_prefilter,
682 predicate,
683 &mut metrics,
684 )
685 .await
686 };
687
688 let mut entry = inner.file_entries[file_index].lock().unwrap();
690 match result {
691 Ok(builder) => {
692 let arc_builder = Arc::new(builder);
693 let is_background = response_tx.is_none();
694
695 let did_cache =
701 if inner.manifest_pruned_files[file_index].load(Ordering::Relaxed) {
702 false
703 } else {
704 cache_builder_if_needed(&mut entry, &arc_builder, &mut metrics)
705 };
706
707 for waiter in entry.waiters.drain(..) {
709 let _ = waiter.send(Ok(arc_builder.clone()));
710 }
711 if let Some(response_tx) = response_tx {
713 let _ = response_tx.send(Ok(arc_builder));
714 }
715
716 debug!(
717 "Pruner worker {} pruned file_index: {}, file: {:?}, metrics: {:?}",
718 worker_id,
719 file_index,
720 file.file_id(),
721 metrics
722 );
723
724 if (!is_background || did_cache)
729 && let Some(part_metrics) = &partition_metrics
730 {
731 let per_file_metrics = if part_metrics.explain_verbose() {
732 let file_id = file.file_id();
733 let mut map = HashMap::new();
734 map.insert(
735 file_id,
736 FileScanMetrics {
737 build_part_cost: metrics.build_cost,
738 ..Default::default()
739 },
740 );
741 Some(map)
742 } else {
743 None
744 };
745 part_metrics.merge_reader_metrics(&metrics, per_file_metrics.as_ref());
746 }
747 }
748 Err(e) => {
749 let arc_error = Arc::new(e);
750 for waiter in entry.waiters.drain(..) {
751 let _ = waiter.send(Err(arc_error.clone()).context(PruneFileSnafu));
752 }
753 if let Some(response_tx) = response_tx {
754 let _ = response_tx.send(Err(arc_error).context(PruneFileSnafu));
755 }
756 }
757 }
758 }
759
760 common_telemetry::debug!(
761 "Pruner worker {} finished, cache_hit: {}, cache_miss: {}, files: {:?}",
762 worker_id,
763 worker_cache_hit,
764 worker_cache_miss,
765 pruned_files,
766 );
767 }
768}
769
770#[cfg(test)]
771impl Pruner {
772 pub(crate) fn test_remaining_ranges(&self, file_index: usize) -> usize {
774 self.inner.file_entries[file_index]
775 .lock()
776 .unwrap()
777 .remaining_ranges
778 }
779
780 fn test_has_builder(&self, file_index: usize) -> bool {
782 self.inner.file_entries[file_index]
783 .lock()
784 .unwrap()
785 .builder
786 .is_some()
787 }
788
789 fn test_is_manifest_pruned(&self, file_index: usize) -> bool {
791 self.inner.manifest_pruned_files[file_index].load(Ordering::Relaxed)
792 }
793
794 #[allow(dead_code)]
796 fn test_clear_builder(&self, file_index: usize) {
797 let mut entry = self.inner.file_entries[file_index].lock().unwrap();
798 if entry.builder.take().is_some() {
799 PRUNER_ACTIVE_BUILDERS.dec();
800 }
801 }
802}
803
804fn should_cache_builder(entry: &FileBuilderEntry) -> bool {
806 entry.builder.is_none() && entry.remaining_ranges > 0
807}
808
809fn cache_builder_if_needed(
812 entry: &mut FileBuilderEntry,
813 builder: &Arc<FileRangeBuilder>,
814 reader_metrics: &mut ReaderMetrics,
815) -> bool {
816 if should_cache_builder(entry) {
817 reader_metrics.metadata_mem_size += builder.memory_size() as isize;
818 reader_metrics.num_range_builders += 1;
819 entry.builder = Some(builder.clone());
820 PRUNER_ACTIVE_BUILDERS.inc();
821 true
822 } else {
823 false
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use common_time::Timestamp;
830 use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
831 use datafusion_common::ScalarValue;
832 use datafusion_expr::{Expr, col, lit};
833 use store_api::region_engine::PartitionRange;
834 use store_api::storage::{FileId, RegionId};
835
836 use super::*;
837 use crate::read::flat_projection::FlatProjectionMapper;
838 use crate::read::range::{RangeMeta, RowGroupIndex};
839 use crate::read::scan_region::{PredicateGroup, ScanInput};
840 use crate::read::scan_util::PartitionMetrics;
841 use crate::sst::file::{FileHandle, FileMeta};
842 use crate::sst::parquet::reader::ReaderMetrics;
843 use crate::test_util::memtable_util::metadata_with_primary_key;
844 use crate::test_util::new_noop_file_purger;
845 use crate::test_util::scheduler_util::SchedulerEnv;
846
847 async fn make_test_pruner(num_files: usize) -> (SchedulerEnv, Arc<Pruner>) {
848 make_test_pruner_with_retained_builders(num_files, false).await
849 }
850
851 async fn make_test_pruner_with_retained_builders(
852 num_files: usize,
853 retain_builders: bool,
854 ) -> (SchedulerEnv, Arc<Pruner>) {
855 let env = SchedulerEnv::new().await;
856 let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
857 let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
858
859 let files: Vec<FileHandle> = (0..num_files)
860 .map(|_| {
861 let meta = FileMeta {
862 region_id: RegionId::new(123, 456),
863 file_id: FileId::random(),
864 time_range: (
865 Timestamp::new_millisecond(0),
866 Timestamp::new_millisecond(1000),
867 ),
868 num_row_groups: 1,
869 num_rows: 1024,
870 level: 0,
871 ..Default::default()
872 };
873 FileHandle::new(meta, new_noop_file_purger())
874 })
875 .collect();
876
877 let input = ScanInput::builder(env.access_layer.clone(), mapper)
878 .with_files(files)
879 .with_append_mode(true)
880 .build();
881 let query_start = input.query_start.unwrap_or_else(Instant::now);
882 let ranges = RangeMeta::unordered_scan_ranges(&input).await;
883 let stream_ctx = Arc::new(StreamContext::unordered_scan_ctx(
884 input,
885 ranges,
886 query_start,
887 ));
888 let pruner = Arc::new(Pruner::new_with_options(
889 stream_ctx,
890 1,
891 PrunerOptions {
892 retain_builders,
893 ..Default::default()
894 },
895 ));
896 (env, pruner)
897 }
898
899 fn file_partition_range(file_index: usize) -> PartitionRange {
905 PartitionRange {
906 start: Timestamp::new_millisecond(0),
907 end: Timestamp::new_millisecond(1001),
908 num_rows: 1024,
909 identifier: file_index,
910 }
911 }
912
913 async fn make_test_pruner_with_predicate(
914 num_files: usize,
915 row_groups_per_file: u64,
916 predicate_exprs: &[Expr],
917 ) -> (SchedulerEnv, Arc<Pruner>) {
918 let env = SchedulerEnv::new().await;
919 let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
920 let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
921 let predicate = PredicateGroup::new(&metadata, predicate_exprs).unwrap();
922
923 let files: Vec<FileHandle> = (0..num_files)
924 .map(|_| {
925 let meta = FileMeta {
926 region_id: RegionId::new(123, 456),
927 file_id: FileId::random(),
928 time_range: (
929 Timestamp::new_millisecond(0),
930 Timestamp::new_millisecond(1000),
931 ),
932 num_row_groups: row_groups_per_file,
933 num_rows: row_groups_per_file * 1024,
934 level: 0,
935 ..Default::default()
936 };
937 FileHandle::new(meta, new_noop_file_purger())
938 })
939 .collect();
940
941 let input = ScanInput::builder(env.access_layer.clone(), mapper)
942 .with_files(files)
943 .with_predicate(predicate)
944 .with_append_mode(true)
945 .build();
946 let query_start = input.query_start.unwrap_or_else(Instant::now);
947 let ranges = RangeMeta::unordered_scan_ranges(&input).await;
948 let stream_ctx = Arc::new(StreamContext::unordered_scan_ctx(
949 input,
950 ranges,
951 query_start,
952 ));
953 let pruner = Arc::new(Pruner::new(stream_ctx, 1));
954 (env, pruner)
955 }
956
957 fn make_partition_metrics() -> PartitionMetrics {
958 let metrics_set = ExecutionPlanMetricsSet::new();
959 PartitionMetrics::new(
960 RegionId::new(123, 456),
961 0,
962 "test",
963 Instant::now(),
964 false,
965 &metrics_set,
966 )
967 }
968
969 #[test]
970 fn should_cache_builder_when_ranges_remain() {
971 let entry = FileBuilderEntry {
972 builder: None,
973 remaining_ranges: 3,
974 waiters: Vec::new(),
975 };
976 assert!(should_cache_builder(&entry));
977 }
978
979 #[test]
980 fn should_not_cache_builder_when_no_ranges_remain() {
981 let entry = FileBuilderEntry {
982 builder: None,
983 remaining_ranges: 0,
984 waiters: Vec::new(),
985 };
986 assert!(!should_cache_builder(&entry));
987 }
988
989 #[test]
990 fn should_not_cache_builder_when_already_cached() {
991 let entry = FileBuilderEntry {
992 builder: Some(Arc::new(FileRangeBuilder::default())),
993 remaining_ranges: 1,
994 waiters: Vec::new(),
995 };
996 assert!(!should_cache_builder(&entry));
997 }
998
999 #[test]
1000 fn cache_builder_records_metrics() {
1001 let mut entry = FileBuilderEntry {
1002 builder: None,
1003 remaining_ranges: 1,
1004 waiters: Vec::new(),
1005 };
1006 let builder = Arc::new(FileRangeBuilder::default());
1007 let mut reader_metrics = ReaderMetrics::default();
1008
1009 assert!(cache_builder_if_needed(
1010 &mut entry,
1011 &builder,
1012 &mut reader_metrics,
1013 ));
1014 assert!(entry.builder.is_some());
1015 assert_eq!(
1016 reader_metrics.metadata_mem_size,
1017 builder.memory_size() as isize
1018 );
1019 assert_eq!(reader_metrics.num_range_builders, 1);
1020
1021 if entry.builder.take().is_some() {
1022 PRUNER_ACTIVE_BUILDERS.dec();
1023 }
1024 }
1025
1026 #[tokio::test]
1027 async fn skip_file_range_decrements_and_clears_builder() {
1028 let (_env, pruner) = make_test_pruner(1).await;
1029
1030 let ranges: Vec<PartitionRange> = (0..3).map(|_| file_partition_range(0)).collect();
1032 pruner.add_partition_ranges(&ranges);
1033 assert_eq!(pruner.test_remaining_ranges(0), 3);
1034
1035 {
1037 let mut entry = pruner.inner.file_entries[0].lock().unwrap();
1038 entry.builder = Some(Arc::new(FileRangeBuilder::default()));
1039 PRUNER_ACTIVE_BUILDERS.inc();
1040 }
1041 assert!(pruner.test_has_builder(0));
1042
1043 let mut reader_metrics = ReaderMetrics::default();
1045 for i in 0..3 {
1046 let index = RowGroupIndex {
1047 index: 0,
1048 row_group_index: i as i64,
1049 };
1050 pruner.skip_file_range(index, &mut reader_metrics);
1051 }
1052
1053 assert_eq!(pruner.test_remaining_ranges(0), 0);
1054 assert!(!pruner.test_has_builder(0));
1055 }
1056
1057 #[tokio::test]
1058 async fn retained_builder_survives_after_last_range() {
1059 let (_env, pruner) = make_test_pruner_with_retained_builders(1, true).await;
1060 pruner.add_partition_ranges(&[file_partition_range(0)]);
1061 {
1062 let mut entry = pruner.inner.file_entries[0].lock().unwrap();
1063 entry.builder = Some(Arc::new(FileRangeBuilder::default()));
1064 PRUNER_ACTIVE_BUILDERS.inc();
1065 }
1066
1067 let mut reader_metrics = ReaderMetrics::default();
1068 pruner.skip_file_range(
1069 RowGroupIndex {
1070 index: 0,
1071 row_group_index: 0,
1072 },
1073 &mut reader_metrics,
1074 );
1075
1076 assert_eq!(pruner.test_remaining_ranges(0), 0);
1077 assert!(pruner.test_has_builder(0));
1078
1079 let partition_metrics = make_partition_metrics();
1080 let mut reader_metrics = ReaderMetrics::default();
1081 let _builder = pruner
1082 .get_file_builder(
1083 0,
1084 PreFilterMode::SkipFields,
1085 &partition_metrics,
1086 &mut reader_metrics,
1087 )
1088 .await
1089 .unwrap();
1090 assert_eq!(reader_metrics.filter_metrics.pruner_cache_hit, 1);
1091 }
1092
1093 #[tokio::test]
1094 async fn add_partition_ranges_keeps_retained_builder() {
1095 let (_env, pruner) = make_test_pruner_with_retained_builders(1, true).await;
1096 pruner.add_partition_ranges(&[file_partition_range(0)]);
1097 {
1098 let mut entry = pruner.inner.file_entries[0].lock().unwrap();
1099 entry.builder = Some(Arc::new(FileRangeBuilder::default()));
1100 PRUNER_ACTIVE_BUILDERS.inc();
1101 }
1102
1103 let mut reader_metrics = ReaderMetrics::default();
1104 pruner.skip_file_range(
1105 RowGroupIndex {
1106 index: 0,
1107 row_group_index: 0,
1108 },
1109 &mut reader_metrics,
1110 );
1111 assert!(pruner.test_has_builder(0));
1112
1113 pruner.add_partition_ranges(&[file_partition_range(0)]);
1114 assert!(pruner.test_has_builder(0));
1115 assert_eq!(pruner.test_remaining_ranges(0), 1);
1116 }
1117
1118 #[tokio::test]
1119 async fn retaining_mode_does_not_cache_after_skip_file_range_consumed_all() {
1120 let (_env, pruner) = make_test_pruner_with_retained_builders(1, true).await;
1121
1122 let ranges = vec![file_partition_range(0)];
1124 pruner.add_partition_ranges(&ranges);
1125 assert_eq!(pruner.test_remaining_ranges(0), 1);
1126
1127 let mut reader_metrics = ReaderMetrics::default();
1131 let index = RowGroupIndex {
1132 index: 0,
1133 row_group_index: 0,
1134 };
1135 pruner.skip_file_range(index, &mut reader_metrics);
1136 assert_eq!(pruner.test_remaining_ranges(0), 0);
1137 assert!(!pruner.test_has_builder(0));
1138
1139 let entry = pruner.inner.file_entries[0].lock().unwrap();
1141 let should_cache = should_cache_builder(&entry);
1142 drop(entry);
1143
1144 assert!(!should_cache);
1145
1146 }
1150
1151 #[tokio::test]
1152 async fn worker_caches_when_ranges_remain() {
1153 let (_env, pruner) = make_test_pruner(1).await;
1154
1155 let ranges: Vec<PartitionRange> = (0..2).map(|_| file_partition_range(0)).collect();
1157 pruner.add_partition_ranges(&ranges);
1158 assert_eq!(pruner.test_remaining_ranges(0), 2);
1159
1160 let mut reader_metrics = ReaderMetrics::default();
1162 let index = RowGroupIndex {
1163 index: 0,
1164 row_group_index: 0,
1165 };
1166 pruner.skip_file_range(index, &mut reader_metrics);
1167 assert_eq!(pruner.test_remaining_ranges(0), 1);
1168
1169 let entry = pruner.inner.file_entries[0].lock().unwrap();
1171 assert!(should_cache_builder(&entry));
1172 }
1173
1174 #[tokio::test]
1180 async fn try_skip_manifest_pruned_file_range_multi_row_groups() {
1181 let predicate_exprs: Vec<Expr> =
1182 vec![col("ts").gt(lit(ScalarValue::TimestampMillisecond(Some(10_000), None)))];
1183 let (_env, pruner) = make_test_pruner_with_predicate(1, 3, &predicate_exprs).await;
1184
1185 let ranges = pruner.inner.stream_ctx.partition_ranges();
1186 assert_eq!(ranges.len(), 3);
1187 pruner.add_partition_ranges(&ranges);
1188 assert_eq!(pruner.test_remaining_ranges(0), 3);
1189
1190 let partition_pruner = Arc::new(PartitionPruner::new(pruner.clone(), &ranges));
1191 let partition_metrics = make_partition_metrics();
1192
1193 for rg in 0..3 {
1195 let index = RowGroupIndex {
1196 index: 0, row_group_index: rg,
1198 };
1199 let skipped =
1200 partition_pruner.try_skip_manifest_pruned_file_range(index, &partition_metrics);
1201 assert!(skipped, "row group {} should be skipped", rg);
1202 }
1203
1204 assert_eq!(pruner.test_remaining_ranges(0), 0);
1206 assert!(pruner.test_is_manifest_pruned(0));
1208 }
1209
1210 #[tokio::test]
1213 async fn try_skip_manifest_pruned_file_range_keeps_overlapping_file() {
1214 let predicate_exprs: Vec<Expr> =
1215 vec![col("ts").gt(lit(ScalarValue::TimestampMillisecond(Some(500), None)))];
1216 let (_env, pruner) = make_test_pruner_with_predicate(1, 2, &predicate_exprs).await;
1217
1218 let ranges = pruner.inner.stream_ctx.partition_ranges();
1219 assert_eq!(ranges.len(), 2);
1220 pruner.add_partition_ranges(&ranges);
1221 assert_eq!(pruner.test_remaining_ranges(0), 2);
1222
1223 let partition_pruner = Arc::new(PartitionPruner::new(pruner.clone(), &ranges));
1224 let partition_metrics = make_partition_metrics();
1225 let range_meta = &pruner.inner.stream_ctx.ranges[ranges[0].identifier];
1226 let index = range_meta.row_group_indices[0];
1227
1228 let skipped =
1229 partition_pruner.try_skip_manifest_pruned_file_range(index, &partition_metrics);
1230
1231 assert!(!skipped);
1232 assert_eq!(pruner.test_remaining_ranges(0), 2);
1233 assert!(!pruner.test_is_manifest_pruned(0));
1234 }
1235
1236 #[tokio::test]
1239 async fn add_partition_ranges_resets_manifest_pruned_flag() {
1240 let predicate_exprs: Vec<Expr> =
1241 vec![col("ts").gt(lit(ScalarValue::TimestampMillisecond(Some(10_000), None)))];
1242 let (_env, pruner) = make_test_pruner_with_predicate(1, 1, &predicate_exprs).await;
1243
1244 let mut reader_metrics = ReaderMetrics::default();
1246 let marked = pruner
1247 .inner
1248 .try_mark_manifest_pruned(0, &mut reader_metrics);
1249 assert!(marked);
1250 assert!(pruner.test_is_manifest_pruned(0));
1251 assert_eq!(reader_metrics.filter_metrics.files_time_range_pruned, 1);
1252
1253 let ranges = vec![file_partition_range(0)];
1255 pruner.add_partition_ranges(&ranges);
1256 assert!(!pruner.test_is_manifest_pruned(0));
1257 assert_eq!(pruner.test_remaining_ranges(0), 1);
1259 }
1260
1261 #[tokio::test]
1264 async fn prune_file_directly_manifest_pruned_returns_empty_builder() {
1265 let predicate_exprs: Vec<Expr> =
1266 vec![col("ts").gt(lit(ScalarValue::TimestampMillisecond(Some(10_000), None)))];
1267 let (_env, pruner) = make_test_pruner_with_predicate(1, 1, &predicate_exprs).await;
1268
1269 assert!(!pruner.test_has_builder(0));
1271
1272 let mut reader_metrics = ReaderMetrics::default();
1273 let builder = pruner
1274 .prune_file_directly(0, PreFilterMode::SkipFields, &mut reader_metrics)
1275 .await
1276 .unwrap();
1277
1278 assert_eq!(
1280 builder.memory_size(),
1281 FileRangeBuilder::default().memory_size()
1282 );
1283 assert!(!pruner.test_has_builder(0));
1285 assert_eq!(reader_metrics.filter_metrics.files_time_range_pruned, 1);
1287 }
1288
1289 #[tokio::test]
1292 async fn try_mark_manifest_pruned_only_counts_first_cas() {
1293 let predicate_exprs: Vec<Expr> =
1294 vec![col("ts").gt(lit(ScalarValue::TimestampMillisecond(Some(10_000), None)))];
1295 let (_env, pruner) = make_test_pruner_with_predicate(1, 1, &predicate_exprs).await;
1296
1297 let mut reader_metrics = ReaderMetrics::default();
1299 let marked = pruner
1300 .inner
1301 .try_mark_manifest_pruned(0, &mut reader_metrics);
1302 assert!(marked);
1303 assert_eq!(reader_metrics.filter_metrics.files_time_range_pruned, 1);
1304 assert!(pruner.test_is_manifest_pruned(0));
1305
1306 let mut reader_metrics2 = ReaderMetrics::default();
1308 let marked2 = pruner
1309 .inner
1310 .try_mark_manifest_pruned(0, &mut reader_metrics2);
1311 assert!(marked2);
1312 assert_eq!(reader_metrics2.filter_metrics.files_time_range_pruned, 0);
1313 }
1314
1315 #[tokio::test]
1316 async fn cached_builder_checks_late_and_tightened_dynamic_filters() {
1317 use datafusion::physical_plan::expressions::{
1318 DynamicFilterPhysicalExpr, lit as physical_lit,
1319 };
1320
1321 let expressions = [col("ts").gt(lit(ScalarValue::TimestampMillisecond(Some(500), None)))];
1322 let (_env, pruner) = make_test_pruner_with_predicate(1, 3, &expressions).await;
1323 let ranges = pruner.inner.stream_ctx.partition_ranges();
1324 pruner.add_partition_ranges(&ranges);
1325 let mut metrics = ReaderMetrics::default();
1326 assert!(!pruner.inner.try_mark_manifest_pruned(0, &mut metrics));
1327 cache_builder_if_needed(
1328 &mut pruner.inner.file_entries[0].lock().unwrap(),
1329 &Arc::new(FileRangeBuilder::default()),
1330 &mut metrics,
1331 );
1332 assert!(!pruner.inner.try_mark_manifest_pruned(0, &mut metrics));
1333 let dynamic = Arc::new(DynamicFilterPhysicalExpr::new(vec![], physical_lit(true)));
1334 pruner
1335 .inner
1336 .stream_ctx
1337 .input
1338 .predicate
1339 .add_dyn_filters(vec![dynamic.clone()]);
1340 assert!(!pruner.inner.try_mark_manifest_pruned(0, &mut metrics));
1341 assert_eq!(pruner.inner.manifest_check_count.load(Ordering::Relaxed), 2);
1342 dynamic.update(physical_lit(false)).unwrap();
1343 assert!(pruner.inner.try_mark_manifest_pruned(0, &mut metrics));
1344 assert_eq!(pruner.inner.manifest_check_count.load(Ordering::Relaxed), 3);
1345 assert_eq!(metrics.filter_metrics.files_time_range_pruned, 1);
1346 }
1347
1348 #[tokio::test]
1349 async fn static_manifest_keep_is_reused_without_builder() {
1350 let expressions = [col("ts").gt(lit(ScalarValue::TimestampMillisecond(Some(500), None)))];
1351 let (_env, pruner) = make_test_pruner_with_predicate(1, 3, &expressions).await;
1352 let ranges = pruner.inner.stream_ctx.partition_ranges();
1353 pruner.add_partition_ranges(&ranges);
1354 let mut metrics = ReaderMetrics::default();
1355 for _ in 0..3 {
1356 assert!(!pruner.inner.try_mark_manifest_pruned(0, &mut metrics));
1357 }
1358 assert_eq!(pruner.inner.manifest_check_count.load(Ordering::Relaxed), 1);
1359 pruner.add_partition_ranges(&ranges);
1360 assert!(!pruner.inner.try_mark_manifest_pruned(0, &mut metrics));
1361 assert_eq!(pruner.inner.manifest_check_count.load(Ordering::Relaxed), 1);
1362 }
1363}