1use arrow::{
21 array::{Array, AsArray},
22 compute::{
23 BatchCoalescer, FilterBuilder, interleave_record_batch, prep_null_mask_filter,
24 take_record_batch,
25 },
26 row::{OwnedRow, RowConverter, Rows, SortField},
27};
28use datafusion_expr::{ColumnarValue, Operator};
29use std::mem::size_of;
30use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
31use std::{cmp::Ordering, collections::BinaryHeap, sync::Arc};
32
33use super::metrics::{
34 BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, MetricCategory,
35 RecordOutput,
36};
37use crate::spill::get_record_batch_memory_size;
38use crate::{SendableRecordBatchStream, stream::RecordBatchStreamAdapter};
39
40use arrow::array::{ArrayRef, RecordBatch, UInt32Array};
41use arrow::datatypes::SchemaRef;
42use datafusion_common::{
43 HashMap, Result, ScalarValue, internal_datafusion_err, internal_err,
44};
45use datafusion_execution::{
46 memory_pool::{MemoryConsumer, MemoryReservation},
47 runtime_env::RuntimeEnv,
48};
49use datafusion_physical_expr::{
50 PhysicalExpr,
51 expressions::{BinaryExpr, DynamicFilterPhysicalExpr, is_not_null, is_null, lit},
52};
53use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
54use parking_lot::RwLock;
55
56pub struct TopK {
114 schema: SchemaRef,
116 metrics: TopKMetrics,
118 reservation: MemoryReservation,
120 batch_size: usize,
122 expr: LexOrdering,
124 row_converter: RowConverter,
126 scratch_rows: Rows,
128 heap: TopKHeap,
130 common_sort_prefix_converter: Option<RowConverter>,
132 common_sort_prefix: Arc<[PhysicalSortExpr]>,
134 filter: Arc<RwLock<TopKDynamicFilters>>,
136 pub(crate) finished: bool,
140}
141
142#[derive(Debug)]
146pub struct TopKDynamicFilters {
147 shared_threshold: Option<TopKThreshold>,
153 expr: Arc<DynamicFilterPhysicalExpr>,
156 remaining_topk_emitters: AtomicUsize,
166}
167
168#[derive(Debug, Clone)]
169struct TopKThreshold {
170 full_sort_key_row: Vec<u8>,
172 common_prefix_row: Option<Vec<u8>>,
178}
179
180impl TopKThreshold {
181 fn new(full_sort_key_row: Vec<u8>, common_prefix_row: Option<Vec<u8>>) -> Self {
182 Self {
183 full_sort_key_row,
184 common_prefix_row,
185 }
186 }
187
188 fn full_sort_key_row(&self) -> &[u8] {
189 self.full_sort_key_row.as_slice()
190 }
191
192 fn common_prefix_row(&self) -> Option<&[u8]> {
193 self.common_prefix_row.as_deref()
194 }
195
196 fn is_more_selective_than(&self, current: &Self) -> bool {
197 self.full_sort_key_row() < current.full_sort_key_row()
198 }
199}
200
201#[derive(Clone, Copy)]
202struct TopKHeapBoundaryRow<'a> {
203 row: &'a TopKRow,
204}
205
206impl<'a> TopKHeapBoundaryRow<'a> {
207 fn new(row: &'a TopKRow) -> Self {
208 Self { row }
209 }
210
211 fn full_sort_key_row(&self) -> &[u8] {
212 self.row.row()
213 }
214
215 fn is_more_selective_than(&self, current: Option<&TopKThreshold>) -> bool {
216 current
217 .map(|current| self.full_sort_key_row() < current.full_sort_key_row())
218 .unwrap_or(true)
219 }
220}
221
222#[derive(Clone, Copy)]
223struct TopKHeapBoundary<'a> {
224 row: &'a TopKRow,
225 batch: &'a RecordBatch,
226}
227
228impl<'a> TopKHeapBoundary<'a> {
229 fn new(row: &'a TopKRow, batch: &'a RecordBatch) -> Self {
230 Self { row, batch }
231 }
232
233 fn threshold_values(
234 &self,
235 sort_exprs: &[PhysicalSortExpr],
236 ) -> Result<Vec<ScalarValue>> {
237 let mut scalar_values = Vec::with_capacity(sort_exprs.len());
238 for sort_expr in sort_exprs {
239 let value = sort_expr
240 .expr
241 .evaluate(&self.batch.slice(self.row.index, 1))?;
242
243 let scalar = match value {
244 ColumnarValue::Scalar(scalar) => scalar,
245 ColumnarValue::Array(array) if array.len() == 1 => {
246 ScalarValue::try_from_array(&array, 0)?
247 }
248 array => {
249 return internal_err!("Expected a scalar value, got {:?}", array);
250 }
251 };
252 scalar_values.push(scalar);
253 }
254
255 Ok(scalar_values)
256 }
257
258 fn threshold(&self, common_prefix_row: Option<Vec<u8>>) -> TopKThreshold {
259 TopKThreshold::new(self.row.row().to_vec(), common_prefix_row)
260 }
261}
262
263impl TopKDynamicFilters {
264 pub fn new(expr: Arc<DynamicFilterPhysicalExpr>) -> Self {
266 Self::new_with_topk_emitter_count(expr, 1)
267 }
268
269 pub fn new_with_topk_emitter_count(
272 expr: Arc<DynamicFilterPhysicalExpr>,
273 topk_emitter_count: usize,
274 ) -> Self {
275 debug_assert!(topk_emitter_count > 0);
276 Self {
277 shared_threshold: None,
278 expr,
279 remaining_topk_emitters: AtomicUsize::new(topk_emitter_count),
280 }
281 }
282
283 pub fn expr(&self) -> Arc<DynamicFilterPhysicalExpr> {
284 Arc::clone(&self.expr)
285 }
286
287 fn mark_topk_emitted(&self) {
288 let previous = self
289 .remaining_topk_emitters
290 .fetch_update(
291 AtomicOrdering::AcqRel,
292 AtomicOrdering::Acquire,
293 |remaining| remaining.checked_sub(1),
294 )
295 .unwrap_or(0);
296 debug_assert!(
297 previous > 0,
298 "TopK dynamic filter emitter completed more times than expected"
299 );
300
301 if previous == 1 {
302 self.expr.mark_complete();
303 }
304 }
305}
306
307const ESTIMATED_BYTES_PER_ROW: usize = 20;
309
310#[derive(Debug, Clone)]
319pub(crate) struct EvictedRow {
320 pub batch: RecordBatch,
322 pub index: usize,
324 pub row_bytes: Vec<u8>,
326}
327
328pub(crate) fn build_sort_fields(
329 ordering: &[PhysicalSortExpr],
330 schema: &SchemaRef,
331) -> Result<Vec<SortField>> {
332 ordering
333 .iter()
334 .map(|e| {
335 Ok(SortField::new_with_options(
336 e.expr.data_type(schema)?,
337 e.options,
338 ))
339 })
340 .collect::<Result<_>>()
341}
342
343impl TopK {
344 #[expect(clippy::too_many_arguments)]
348 #[expect(clippy::needless_pass_by_value)]
349 pub fn try_new(
350 partition_id: usize,
351 schema: SchemaRef,
352 common_sort_prefix: Vec<PhysicalSortExpr>,
353 expr: LexOrdering,
354 k: usize,
355 batch_size: usize,
356 runtime: Arc<RuntimeEnv>,
357 metrics: &ExecutionPlanMetricsSet,
358 filter: Arc<RwLock<TopKDynamicFilters>>,
359 ) -> Result<Self> {
360 let reservation = MemoryConsumer::new(format!("TopK[{partition_id}]"))
361 .register(&runtime.memory_pool);
362
363 let sort_fields = build_sort_fields(&expr, &schema)?;
364
365 let row_converter = RowConverter::new(sort_fields)?;
368 let scratch_rows =
369 row_converter.empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size);
370
371 let common_prefix_row_converter = if common_sort_prefix.is_empty() {
372 None
373 } else {
374 let input_sort_fields = build_sort_fields(&common_sort_prefix, &schema)?;
375 Some(RowConverter::new(input_sort_fields)?)
376 };
377
378 Ok(Self {
379 schema: Arc::clone(&schema),
380 metrics: TopKMetrics::new(metrics, partition_id),
381 reservation,
382 batch_size,
383 expr,
384 row_converter,
385 scratch_rows,
386 heap: TopKHeap::new(k),
387 common_sort_prefix_converter: common_prefix_row_converter,
388 common_sort_prefix: Arc::from(common_sort_prefix),
389 finished: false,
390 filter,
391 })
392 }
393
394 #[expect(clippy::needless_pass_by_value)]
397 pub fn insert_batch(&mut self, batch: RecordBatch) -> Result<()> {
398 let baseline = self.metrics.baseline.clone();
400 let _timer = baseline.elapsed_compute().timer();
401
402 let mut sort_keys: Vec<ArrayRef> = self
403 .expr
404 .iter()
405 .map(|expr| {
406 let value = expr.expr.evaluate(&batch)?;
407 value.into_array(batch.num_rows())
408 })
409 .collect::<Result<Vec<_>>>()?;
410
411 let mut selected_rows = None;
412
413 let filter = self.filter.read().expr.current()?;
415 let filtered = filter.evaluate(&batch)?;
416 let num_rows = batch.num_rows();
417 let array = filtered.into_array(num_rows)?;
418 let mut filter = array.as_boolean().clone();
419 if !filter.has_true() {
420 self.attempt_early_completion(&batch)?;
423 return Ok(());
424 }
425 if filter.null_count() > 0 || filter.has_false() {
427 if filter.nulls().is_some() {
431 filter = prep_null_mask_filter(&filter);
432 }
433
434 let filter_predicate = FilterBuilder::new(&filter);
435 let filter_predicate = if sort_keys.len() > 1 {
436 filter_predicate.optimize().build()
438 } else {
439 filter_predicate.build()
440 };
441 selected_rows = Some(filter);
442 sort_keys = sort_keys
443 .iter()
444 .map(|key| filter_predicate.filter(key).map_err(|x| x.into()))
445 .collect::<Result<Vec<_>>>()?;
446 }
447 let rows = &mut self.scratch_rows;
449 rows.clear();
450 self.row_converter.append(rows, &sort_keys)?;
451
452 let mut batch_entry = self.heap.register_batch(batch.clone());
453
454 let replacements = match selected_rows {
455 Some(filter) => {
456 self.find_new_topk_items(filter.values().set_indices(), &mut batch_entry)
457 }
458 None => self.find_new_topk_items(0..sort_keys[0].len(), &mut batch_entry),
459 };
460
461 if replacements > 0 {
462 self.metrics.row_replacements.add(replacements);
463
464 self.heap.insert_batch_entry(batch_entry);
465
466 self.heap.maybe_compact()?;
468
469 self.reservation.try_resize(self.size())?;
471
472 self.attempt_early_completion(&batch)?;
476
477 self.update_filter()?;
479 } else {
480 self.attempt_early_completion(&batch)?;
483 }
484
485 Ok(())
486 }
487
488 fn find_new_topk_items(
489 &mut self,
490 items: impl Iterator<Item = usize>,
491 batch_entry: &mut RecordBatchEntry,
492 ) -> usize {
493 let mut replacements = 0;
494 let rows = &mut self.scratch_rows;
495 for (index, row) in items.zip(rows.iter()) {
496 match self.heap.max() {
497 Some(max_row) if row.as_ref() >= max_row.row() => {}
500 None | Some(_) => {
502 self.heap.add(batch_entry, row, index);
503 replacements += 1;
504 }
505 }
506 }
507 replacements
508 }
509
510 fn current_heap_boundary_row(&self) -> Option<TopKHeapBoundaryRow<'_>> {
511 self.heap.max().map(TopKHeapBoundaryRow::new)
512 }
513
514 fn current_heap_boundary(&self) -> Result<Option<TopKHeapBoundary<'_>>> {
515 let Some(row) = self.heap.max() else {
516 return Ok(None);
517 };
518
519 self.heap_boundary(row).map(Some)
520 }
521
522 fn heap_boundary<'a>(&'a self, row: &'a TopKRow) -> Result<TopKHeapBoundary<'a>> {
523 let batch_entry = self
524 .heap
525 .store
526 .get(row.batch_id)
527 .ok_or_else(|| internal_datafusion_err!("Invalid batch ID in TopKRow"))?;
528
529 Ok(TopKHeapBoundary::new(row, &batch_entry.batch))
530 }
531
532 fn update_filter(&mut self) -> Result<()> {
543 let Some(boundary_row) = self.current_heap_boundary_row() else {
545 return Ok(());
546 };
547
548 let needs_update = {
551 let filter = self.filter.read();
552 boundary_row.is_more_selective_than(filter.shared_threshold.as_ref())
553 };
554
555 if !needs_update {
557 return Ok(());
558 }
559
560 let boundary = self.heap_boundary(boundary_row.row)?;
561
562 let thresholds = boundary.threshold_values(&self.expr)?;
564
565 let predicate = Self::build_filter_expression(&self.expr, &thresholds)?;
567 let new_threshold =
568 boundary.threshold(self.encode_topk_common_prefix_row(boundary)?);
569
570 let mut filter = self.filter.write();
573 let still_needs_update = filter
574 .shared_threshold
575 .as_ref()
576 .map(|current| new_threshold.is_more_selective_than(current))
577 .unwrap_or(true);
578 if !still_needs_update {
579 return Ok(());
582 }
583 filter.shared_threshold = Some(new_threshold);
584
585 if let Some(pred) = predicate
587 && !pred.eq(&lit(true))
588 {
589 filter.expr.update(pred)?;
590 }
591
592 Ok(())
593 }
594
595 fn build_filter_expression(
598 sort_exprs: &[PhysicalSortExpr],
599 thresholds: &[ScalarValue],
600 ) -> Result<Option<Arc<dyn PhysicalExpr>>> {
601 let mut filters: Vec<Arc<dyn PhysicalExpr>> =
603 Vec::with_capacity(thresholds.len());
604
605 let mut prev_sort_expr: Option<Arc<dyn PhysicalExpr>> = None;
606 for (sort_expr, value) in sort_exprs.iter().zip(thresholds.iter()) {
607 let op = if sort_expr.options.descending {
609 Operator::Gt
611 } else {
612 Operator::Lt
614 };
615
616 let value_null = value.is_null();
617
618 let comparison = Arc::new(BinaryExpr::new(
619 Arc::clone(&sort_expr.expr),
620 op,
621 lit(value.clone()),
622 ));
623
624 let comparison_with_null = match (sort_expr.options.nulls_first, value_null) {
625 (true, true) => lit(false),
627 (true, false) => Arc::new(BinaryExpr::new(
628 is_null(Arc::clone(&sort_expr.expr))?,
629 Operator::Or,
630 comparison,
631 )),
632 (false, true) => is_not_null(Arc::clone(&sort_expr.expr))?,
635 (false, false) => comparison,
636 };
637
638 let mut eq_expr = Arc::new(BinaryExpr::new(
639 Arc::clone(&sort_expr.expr),
640 Operator::Eq,
641 lit(value.clone()),
642 ));
643
644 if value_null {
645 eq_expr = Arc::new(BinaryExpr::new(
646 is_null(Arc::clone(&sort_expr.expr))?,
647 Operator::Or,
648 eq_expr,
649 ));
650 }
651
652 match prev_sort_expr.take() {
657 None => {
658 prev_sort_expr = Some(eq_expr);
659 filters.push(comparison_with_null);
660 }
661 Some(p) => {
662 filters.push(Arc::new(BinaryExpr::new(
663 Arc::clone(&p),
664 Operator::And,
665 comparison_with_null,
666 )));
667
668 prev_sort_expr =
669 Some(Arc::new(BinaryExpr::new(p, Operator::And, eq_expr)));
670 }
671 }
672 }
673
674 let dynamic_predicate = filters
675 .into_iter()
676 .reduce(|a, b| Arc::new(BinaryExpr::new(a, Operator::Or, b)));
677
678 Ok(dynamic_predicate)
679 }
680
681 fn attempt_early_completion(&mut self, batch: &RecordBatch) -> Result<()> {
688 if batch.num_rows() == 0 {
690 return Ok(());
691 }
692
693 let Some(prefix_converter) = &self.common_sort_prefix_converter else {
696 return Ok(());
697 };
698
699 let last_row_idx = batch.num_rows() - 1;
701 let mut batch_prefix_scratch =
702 prefix_converter.empty_rows(1, ESTIMATED_BYTES_PER_ROW); self.append_common_prefix_row(
705 prefix_converter,
706 batch,
707 last_row_idx,
708 &mut batch_prefix_scratch,
709 )?;
710 let batch_common_prefix_row = batch_prefix_scratch.row(0);
711 let batch_common_prefix = batch_common_prefix_row.as_ref();
712
713 let finished_by_shared_threshold = self
714 .filter
715 .read()
716 .shared_threshold
717 .as_ref()
718 .and_then(TopKThreshold::common_prefix_row)
719 .map(|common_prefix_row| batch_common_prefix > common_prefix_row)
720 .unwrap_or(false);
721 if finished_by_shared_threshold {
722 self.finished = true;
723 return Ok(());
724 }
725
726 let Some(boundary) = self.current_heap_boundary()? else {
728 return Ok(());
729 };
730
731 if self.batch_prefix_exceeds_heap_boundary(batch_common_prefix, boundary)? {
732 self.finished = true;
733 }
734
735 Ok(())
736 }
737
738 fn batch_prefix_exceeds_heap_boundary(
739 &self,
740 batch_common_prefix: &[u8],
741 boundary: TopKHeapBoundary<'_>,
742 ) -> Result<bool> {
743 let Some(heap_common_prefix_row) =
744 self.encode_topk_common_prefix_row(boundary)?
745 else {
746 return Ok(false);
747 };
748
749 Ok(batch_common_prefix > heap_common_prefix_row.as_slice())
750 }
751
752 fn encode_topk_common_prefix_row(
753 &self,
754 boundary: TopKHeapBoundary<'_>,
755 ) -> Result<Option<Vec<u8>>> {
756 let Some(prefix_converter) = &self.common_sort_prefix_converter else {
757 return Ok(None);
758 };
759
760 let mut scratch = prefix_converter.empty_rows(1, ESTIMATED_BYTES_PER_ROW);
761 self.append_common_prefix_row(
762 prefix_converter,
763 boundary.batch,
764 boundary.row.index,
765 &mut scratch,
766 )?;
767 Ok(Some(scratch.row(0).as_ref().to_vec()))
768 }
769
770 fn append_common_prefix_row(
771 &self,
772 prefix_converter: &RowConverter,
773 batch: &RecordBatch,
774 row_idx: usize,
775 scratch: &mut Rows,
776 ) -> Result<()> {
777 let row = batch.slice(row_idx, 1);
778 let prefix_columns: Vec<ArrayRef> = self
779 .common_sort_prefix
780 .iter()
781 .map(|expr| expr.expr.evaluate(&row)?.into_array(1))
782 .collect::<Result<_>>()?;
783
784 prefix_converter.append(scratch, &prefix_columns)?;
785 Ok(())
786 }
787
788 pub fn emit(self) -> Result<SendableRecordBatchStream> {
790 let Self {
791 schema,
792 metrics,
793 reservation: _,
794 batch_size,
795 expr: _,
796 row_converter: _,
797 scratch_rows: _,
798 mut heap,
799 common_sort_prefix_converter: _,
800 common_sort_prefix: _,
801 finished: _,
802 filter,
803 } = self;
804 let _timer = metrics.baseline.elapsed_compute().timer(); filter.read().mark_topk_emitted();
809
810 let mut batches = vec![];
812 if let Some(mut batch) = heap.emit()? {
813 (&batch).record_output(&metrics.baseline);
814
815 loop {
816 if batch.num_rows() <= batch_size {
817 batches.push(Ok(batch));
818 break;
819 } else {
820 batches.push(Ok(batch.slice(0, batch_size)));
821 let remaining_length = batch.num_rows() - batch_size;
822 batch = batch.slice(batch_size, remaining_length);
823 }
824 }
825 };
826 Ok(Box::pin(RecordBatchStreamAdapter::new(
827 schema,
828 futures::stream::iter(batches),
829 )))
830 }
831
832 fn size(&self) -> usize {
834 size_of::<Self>()
835 + self.row_converter.size()
836 + self.scratch_rows.size()
837 + self.heap.size()
838 }
839}
840
841struct TopKMetrics {
842 pub baseline: BaselineMetrics,
844
845 pub row_replacements: Count,
847}
848
849impl TopKMetrics {
850 fn new(metrics: &ExecutionPlanMetricsSet, partition: usize) -> Self {
851 Self {
852 baseline: BaselineMetrics::new(metrics, partition),
853 row_replacements: MetricBuilder::new(metrics)
854 .with_category(MetricCategory::Rows)
855 .counter("row_replacements", partition),
856 }
857 }
858}
859
860struct TopKHeap {
868 k: usize,
870 inner: BinaryHeap<TopKRow>,
873 store: RecordBatchStore,
875 owned_bytes: usize,
877}
878
879impl TopKHeap {
880 fn new(k: usize) -> Self {
881 assert!(k > 0);
882 Self {
883 k,
884 inner: BinaryHeap::new(),
885 store: RecordBatchStore::new(),
886 owned_bytes: 0,
887 }
888 }
889
890 pub fn register_batch(&mut self, batch: RecordBatch) -> RecordBatchEntry {
893 self.store.register(batch)
894 }
895
896 pub fn insert_batch_entry(&mut self, entry: RecordBatchEntry) {
899 self.store.insert(entry)
900 }
901
902 fn max(&self) -> Option<&TopKRow> {
906 if self.inner.len() < self.k {
907 None
908 } else {
909 self.inner.peek()
910 }
911 }
912
913 fn add(
921 &mut self,
922 batch_entry: &mut RecordBatchEntry,
923 row: impl AsRef<[u8]>,
924 index: usize,
925 ) -> Option<EvictedRow> {
926 let batch_id = batch_entry.id;
927 batch_entry.uses += 1;
928
929 assert!(self.inner.len() <= self.k);
930 let row = row.as_ref();
931
932 if self.inner.len() == self.k {
934 let mut prev_min = self.inner.peek_mut().unwrap();
935
936 let evicted_batch = if prev_min.batch_id == batch_entry.id {
943 batch_entry.batch.clone()
944 } else {
945 self.store
946 .get(prev_min.batch_id)
947 .map(|entry| entry.batch.clone())
948 .expect("evicted row's batch must be present in the store")
949 };
950 let evicted = EvictedRow {
951 batch: evicted_batch,
952 index: prev_min.index,
953 row_bytes: prev_min.row.clone(),
954 };
955
956 if prev_min.batch_id == batch_entry.id {
958 batch_entry.uses -= 1;
959 } else {
960 self.store.unuse(prev_min.batch_id);
961 }
962
963 self.owned_bytes -= prev_min.owned_size();
965
966 prev_min.replace_with(row, batch_id, index);
967
968 self.owned_bytes += prev_min.owned_size();
969
970 Some(evicted)
971 } else {
972 let new_row = TopKRow::new(row, batch_id, index);
973 self.owned_bytes += new_row.owned_size();
974 self.inner.push(new_row);
976 None
977 }
978 }
979
980 pub fn emit(&mut self) -> Result<Option<RecordBatch>> {
983 Ok(self.emit_with_state()?.0)
984 }
985
986 fn emit_with_state(&mut self) -> Result<(Option<RecordBatch>, Vec<TopKRow>)> {
990 let topk_rows = std::mem::take(&mut self.inner).into_sorted_vec();
992
993 if self.store.is_empty() {
994 return Ok((None, topk_rows));
995 }
996
997 let mut record_batches = Vec::new();
1000 let mut batch_id_array_pos = HashMap::new();
1001 for (array_pos, (batch_id, batch)) in self.store.batches.iter().enumerate() {
1002 record_batches.push(&batch.batch);
1003 batch_id_array_pos.insert(*batch_id, array_pos);
1004 }
1005
1006 let indices: Vec<_> = topk_rows
1007 .iter()
1008 .map(|k| (batch_id_array_pos[&k.batch_id], k.index))
1009 .collect();
1010
1011 let new_batch = interleave_record_batch(&record_batches, &indices)?;
1016
1017 Ok((Some(new_batch), topk_rows))
1018 }
1019
1020 pub fn maybe_compact(&mut self) -> Result<()> {
1023 if self.store.len() <= 1 {
1025 return Ok(());
1026 }
1027
1028 let total_rows = self.store.total_rows;
1029 let num_rows = self.inner.len();
1030
1031 if total_rows <= num_rows * 2 {
1035 return Ok(());
1036 }
1037
1038 let (new_batch, mut topk_rows) = self.emit_with_state()?;
1044 let Some(new_batch) = new_batch else {
1045 return Ok(());
1046 };
1047
1048 self.store.clear();
1051
1052 let mut batch_entry = self.register_batch(new_batch);
1053 batch_entry.uses = num_rows;
1054
1055 for (i, topk_row) in topk_rows.iter_mut().enumerate() {
1059 topk_row.batch_id = batch_entry.id;
1060 topk_row.index = i;
1061 }
1062 self.insert_batch_entry(batch_entry);
1063 self.inner = BinaryHeap::from(topk_rows);
1065
1066 Ok(())
1067 }
1068
1069 fn size(&self) -> usize {
1071 size_of::<Self>()
1072 + (self.inner.capacity() * size_of::<TopKRow>())
1073 + self.store.size()
1074 + self.owned_bytes
1075 }
1076}
1077
1078#[derive(Debug, PartialEq)]
1084struct TopKRow {
1085 row: Vec<u8>,
1089 batch_id: u32,
1091 index: usize,
1093}
1094
1095impl TopKRow {
1096 fn new(row: impl AsRef<[u8]>, batch_id: u32, index: usize) -> Self {
1098 Self {
1099 row: row.as_ref().to_vec(),
1100 batch_id,
1101 index,
1102 }
1103 }
1104
1105 fn replace_with(&mut self, new_row: impl AsRef<[u8]>, batch_id: u32, index: usize) {
1107 self.row.clear();
1108 self.row.extend_from_slice(new_row.as_ref());
1109
1110 self.batch_id = batch_id;
1111 self.index = index;
1112 }
1113
1114 fn owned_size(&self) -> usize {
1117 self.row.capacity()
1118 }
1119
1120 fn row(&self) -> &[u8] {
1122 self.row.as_slice()
1123 }
1124}
1125
1126impl Eq for TopKRow {}
1127
1128impl PartialOrd for TopKRow {
1129 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1130 Some(self.cmp(other))
1132 }
1133}
1134
1135impl Ord for TopKRow {
1136 fn cmp(&self, other: &Self) -> Ordering {
1137 self.row.cmp(&other.row)
1138 }
1139}
1140
1141#[derive(Debug)]
1142struct RecordBatchEntry {
1143 id: u32,
1144 batch: RecordBatch,
1145 uses: usize,
1147}
1148
1149#[derive(Debug)]
1154struct RecordBatchStore {
1155 next_id: u32,
1157 batches: HashMap<u32, RecordBatchEntry>,
1159 batches_size: usize,
1161 total_rows: usize,
1163}
1164
1165impl RecordBatchStore {
1166 fn new() -> Self {
1167 Self {
1168 next_id: 0,
1169 batches: HashMap::new(),
1170 batches_size: 0,
1171 total_rows: 0,
1172 }
1173 }
1174
1175 pub fn register(&mut self, batch: RecordBatch) -> RecordBatchEntry {
1178 let id = self.next_id;
1179 self.next_id += 1;
1180 RecordBatchEntry { id, batch, uses: 0 }
1181 }
1182
1183 pub fn insert(&mut self, entry: RecordBatchEntry) {
1186 if entry.uses > 0 {
1188 self.batches_size += get_record_batch_memory_size(&entry.batch);
1189 self.total_rows += entry.batch.num_rows();
1190 self.batches.insert(entry.id, entry);
1191 }
1192 }
1193
1194 fn clear(&mut self) {
1196 self.batches.clear();
1197 self.batches_size = 0;
1198 self.total_rows = 0;
1199 }
1200
1201 fn get(&self, id: u32) -> Option<&RecordBatchEntry> {
1202 self.batches.get(&id)
1203 }
1204
1205 fn len(&self) -> usize {
1207 self.batches.len()
1208 }
1209
1210 fn is_empty(&self) -> bool {
1212 self.batches.is_empty()
1213 }
1214
1215 pub fn unuse(&mut self, id: u32) {
1220 let remove = if let Some(batch_entry) = self.batches.get_mut(&id) {
1221 batch_entry.uses = batch_entry.uses.checked_sub(1).expect("underflow");
1222 batch_entry.uses == 0
1223 } else {
1224 panic!("No entry for id {id}");
1225 };
1226
1227 if remove {
1228 let old_entry = self.batches.remove(&id).unwrap();
1229 self.batches_size = self
1230 .batches_size
1231 .checked_sub(get_record_batch_memory_size(&old_entry.batch))
1232 .unwrap();
1233
1234 self.total_rows = self
1235 .total_rows
1236 .checked_sub(old_entry.batch.num_rows())
1237 .unwrap();
1238 }
1239 }
1240
1241 pub fn size(&self) -> usize {
1244 size_of::<Self>()
1245 + self.batches.capacity() * (size_of::<u32>() + size_of::<RecordBatchEntry>())
1246 + self.batches_size
1247 }
1248}
1249
1250pub(crate) struct PartitionedTopK {
1262 schema: SchemaRef,
1263 metrics: TopKMetrics,
1264 reservation: MemoryReservation,
1265 expr: LexOrdering,
1267 row_converter: RowConverter,
1269 scratch_rows: Rows,
1271 partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
1273 partition_converter: RowConverter,
1275 heaps: HashMap<OwnedRow, TopKHeap>,
1277 k: usize,
1278 batch_size: usize,
1279}
1280
1281impl PartitionedTopK {
1282 #[expect(clippy::too_many_arguments)]
1283 pub(crate) fn try_new(
1284 partition_id: usize,
1285 schema: SchemaRef,
1286 partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
1287 partition_sort_fields: Vec<SortField>,
1288 order_expr: LexOrdering,
1289 k: usize,
1290 batch_size: usize,
1291 runtime: &Arc<RuntimeEnv>,
1292 metrics: &ExecutionPlanMetricsSet,
1293 ) -> Result<Self> {
1294 assert!(k > 0, "PartitionedTopK requires k > 0");
1295 let reservation = MemoryConsumer::new(format!("PartitionedTopK[{partition_id}]"))
1296 .register(&runtime.memory_pool);
1297
1298 let order_sort_fields = build_sort_fields(&order_expr, &schema)?;
1299 let row_converter = RowConverter::new(order_sort_fields)?;
1300 let scratch_rows =
1301 row_converter.empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size);
1302
1303 let partition_converter = RowConverter::new(partition_sort_fields)?;
1304
1305 Ok(Self {
1306 schema,
1307 metrics: TopKMetrics::new(metrics, partition_id),
1308 reservation,
1309 expr: order_expr,
1310 row_converter,
1311 scratch_rows,
1312 partition_exprs,
1313 partition_converter,
1314 heaps: HashMap::new(),
1315 k,
1316 batch_size,
1317 })
1318 }
1319
1320 pub(crate) fn insert_batch(&mut self, batch: &RecordBatch) -> Result<()> {
1324 let baseline = self.metrics.baseline.clone();
1325 let _timer = baseline.elapsed_compute().timer();
1326
1327 let num_rows = batch.num_rows();
1328 if num_rows == 0 {
1329 return Ok(());
1330 }
1331
1332 let pk_arrays: Vec<ArrayRef> = self
1334 .partition_exprs
1335 .iter()
1336 .map(|e| e.evaluate(batch).and_then(|v| v.into_array(num_rows)))
1337 .collect::<Result<_>>()?;
1338 let pk_rows = self.partition_converter.convert_columns(&pk_arrays)?;
1339
1340 let mut groups: HashMap<OwnedRow, Vec<u32>> = HashMap::new();
1342 for i in 0..num_rows {
1343 groups
1344 .entry(pk_rows.row(i).owned())
1345 .or_default()
1346 .push(i as u32);
1347 }
1348
1349 let ob_arrays: Vec<ArrayRef> = self
1351 .expr
1352 .iter()
1353 .map(|e| e.expr.evaluate(batch).and_then(|v| v.into_array(num_rows)))
1354 .collect::<Result<_>>()?;
1355 self.scratch_rows.clear();
1356 self.row_converter
1357 .append(&mut self.scratch_rows, &ob_arrays)?;
1358
1359 let k = self.k;
1362 let mut replacements: usize = 0;
1363 for (pk, indices) in groups {
1364 let heap = self.heaps.entry(pk).or_insert_with(|| TopKHeap::new(k));
1365
1366 let any_qualify = indices.iter().any(|&orig_idx| {
1370 let bytes = self.scratch_rows.row(orig_idx as usize);
1371 match heap.max() {
1372 Some(max_row) => bytes.as_ref() < max_row.row(),
1373 None => true,
1374 }
1375 });
1376 if !any_qualify {
1377 continue;
1378 }
1379
1380 let indices_arr = UInt32Array::from(indices);
1381 let sub_batch = take_record_batch(batch, &indices_arr)?;
1382 let mut entry = heap.register_batch(sub_batch);
1383
1384 for (sub_idx, &orig_idx) in indices_arr.values().iter().enumerate() {
1385 let row = self.scratch_rows.row(orig_idx as usize);
1386 match heap.max() {
1387 Some(max_row) if row.as_ref() >= max_row.row() => {}
1388 None | Some(_) => {
1389 heap.add(&mut entry, row, sub_idx);
1390 replacements += 1;
1391 }
1392 }
1393 }
1394
1395 heap.insert_batch_entry(entry);
1396 heap.maybe_compact()?;
1397 }
1398
1399 if replacements > 0 {
1400 self.metrics.row_replacements.add(replacements);
1401 }
1402 self.reservation.try_resize(self.size())?;
1403 Ok(())
1404 }
1405
1406 pub(crate) fn emit(self) -> Result<SendableRecordBatchStream> {
1410 let Self {
1411 schema,
1412 metrics,
1413 reservation: _,
1414 expr: _,
1415 row_converter: _,
1416 scratch_rows: _,
1417 partition_exprs: _,
1418 partition_converter: _,
1419 mut heaps,
1420 k: _,
1421 batch_size,
1422 } = self;
1423 let _timer = metrics.baseline.elapsed_compute().timer();
1424
1425 let mut sorted_pks: Vec<OwnedRow> = heaps.keys().cloned().collect();
1426 sorted_pks.sort();
1427
1428 let mut coalescer = BatchCoalescer::new(Arc::clone(&schema), batch_size);
1429
1430 for pk in sorted_pks {
1431 let mut heap = heaps.remove(&pk).expect("key from heaps.keys()");
1432 if let Some(batch) = heap.emit()? {
1433 (&batch).record_output(&metrics.baseline);
1434 coalescer.push_batch(batch)?;
1435 }
1436 }
1437 coalescer.finish_buffered_batch()?;
1438
1439 let mut out: Vec<Result<RecordBatch>> = Vec::new();
1440 while let Some(b) = coalescer.next_completed_batch() {
1441 out.push(Ok(b));
1442 }
1443
1444 Ok(Box::pin(RecordBatchStreamAdapter::new(
1445 schema,
1446 futures::stream::iter(out),
1447 )))
1448 }
1449
1450 fn size(&self) -> usize {
1453 size_of::<Self>()
1454 + self.row_converter.size()
1455 + self.partition_converter.size()
1456 + self.scratch_rows.size()
1457 + self.heaps.values().map(|h| h.size()).sum::<usize>()
1458 + self.heaps.capacity() * (size_of::<OwnedRow>() + size_of::<TopKHeap>())
1459 }
1460}
1461
1462#[derive(Debug)]
1466struct TieEntry {
1467 batch: RecordBatch,
1468 row_indices: Vec<u32>,
1471 batch_bytes: usize,
1475}
1476
1477struct RankPartitionState {
1485 heap: TopKHeap,
1486 ties: Vec<TieEntry>,
1487}
1488
1489impl RankPartitionState {
1490 fn size(&self) -> usize {
1491 let ties_buffer = self.ties.capacity() * size_of::<TieEntry>();
1492 let ties_contents: usize = self
1493 .ties
1494 .iter()
1495 .map(|t| t.row_indices.capacity() * size_of::<u32>() + t.batch_bytes)
1496 .sum();
1497 self.heap.size() + ties_buffer + ties_contents
1498 }
1499}
1500
1501pub(crate) struct PartitionedTopKRank {
1526 schema: SchemaRef,
1527 metrics: TopKMetrics,
1528 reservation: MemoryReservation,
1529 expr: LexOrdering,
1531 row_converter: RowConverter,
1533 scratch_rows: Rows,
1535 partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
1537 partition_converter: RowConverter,
1539 partition_scratch_rows: Rows,
1543 states: HashMap<OwnedRow, RankPartitionState>,
1545 k: usize,
1546 batch_size: usize,
1547}
1548
1549impl PartitionedTopKRank {
1550 #[expect(clippy::too_many_arguments)]
1551 pub(crate) fn try_new(
1552 partition_id: usize,
1553 schema: SchemaRef,
1554 partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
1555 partition_sort_fields: Vec<SortField>,
1556 order_expr: LexOrdering,
1557 k: usize,
1558 batch_size: usize,
1559 runtime: &Arc<RuntimeEnv>,
1560 metrics: &ExecutionPlanMetricsSet,
1561 ) -> Result<Self> {
1562 assert!(k > 0, "PartitionedTopKRank requires k > 0");
1563 let reservation =
1564 MemoryConsumer::new(format!("PartitionedTopKRank[{partition_id}]"))
1565 .register(&runtime.memory_pool);
1566
1567 let order_sort_fields = build_sort_fields(&order_expr, &schema)?;
1568 let row_converter = RowConverter::new(order_sort_fields)?;
1569 let scratch_rows =
1570 row_converter.empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size);
1571
1572 let partition_converter = RowConverter::new(partition_sort_fields)?;
1573 let partition_scratch_rows = partition_converter
1574 .empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size);
1575
1576 Ok(Self {
1577 schema,
1578 metrics: TopKMetrics::new(metrics, partition_id),
1579 reservation,
1580 expr: order_expr,
1581 row_converter,
1582 scratch_rows,
1583 partition_exprs,
1584 partition_converter,
1585 partition_scratch_rows,
1586 states: HashMap::new(),
1587 k,
1588 batch_size,
1589 })
1590 }
1591
1592 pub(crate) fn insert_batch(&mut self, batch: &RecordBatch) -> Result<()> {
1597 let baseline = self.metrics.baseline.clone();
1598 let _timer = baseline.elapsed_compute().timer();
1599
1600 let num_rows = batch.num_rows();
1601 if num_rows == 0 {
1602 return Ok(());
1603 }
1604
1605 let input_batch_bytes = get_record_batch_memory_size(batch);
1610
1611 let pk_arrays: Vec<ArrayRef> = self
1614 .partition_exprs
1615 .iter()
1616 .map(|e| e.evaluate(batch).and_then(|v| v.into_array(num_rows)))
1617 .collect::<Result<_>>()?;
1618 self.partition_scratch_rows.clear();
1619 self.partition_converter
1620 .append(&mut self.partition_scratch_rows, &pk_arrays)?;
1621 let pk_rows = &self.partition_scratch_rows;
1622
1623 let mut groups: HashMap<OwnedRow, Vec<u32>> = HashMap::new();
1625 for i in 0..num_rows {
1626 groups
1627 .entry(pk_rows.row(i).owned())
1628 .or_default()
1629 .push(i as u32);
1630 }
1631
1632 let ob_arrays: Vec<ArrayRef> = self
1634 .expr
1635 .iter()
1636 .map(|e| e.expr.evaluate(batch).and_then(|v| v.into_array(num_rows)))
1637 .collect::<Result<_>>()?;
1638 self.scratch_rows.clear();
1639 self.row_converter
1640 .append(&mut self.scratch_rows, &ob_arrays)?;
1641
1642 let k = self.k;
1644 let mut replacements: usize = 0;
1645
1646 for (pk, indices) in groups {
1647 let state = self.states.entry(pk).or_insert_with(|| RankPartitionState {
1648 heap: TopKHeap::new(k),
1649 ties: Vec::new(),
1650 });
1651
1652 let mut equal_indices: Vec<u32> = Vec::new();
1657 let mut entry: Option<RecordBatchEntry> = None;
1660
1661 for &orig_idx in &indices {
1662 let row = self.scratch_rows.row(orig_idx as usize);
1663
1664 let classification = state
1668 .heap
1669 .max()
1670 .map(|max_row| row.as_ref().cmp(max_row.row()));
1671
1672 match classification {
1673 Some(Ordering::Equal) => {
1674 equal_indices.push(orig_idx);
1675 continue;
1676 }
1677 Some(Ordering::Greater) => continue,
1678 Some(Ordering::Less) | None => {
1679 let entry_ref = entry.get_or_insert_with(|| {
1682 state.heap.register_batch(batch.clone())
1683 });
1684 if let Some(EvictedRow {
1685 batch: evicted_batch,
1686 index: evicted_index,
1687 row_bytes: evicted_bytes,
1688 }) = state.heap.add(entry_ref, row, orig_idx as usize)
1689 {
1690 let boundary_changed = state
1694 .heap
1695 .max()
1696 .expect("heap was full to evict; must still be full")
1697 .row()
1698 != evicted_bytes.as_slice();
1699 if boundary_changed {
1700 state.ties.clear();
1706 equal_indices.clear();
1707 } else {
1708 let batch_bytes =
1712 get_record_batch_memory_size(&evicted_batch);
1713 state.ties.push(TieEntry {
1714 batch: evicted_batch,
1715 row_indices: vec![evicted_index as u32],
1716 batch_bytes,
1717 });
1718 }
1719 }
1720 replacements += 1;
1721 }
1722 }
1723 }
1724
1725 if let Some(e) = entry {
1726 state.heap.insert_batch_entry(e);
1727 state.heap.maybe_compact()?;
1728 }
1729
1730 if !equal_indices.is_empty() {
1732 state.ties.push(TieEntry {
1733 batch: batch.clone(),
1734 row_indices: equal_indices,
1735 batch_bytes: input_batch_bytes,
1736 });
1737 }
1738 }
1739
1740 if replacements > 0 {
1741 self.metrics.row_replacements.add(replacements);
1742 }
1743 self.reservation.try_resize(self.size())?;
1744 Ok(())
1745 }
1746
1747 pub(crate) fn emit(self) -> Result<SendableRecordBatchStream> {
1753 let Self {
1754 schema,
1755 metrics,
1756 reservation: _,
1757 expr: _,
1758 row_converter: _,
1759 scratch_rows: _,
1760 partition_exprs: _,
1761 partition_converter: _,
1762 partition_scratch_rows: _,
1763 mut states,
1764 k: _,
1765 batch_size,
1766 } = self;
1767 let _timer = metrics.baseline.elapsed_compute().timer();
1768
1769 let mut sorted_pks: Vec<OwnedRow> = states.keys().cloned().collect();
1770 sorted_pks.sort();
1771
1772 let mut coalescer = BatchCoalescer::new(Arc::clone(&schema), batch_size);
1773
1774 for pk in sorted_pks {
1775 let RankPartitionState { mut heap, ties, .. } =
1776 states.remove(&pk).expect("key from states.keys()");
1777 if let Some(batch) = heap.emit()? {
1778 (&batch).record_output(&metrics.baseline);
1779 coalescer.push_batch(batch)?;
1780 }
1781 for tie in ties {
1782 let indices = UInt32Array::from(tie.row_indices);
1783 let tie_batch = take_record_batch(&tie.batch, &indices)?;
1784 (&tie_batch).record_output(&metrics.baseline);
1785 coalescer.push_batch(tie_batch)?;
1786 }
1787 }
1788 coalescer.finish_buffered_batch()?;
1789
1790 let mut out: Vec<Result<RecordBatch>> = Vec::new();
1791 while let Some(b) = coalescer.next_completed_batch() {
1792 out.push(Ok(b));
1793 }
1794
1795 Ok(Box::pin(RecordBatchStreamAdapter::new(
1796 schema,
1797 futures::stream::iter(out),
1798 )))
1799 }
1800
1801 fn size(&self) -> usize {
1803 size_of::<Self>()
1804 + self.row_converter.size()
1805 + self.partition_converter.size()
1806 + self.scratch_rows.size()
1807 + self.partition_scratch_rows.size()
1808 + self.states.values().map(|s| s.size()).sum::<usize>()
1809 + self.states.capacity()
1810 * (size_of::<OwnedRow>() + size_of::<RankPartitionState>())
1811 }
1812}
1813
1814#[cfg(test)]
1815mod tests {
1816 use super::*;
1817 use arrow::array::{BooleanArray, Float64Array, Int32Array};
1818 use arrow::datatypes::{DataType, Field, Schema};
1819 use arrow_schema::SortOptions;
1820 use datafusion_common::assert_batches_eq;
1821 use datafusion_physical_expr::{DynamicFilterTracking, expressions::col};
1822 use futures::TryStreamExt;
1823
1824 #[test]
1826 fn test_record_batch_store_size() {
1827 let schema = Arc::new(Schema::new(vec![
1829 Field::new("ints", DataType::Int32, true),
1830 Field::new("float64", DataType::Float64, false),
1831 ]));
1832 let mut record_batch_store = RecordBatchStore::new();
1833 let int_array =
1834 Int32Array::from(vec![Some(1), Some(2), Some(3), Some(4), Some(5)]); let float64_array = Float64Array::from(vec![1.0, 2.0, 3.0, 4.0, 5.0]); let record_batch_entry = RecordBatchEntry {
1838 id: 0,
1839 batch: RecordBatch::try_new(
1840 schema,
1841 vec![Arc::new(int_array), Arc::new(float64_array)],
1842 )
1843 .unwrap(),
1844 uses: 1,
1845 };
1846
1847 record_batch_store.insert(record_batch_entry);
1849 assert_eq!(record_batch_store.batches_size, 60);
1850
1851 record_batch_store.unuse(0);
1853 assert_eq!(record_batch_store.batches_size, 0);
1854 }
1855
1856 fn make_ab_schema() -> SchemaRef {
1857 make_ab_schema_with_nullable_a(false)
1858 }
1859
1860 fn make_ab_schema_with_nullable_a(a_nullable: bool) -> SchemaRef {
1861 Arc::new(Schema::new(vec![
1862 Field::new("a", DataType::Int32, a_nullable),
1863 Field::new("b", DataType::Float64, false),
1864 ]))
1865 }
1866
1867 fn make_topk_filter() -> Arc<RwLock<TopKDynamicFilters>> {
1869 make_shared_topk_filter(1)
1870 }
1871
1872 fn make_shared_topk_filter(
1873 topk_emitter_count: usize,
1874 ) -> Arc<RwLock<TopKDynamicFilters>> {
1875 Arc::new(RwLock::new(
1876 TopKDynamicFilters::new_with_topk_emitter_count(
1877 Arc::new(DynamicFilterPhysicalExpr::new(vec![], lit(true))),
1878 topk_emitter_count,
1879 ),
1880 ))
1881 }
1882
1883 fn make_ab_topk(
1886 schema: SchemaRef,
1887 filter: Arc<RwLock<TopKDynamicFilters>>,
1888 ) -> Result<TopK> {
1889 make_ab_topk_with_options(0, schema, filter, SortOptions::default())
1890 }
1891
1892 fn make_ab_topk_with_options(
1893 partition_id: usize,
1894 schema: SchemaRef,
1895 filter: Arc<RwLock<TopKDynamicFilters>>,
1896 a_options: SortOptions,
1897 ) -> Result<TopK> {
1898 let sort_expr_a = PhysicalSortExpr {
1899 expr: col("a", schema.as_ref())?,
1900 options: a_options,
1901 };
1902 let sort_expr_b = PhysicalSortExpr {
1903 expr: col("b", schema.as_ref())?,
1904 options: SortOptions::default(),
1905 };
1906
1907 TopK::try_new(
1908 partition_id,
1909 schema,
1910 vec![sort_expr_a.clone()],
1911 LexOrdering::from([sort_expr_a, sort_expr_b]),
1912 3,
1913 2,
1914 Arc::new(RuntimeEnv::default()),
1915 &ExecutionPlanMetricsSet::new(),
1916 filter,
1917 )
1918 }
1919
1920 fn make_ab_batch(
1921 schema: SchemaRef,
1922 a: &[Option<i32>],
1923 b: &[f64],
1924 ) -> Result<RecordBatch> {
1925 Ok(RecordBatch::try_new(
1926 schema,
1927 vec![
1928 Arc::new(Int32Array::from(a.to_vec())) as ArrayRef,
1929 Arc::new(Float64Array::from(b.to_vec())) as ArrayRef,
1930 ],
1931 )?)
1932 }
1933
1934 type AbRow = (Option<i32>, f64);
1935
1936 fn make_ab_rows_batch(schema: SchemaRef, rows: &[AbRow]) -> Result<RecordBatch> {
1937 let (a, b): (Vec<_>, Vec<_>) = rows.iter().copied().unzip();
1938 make_ab_batch(schema, &a, &b)
1939 }
1940
1941 #[tokio::test]
1942 async fn test_early_completion_marks_finished_with_prefix() -> Result<()> {
1943 let schema = make_ab_schema();
1944 let mut topk = make_ab_topk(Arc::clone(&schema), make_topk_filter())?;
1945
1946 topk.insert_batch(make_ab_batch(
1947 Arc::clone(&schema),
1948 &[Some(1), Some(1), Some(2)],
1949 &[20.0, 15.0, 30.0],
1950 )?)?;
1951 assert!(!topk.finished);
1952
1953 topk.insert_batch(make_ab_batch(
1954 Arc::clone(&schema),
1955 &[Some(2), Some(3)],
1956 &[10.0, 20.0],
1957 )?)?;
1958 assert!(topk.finished);
1959
1960 let results: Vec<_> = topk.emit()?.try_collect().await?;
1961 assert_batches_eq!(
1962 &[
1963 "+---+------+",
1964 "| a | b |",
1965 "+---+------+",
1966 "| 1 | 15.0 |",
1967 "| 1 | 20.0 |",
1968 "| 2 | 10.0 |",
1969 "+---+------+",
1970 ],
1971 &results
1972 );
1973
1974 Ok(())
1975 }
1976
1977 #[tokio::test]
1981 async fn test_early_completion_fires_when_filter_rejects_entire_batch() -> Result<()>
1982 {
1983 let schema = make_ab_schema();
1984 let mut topk = make_ab_topk(Arc::clone(&schema), make_topk_filter())?;
1985
1986 topk.insert_batch(make_ab_batch(
1987 Arc::clone(&schema),
1988 &[Some(1), Some(1), Some(2)],
1989 &[20.0, 15.0, 30.0],
1990 )?)?;
1991 assert!(!topk.finished);
1992
1993 topk.insert_batch(make_ab_batch(
1994 Arc::clone(&schema),
1995 &[Some(3), Some(3)],
1996 &[10.0, 20.0],
1997 )?)?;
1998 assert!(topk.finished);
1999
2000 let results: Vec<_> = topk.emit()?.try_collect().await?;
2001 assert_batches_eq!(
2002 &[
2003 "+---+------+",
2004 "| a | b |",
2005 "+---+------+",
2006 "| 1 | 15.0 |",
2007 "| 1 | 20.0 |",
2008 "| 2 | 30.0 |",
2009 "+---+------+",
2010 ],
2011 &results
2012 );
2013
2014 Ok(())
2015 }
2016
2017 #[tokio::test]
2018 async fn test_early_completion_fires_when_batch_makes_no_replacements() -> Result<()>
2019 {
2020 let schema = make_ab_schema();
2021 let filter = make_topk_filter();
2022 let mut topk = make_ab_topk(Arc::clone(&schema), Arc::clone(&filter))?;
2023
2024 topk.insert_batch(make_ab_batch(
2025 Arc::clone(&schema),
2026 &[Some(1), Some(1), Some(2)],
2027 &[20.0, 15.0, 30.0],
2028 )?)?;
2029 assert!(!topk.finished);
2030
2031 let replacements_before = topk.metrics.row_replacements.value();
2032
2033 filter.read().expr().update(lit(true))?;
2037 topk.insert_batch(make_ab_batch(
2038 Arc::clone(&schema),
2039 &[Some(3), Some(3)],
2040 &[10.0, 20.0],
2041 )?)?;
2042 assert_eq!(topk.metrics.row_replacements.value(), replacements_before);
2043 assert!(topk.finished);
2044
2045 let results: Vec<_> = topk.emit()?.try_collect().await?;
2046 assert_batches_eq!(
2047 &[
2048 "+---+------+",
2049 "| a | b |",
2050 "+---+------+",
2051 "| 1 | 15.0 |",
2052 "| 1 | 20.0 |",
2053 "| 2 | 30.0 |",
2054 "+---+------+",
2055 ],
2056 &results
2057 );
2058
2059 Ok(())
2060 }
2061
2062 struct SharedPrefixCase {
2063 name: &'static str,
2064 a_nullable: bool,
2065 a_options: SortOptions,
2066 threshold_source_rows: &'static [AbRow],
2067 lagging_partition_rows: &'static [AbRow],
2068 expected_finished: bool,
2069 }
2070
2071 fn assert_shared_prefix_case(case: SharedPrefixCase) -> Result<()> {
2072 let schema = make_ab_schema_with_nullable_a(case.a_nullable);
2073 let filter = make_shared_topk_filter(2);
2074
2075 let mut threshold_source = make_ab_topk_with_options(
2076 0,
2077 Arc::clone(&schema),
2078 Arc::clone(&filter),
2079 case.a_options,
2080 )?;
2081 threshold_source.insert_batch(make_ab_rows_batch(
2082 Arc::clone(&schema),
2083 case.threshold_source_rows,
2084 )?)?;
2085 assert!(
2086 filter
2087 .read()
2088 .shared_threshold
2089 .as_ref()
2090 .and_then(TopKThreshold::common_prefix_row)
2091 .is_some(),
2092 "{}: threshold-source partition should establish the shared prefix threshold",
2093 case.name
2094 );
2095
2096 let mut lagging_partition = make_ab_topk_with_options(
2097 1,
2098 Arc::clone(&schema),
2099 Arc::clone(&filter),
2100 case.a_options,
2101 )?;
2102 lagging_partition
2103 .insert_batch(make_ab_rows_batch(schema, case.lagging_partition_rows)?)?;
2104
2105 assert!(
2106 lagging_partition.heap.inner.is_empty(),
2107 "{}: lagging partition's local heap should remain empty",
2108 case.name
2109 );
2110 assert_eq!(
2111 lagging_partition.finished, case.expected_finished,
2112 "{}",
2113 case.name
2114 );
2115
2116 Ok(())
2117 }
2118
2119 #[test]
2120 fn test_shared_filter_can_finish_partition_before_local_heap_is_full() -> Result<()> {
2121 assert_shared_prefix_case(SharedPrefixCase {
2122 name: "shared threshold should finish lagging partition",
2123 a_nullable: false,
2124 a_options: SortOptions::default(),
2125 threshold_source_rows: &[(Some(1), 20.0), (Some(1), 15.0), (Some(2), 30.0)],
2126 lagging_partition_rows: &[(Some(3), 10.0), (Some(3), 20.0)],
2127 expected_finished: true,
2128 })
2129 }
2130
2131 #[test]
2132 fn test_shared_prefix_threshold_boundary_cases() -> Result<()> {
2133 for case in [
2134 SharedPrefixCase {
2135 name: "equal prefix cannot prove completion",
2136 a_nullable: false,
2137 a_options: SortOptions::default(),
2138 threshold_source_rows: &[
2139 (Some(1), 20.0),
2140 (Some(1), 15.0),
2141 (Some(2), 30.0),
2142 ],
2143 lagging_partition_rows: &[(Some(2), 40.0), (Some(2), 50.0)],
2144 expected_finished: false,
2145 },
2146 SharedPrefixCase {
2147 name: "descending prefix uses sort-order row encoding",
2148 a_nullable: false,
2149 a_options: SortOptions {
2150 descending: true,
2151 nulls_first: true,
2152 },
2153 threshold_source_rows: &[
2154 (Some(10), 1.0),
2155 (Some(10), 2.0),
2156 (Some(9), 3.0),
2157 ],
2158 lagging_partition_rows: &[(Some(8), 1.0), (Some(8), 2.0)],
2159 expected_finished: true,
2160 },
2161 SharedPrefixCase {
2162 name: "NULLS LAST prefix uses sort-order row encoding",
2163 a_nullable: true,
2164 a_options: SortOptions {
2165 descending: false,
2166 nulls_first: false,
2167 },
2168 threshold_source_rows: &[
2169 (Some(1), 20.0),
2170 (Some(1), 15.0),
2171 (Some(2), 30.0),
2172 ],
2173 lagging_partition_rows: &[(None, 10.0), (None, 20.0)],
2174 expected_finished: true,
2175 },
2176 ] {
2177 assert_shared_prefix_case(case)?;
2178 }
2179 Ok(())
2180 }
2181
2182 fn make_single_column_topk(
2183 dynamic_filter: Arc<DynamicFilterPhysicalExpr>,
2184 ) -> Result<(SchemaRef, TopK)> {
2185 make_single_column_topk_with_filter(
2186 0,
2187 Arc::new(RwLock::new(TopKDynamicFilters::new(dynamic_filter))),
2188 )
2189 }
2190
2191 fn make_single_column_topk_with_filter(
2192 partition_id: usize,
2193 filter: Arc<RwLock<TopKDynamicFilters>>,
2194 ) -> Result<(SchemaRef, TopK)> {
2195 let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
2196 let sort_expr = PhysicalSortExpr {
2197 expr: col("a", schema.as_ref())?,
2198 options: SortOptions::default(),
2199 };
2200
2201 let topk = TopK::try_new(
2202 partition_id,
2203 Arc::clone(&schema),
2204 vec![sort_expr.clone()],
2205 LexOrdering::from([sort_expr]),
2206 2,
2207 10,
2208 Arc::new(RuntimeEnv::default()),
2209 &ExecutionPlanMetricsSet::new(),
2210 filter,
2211 )?;
2212
2213 Ok((schema, topk))
2214 }
2215
2216 #[tokio::test]
2217 async fn test_topk_marks_filter_complete() -> Result<()> {
2218 let dynamic_filter = Arc::new(DynamicFilterPhysicalExpr::new(vec![], lit(true)));
2219 let dynamic_filter_clone = Arc::clone(&dynamic_filter);
2220 let (schema, mut topk) = make_single_column_topk(dynamic_filter)?;
2221
2222 let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(3), Some(1), Some(2)]));
2223 let batch = RecordBatch::try_new(Arc::clone(&schema), vec![array])?;
2224 topk.insert_batch(batch)?;
2225
2226 let _results: Vec<_> = topk.emit()?.try_collect().await?;
2227
2228 tokio::time::timeout(
2229 std::time::Duration::from_secs(1),
2230 dynamic_filter_clone.wait_complete(),
2231 )
2232 .await
2233 .expect("single-emitter TopK should mark the dynamic filter complete");
2234
2235 Ok(())
2236 }
2237
2238 #[tokio::test]
2239 async fn test_shared_topk_filter_completes_after_last_emitter() -> Result<()> {
2240 let dynamic_filter = Arc::new(DynamicFilterPhysicalExpr::new(vec![], lit(true)));
2241 let dynamic_filter_clone = Arc::clone(&dynamic_filter);
2242 let shared_filter = Arc::new(RwLock::new(
2243 TopKDynamicFilters::new_with_topk_emitter_count(dynamic_filter, 2),
2244 ));
2245
2246 let (schema, mut topk_0) =
2247 make_single_column_topk_with_filter(0, Arc::clone(&shared_filter))?;
2248 let (_, mut topk_1) =
2249 make_single_column_topk_with_filter(1, Arc::clone(&shared_filter))?;
2250
2251 let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(3), Some(1), Some(2)]));
2252 let batch = RecordBatch::try_new(Arc::clone(&schema), vec![array])?;
2253 topk_0.insert_batch(batch)?;
2254 let _results: Vec<_> = topk_0.emit()?.try_collect().await?;
2255
2256 let dynamic_filter_expr: Arc<dyn PhysicalExpr> =
2257 Arc::<DynamicFilterPhysicalExpr>::clone(&dynamic_filter_clone);
2258 assert!(
2259 matches!(
2260 DynamicFilterTracking::classify(&dynamic_filter_expr),
2261 DynamicFilterTracking::Watching(_)
2262 ),
2263 "the shared filter should remain watchable until every TopK emits"
2264 );
2265
2266 let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(6), Some(4), Some(5)]));
2267 let batch = RecordBatch::try_new(schema, vec![array])?;
2268 topk_1.insert_batch(batch)?;
2269 let _results: Vec<_> = topk_1.emit()?.try_collect().await?;
2270
2271 tokio::time::timeout(
2272 std::time::Duration::from_secs(1),
2273 dynamic_filter_clone.wait_complete(),
2274 )
2275 .await
2276 .expect("the final shared TopK emitter should mark the dynamic filter complete");
2277
2278 Ok(())
2279 }
2280
2281 #[tokio::test]
2284 async fn test_topk_memory_compaction() -> Result<()> {
2285 let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
2286
2287 let sort_expr = PhysicalSortExpr {
2288 expr: col("a", schema.as_ref())?,
2289 options: SortOptions::default(),
2290 };
2291
2292 let full_expr = LexOrdering::from([sort_expr.clone()]);
2293 let prefix = vec![sort_expr];
2294
2295 let runtime = Arc::new(RuntimeEnv::default());
2296 let metrics = ExecutionPlanMetricsSet::new();
2297
2298 let k = 5;
2299 let mut topk = TopK::try_new(
2300 0,
2301 Arc::clone(&schema),
2302 prefix,
2303 full_expr,
2304 k,
2305 8192,
2306 runtime,
2307 &metrics,
2308 Arc::new(RwLock::new(TopKDynamicFilters::new(Arc::new(
2309 DynamicFilterPhysicalExpr::new(vec![], lit(true)),
2310 )))),
2311 )?;
2312
2313 let large_values: Vec<i32> = (1..=100_000).collect();
2316 let array1: ArrayRef = Arc::new(Int32Array::from(large_values));
2317 let batch1 = RecordBatch::try_new(Arc::clone(&schema), vec![array1])?;
2318 topk.insert_batch(batch1)?;
2319
2320 assert_eq!(
2323 topk.heap.store.len(),
2324 1,
2325 "should have 1 batch before second insert"
2326 );
2327
2328 let array2: ArrayRef = Arc::new(Int32Array::from(vec![-1, 0]));
2334 let batch2 = RecordBatch::try_new(Arc::clone(&schema), vec![array2])?;
2335 let replacements_before = topk.metrics.row_replacements.value();
2336 topk.insert_batch(batch2)?;
2337
2338 assert!(
2342 topk.metrics.row_replacements.value() > replacements_before,
2343 "batch2 must produce replacements so compaction is exercised"
2344 );
2345
2346 assert_eq!(
2350 topk.heap.store.len(),
2351 1,
2352 "store should be compacted to 1 batch"
2353 );
2354
2355 let results: Vec<_> = topk.emit()?.try_collect().await?;
2357 assert_batches_eq!(
2358 &[
2359 "+----+", "| a |", "+----+", "| -1 |", "| 0 |", "| 1 |", "| 2 |",
2360 "| 3 |", "+----+",
2361 ],
2362 &results
2363 );
2364
2365 Ok(())
2366 }
2367
2368 #[tokio::test]
2378 async fn test_topk_memory_compaction_skipped_when_marginal() -> Result<()> {
2379 let schema =
2380 Arc::new(Schema::new(vec![Field::new("a", DataType::Boolean, false)]));
2381
2382 let sort_expr = PhysicalSortExpr {
2383 expr: col("a", schema.as_ref())?,
2384 options: SortOptions::default(),
2385 };
2386 let full_expr = LexOrdering::from([sort_expr.clone()]);
2387 let prefix = vec![sort_expr];
2388
2389 let runtime = Arc::new(RuntimeEnv::default());
2390 let metrics = ExecutionPlanMetricsSet::new();
2391
2392 let k = 10;
2393 let mut topk = TopK::try_new(
2394 0,
2395 Arc::clone(&schema),
2396 prefix,
2397 full_expr,
2398 k,
2399 8192,
2400 runtime,
2401 &metrics,
2402 Arc::new(RwLock::new(TopKDynamicFilters::new(Arc::new(
2403 DynamicFilterPhysicalExpr::new(vec![], lit(true)),
2404 )))),
2405 )?;
2406
2407 let batch1 = RecordBatch::try_new(
2410 Arc::clone(&schema),
2411 vec![
2412 Arc::new(BooleanArray::from(vec![false, false, true, true, true]))
2413 as ArrayRef,
2414 ],
2415 )?;
2416 topk.insert_batch(batch1)?;
2417
2418 let batch2 = RecordBatch::try_new(
2419 Arc::clone(&schema),
2420 vec![
2421 Arc::new(BooleanArray::from(vec![false, false, false, true, true]))
2422 as ArrayRef,
2423 ],
2424 )?;
2425 topk.insert_batch(batch2)?;
2426
2427 assert_eq!(
2430 topk.heap.store.len(),
2431 2,
2432 "store must keep 2 batches when savings would be marginal"
2433 );
2434 assert_eq!(topk.heap.inner.len(), 10, "heap should hold all 10 rows");
2435
2436 let results: Vec<_> = topk.emit()?.try_collect().await?;
2438 assert_batches_eq!(
2439 &[
2440 "+-------+",
2441 "| a |",
2442 "+-------+",
2443 "| false |",
2444 "| false |",
2445 "| false |",
2446 "| false |",
2447 "| false |",
2448 "| true |",
2449 "| true |",
2450 "| true |",
2451 "| true |",
2452 "| true |",
2453 "+-------+",
2454 ],
2455 &results
2456 );
2457
2458 Ok(())
2459 }
2460
2461 fn build_partitioned_topk(k: usize) -> Result<(Arc<Schema>, PartitionedTopK)> {
2465 build_partitioned_topk_with_opts(k, SortOptions::default(), false)
2466 }
2467
2468 fn build_partitioned_topk_with_opts(
2473 k: usize,
2474 val_sort_options: SortOptions,
2475 val_nullable: bool,
2476 ) -> Result<(Arc<Schema>, PartitionedTopK)> {
2477 let schema = Arc::new(Schema::new(vec![
2478 Field::new("pk", DataType::Int32, false),
2479 Field::new("val", DataType::Int32, val_nullable),
2480 ]));
2481
2482 let pk_expr: Arc<dyn PhysicalExpr> = col("pk", schema.as_ref())?;
2483 let pk_sort_expr = PhysicalSortExpr {
2484 expr: Arc::clone(&pk_expr),
2485 options: SortOptions::default(),
2486 };
2487 let val_sort_expr = PhysicalSortExpr {
2488 expr: col("val", schema.as_ref())?,
2489 options: val_sort_options,
2490 };
2491
2492 let partition_sort_fields = build_sort_fields(&[pk_sort_expr], &schema)?;
2493 let order_expr = LexOrdering::from([val_sort_expr]);
2494
2495 let state = PartitionedTopK::try_new(
2496 0,
2497 Arc::clone(&schema),
2498 vec![pk_expr],
2499 partition_sort_fields,
2500 order_expr,
2501 k,
2502 8, &Arc::new(RuntimeEnv::default()),
2504 &ExecutionPlanMetricsSet::new(),
2505 )?;
2506 Ok((schema, state))
2507 }
2508
2509 fn pk_val_batch(
2510 schema: &Arc<Schema>,
2511 pks: Vec<i32>,
2512 vals: Vec<i32>,
2513 ) -> Result<RecordBatch> {
2514 Ok(RecordBatch::try_new(
2515 Arc::clone(schema),
2516 vec![
2517 Arc::new(Int32Array::from(pks)),
2518 Arc::new(Int32Array::from(vals)),
2519 ],
2520 )?)
2521 }
2522
2523 fn nullable_pk_val_batch(
2526 schema: &Arc<Schema>,
2527 pks: Vec<i32>,
2528 vals: Vec<Option<i32>>,
2529 ) -> Result<RecordBatch> {
2530 Ok(RecordBatch::try_new(
2531 Arc::clone(schema),
2532 vec![
2533 Arc::new(Int32Array::from(pks)),
2534 Arc::new(Int32Array::from(vals)),
2535 ],
2536 )?)
2537 }
2538
2539 #[tokio::test]
2543 async fn test_partitioned_topk_multi_partition_within_batch() -> Result<()> {
2544 let (schema, mut state) = build_partitioned_topk(2)?;
2545
2546 let batch =
2550 pk_val_batch(&schema, vec![1, 2, 1, 2, 1, 3], vec![10, 20, 5, 15, 8, 7])?;
2551 state.insert_batch(&batch)?;
2552
2553 let results: Vec<_> = state.emit()?.try_collect().await?;
2554 assert_batches_eq!(
2555 &[
2556 "+----+-----+",
2557 "| pk | val |",
2558 "+----+-----+",
2559 "| 1 | 5 |",
2560 "| 1 | 8 |",
2561 "| 2 | 15 |",
2562 "| 2 | 20 |",
2563 "| 3 | 7 |",
2564 "+----+-----+",
2565 ],
2566 &results
2567 );
2568 Ok(())
2569 }
2570
2571 #[tokio::test]
2576 async fn test_partitioned_topk_cross_batch_eviction() -> Result<()> {
2577 let (schema, mut state) = build_partitioned_topk(2)?;
2578
2579 state.insert_batch(&pk_val_batch(&schema, vec![1, 1], vec![50, 40])?)?;
2581
2582 state.insert_batch(&pk_val_batch(
2585 &schema,
2586 vec![1, 2, 1],
2587 vec![10, 99, 60], )?)?;
2589
2590 let results: Vec<_> = state.emit()?.try_collect().await?;
2591 assert_batches_eq!(
2592 &[
2593 "+----+-----+",
2594 "| pk | val |",
2595 "+----+-----+",
2596 "| 1 | 10 |",
2597 "| 1 | 40 |",
2598 "| 2 | 99 |",
2599 "+----+-----+",
2600 ],
2601 &results
2602 );
2603 Ok(())
2604 }
2605
2606 #[tokio::test]
2608 async fn test_partitioned_topk_empty_input() -> Result<()> {
2609 let (_schema, state) = build_partitioned_topk(3)?;
2610 let results: Vec<_> = state.emit()?.try_collect().await?;
2611 assert!(results.is_empty(), "empty input → empty output");
2612 Ok(())
2613 }
2614
2615 #[tokio::test]
2618 async fn test_partitioned_topk_fetch_one() -> Result<()> {
2619 let (schema, mut state) = build_partitioned_topk(1)?;
2620 state.insert_batch(&pk_val_batch(
2621 &schema,
2622 vec![1, 1, 2, 2, 3],
2623 vec![3, 1, 9, 4, 7],
2624 )?)?;
2625
2626 let results: Vec<_> = state.emit()?.try_collect().await?;
2627 assert_batches_eq!(
2628 &[
2629 "+----+-----+",
2630 "| pk | val |",
2631 "+----+-----+",
2632 "| 1 | 1 |",
2633 "| 2 | 4 |",
2634 "| 3 | 7 |",
2635 "+----+-----+",
2636 ],
2637 &results
2638 );
2639 Ok(())
2640 }
2641
2642 #[tokio::test]
2647 async fn test_partitioned_topk_desc_ordering() -> Result<()> {
2648 let (schema, mut state) = build_partitioned_topk_with_opts(
2649 2,
2650 SortOptions {
2651 descending: true,
2652 nulls_first: false,
2653 },
2654 false,
2655 )?;
2656
2657 let batch = pk_val_batch(
2660 &schema,
2661 vec![1, 2, 1, 2, 1, 1, 2],
2662 vec![10, 20, 5, 15, 8, 12, 25],
2663 )?;
2664 state.insert_batch(&batch)?;
2665
2666 let results: Vec<_> = state.emit()?.try_collect().await?;
2667 assert_batches_eq!(
2668 &[
2669 "+----+-----+",
2670 "| pk | val |",
2671 "+----+-----+",
2672 "| 1 | 12 |",
2673 "| 1 | 10 |",
2674 "| 2 | 25 |",
2675 "| 2 | 20 |",
2676 "+----+-----+",
2677 ],
2678 &results
2679 );
2680 Ok(())
2681 }
2682
2683 #[tokio::test]
2689 async fn test_partitioned_topk_nulls_last_ordering() -> Result<()> {
2690 let (schema, mut state) = build_partitioned_topk_with_opts(
2691 1,
2692 SortOptions {
2693 descending: false,
2694 nulls_first: false,
2695 },
2696 true,
2697 )?;
2698
2699 let batch = nullable_pk_val_batch(
2703 &schema,
2704 vec![1, 2, 1, 1, 3, 3, 3],
2705 vec![None, None, Some(7), None, None, Some(4), Some(2)],
2706 )?;
2707 state.insert_batch(&batch)?;
2708
2709 let results: Vec<_> = state.emit()?.try_collect().await?;
2710 assert_batches_eq!(
2711 &[
2712 "+----+-----+",
2713 "| pk | val |",
2714 "+----+-----+",
2715 "| 1 | 7 |",
2716 "| 2 | |",
2717 "| 3 | 2 |",
2718 "+----+-----+",
2719 ],
2720 &results
2721 );
2722 Ok(())
2723 }
2724
2725 #[tokio::test]
2729 async fn test_partitioned_topk_nulls_first_ordering() -> Result<()> {
2730 let (schema, mut state) = build_partitioned_topk_with_opts(
2731 2,
2732 SortOptions {
2733 descending: false,
2734 nulls_first: true,
2735 },
2736 true,
2737 )?;
2738
2739 let batch = nullable_pk_val_batch(
2743 &schema,
2744 vec![1, 2, 1, 3, 1, 2, 1, 3],
2745 vec![
2746 None,
2747 Some(7),
2748 Some(5),
2749 Some(3),
2750 None,
2751 None,
2752 Some(8),
2753 Some(1),
2754 ],
2755 )?;
2756 state.insert_batch(&batch)?;
2757
2758 let results: Vec<_> = state.emit()?.try_collect().await?;
2759 assert_batches_eq!(
2760 &[
2761 "+----+-----+",
2762 "| pk | val |",
2763 "+----+-----+",
2764 "| 1 | |",
2765 "| 1 | |",
2766 "| 2 | |",
2767 "| 2 | 7 |",
2768 "| 3 | 1 |",
2769 "| 3 | 3 |",
2770 "+----+-----+",
2771 ],
2772 &results
2773 );
2774 Ok(())
2775 }
2776
2777 fn build_partitioned_topk_rank(
2788 k: usize,
2789 ) -> Result<(Arc<Schema>, PartitionedTopKRank)> {
2790 build_partitioned_topk_rank_with_opts(k, SortOptions::default(), false)
2791 }
2792
2793 fn build_partitioned_topk_rank_with_opts(
2797 k: usize,
2798 val_sort_options: SortOptions,
2799 val_nullable: bool,
2800 ) -> Result<(Arc<Schema>, PartitionedTopKRank)> {
2801 let schema = Arc::new(Schema::new(vec![
2802 Field::new("pk", DataType::Int32, false),
2803 Field::new("val", DataType::Int32, val_nullable),
2804 ]));
2805
2806 let pk_expr: Arc<dyn PhysicalExpr> = col("pk", schema.as_ref())?;
2807 let pk_sort_expr = PhysicalSortExpr {
2808 expr: Arc::clone(&pk_expr),
2809 options: SortOptions::default(),
2810 };
2811 let val_sort_expr = PhysicalSortExpr {
2812 expr: col("val", schema.as_ref())?,
2813 options: val_sort_options,
2814 };
2815
2816 let partition_sort_fields = build_sort_fields(&[pk_sort_expr], &schema)?;
2817 let order_expr = LexOrdering::from([val_sort_expr]);
2818
2819 let state = PartitionedTopKRank::try_new(
2820 0,
2821 Arc::clone(&schema),
2822 vec![pk_expr],
2823 partition_sort_fields,
2824 order_expr,
2825 k,
2826 8, &Arc::new(RuntimeEnv::default()),
2828 &ExecutionPlanMetricsSet::new(),
2829 )?;
2830 Ok((schema, state))
2831 }
2832
2833 #[tokio::test]
2838 async fn test_partitioned_topk_rank_multi_partition_within_batch() -> Result<()> {
2839 let (schema, mut state) = build_partitioned_topk_rank(2)?;
2840
2841 let batch =
2845 pk_val_batch(&schema, vec![1, 2, 1, 2, 1, 3], vec![10, 20, 5, 15, 8, 7])?;
2846 state.insert_batch(&batch)?;
2847
2848 let results: Vec<_> = state.emit()?.try_collect().await?;
2849 assert_batches_eq!(
2850 &[
2851 "+----+-----+",
2852 "| pk | val |",
2853 "+----+-----+",
2854 "| 1 | 5 |",
2855 "| 1 | 8 |",
2856 "| 2 | 15 |",
2857 "| 2 | 20 |",
2858 "| 3 | 7 |",
2859 "+----+-----+",
2860 ],
2861 &results
2862 );
2863 Ok(())
2864 }
2865
2866 #[tokio::test]
2871 async fn test_partitioned_topk_rank_cross_batch_eviction() -> Result<()> {
2872 let (schema, mut state) = build_partitioned_topk_rank(2)?;
2873
2874 state.insert_batch(&pk_val_batch(&schema, vec![1, 1], vec![50, 40])?)?;
2876
2877 state.insert_batch(&pk_val_batch(&schema, vec![1, 2, 1], vec![10, 99, 60])?)?;
2880
2881 let results: Vec<_> = state.emit()?.try_collect().await?;
2882 assert_batches_eq!(
2883 &[
2884 "+----+-----+",
2885 "| pk | val |",
2886 "+----+-----+",
2887 "| 1 | 10 |",
2888 "| 1 | 40 |",
2889 "| 2 | 99 |",
2890 "+----+-----+",
2891 ],
2892 &results
2893 );
2894 Ok(())
2895 }
2896
2897 #[tokio::test]
2899 async fn test_partitioned_topk_rank_empty_input() -> Result<()> {
2900 let (_schema, state) = build_partitioned_topk_rank(3)?;
2901 let results: Vec<_> = state.emit()?.try_collect().await?;
2902 assert!(results.is_empty(), "empty input → empty output");
2903 Ok(())
2904 }
2905
2906 #[tokio::test]
2911 async fn test_partitioned_topk_rank_fetch_one() -> Result<()> {
2912 let (schema, mut state) = build_partitioned_topk_rank(1)?;
2913 state.insert_batch(&pk_val_batch(
2914 &schema,
2915 vec![1, 1, 2, 2, 3],
2916 vec![3, 1, 9, 4, 7],
2917 )?)?;
2918
2919 let results: Vec<_> = state.emit()?.try_collect().await?;
2920 assert_batches_eq!(
2921 &[
2922 "+----+-----+",
2923 "| pk | val |",
2924 "+----+-----+",
2925 "| 1 | 1 |",
2926 "| 2 | 4 |",
2927 "| 3 | 7 |",
2928 "+----+-----+",
2929 ],
2930 &results
2931 );
2932 Ok(())
2933 }
2934
2935 #[tokio::test]
2940 async fn test_partitioned_topk_rank_desc_ordering() -> Result<()> {
2941 let (schema, mut state) = build_partitioned_topk_rank_with_opts(
2942 2,
2943 SortOptions {
2944 descending: true,
2945 nulls_first: false,
2946 },
2947 false,
2948 )?;
2949
2950 let batch = pk_val_batch(
2953 &schema,
2954 vec![1, 2, 1, 2, 1, 1, 2],
2955 vec![10, 20, 5, 15, 8, 12, 25],
2956 )?;
2957 state.insert_batch(&batch)?;
2958
2959 let results: Vec<_> = state.emit()?.try_collect().await?;
2960 assert_batches_eq!(
2961 &[
2962 "+----+-----+",
2963 "| pk | val |",
2964 "+----+-----+",
2965 "| 1 | 12 |",
2966 "| 1 | 10 |",
2967 "| 2 | 25 |",
2968 "| 2 | 20 |",
2969 "+----+-----+",
2970 ],
2971 &results
2972 );
2973 Ok(())
2974 }
2975
2976 #[tokio::test]
2982 async fn test_partitioned_topk_rank_nulls_last_ordering() -> Result<()> {
2983 let (schema, mut state) = build_partitioned_topk_rank_with_opts(
2984 1,
2985 SortOptions {
2986 descending: false,
2987 nulls_first: false,
2988 },
2989 true,
2990 )?;
2991
2992 let batch = nullable_pk_val_batch(
2996 &schema,
2997 vec![1, 2, 1, 1, 3, 3, 3],
2998 vec![None, None, Some(7), None, None, Some(4), Some(2)],
2999 )?;
3000 state.insert_batch(&batch)?;
3001
3002 let results: Vec<_> = state.emit()?.try_collect().await?;
3003 assert_batches_eq!(
3004 &[
3005 "+----+-----+",
3006 "| pk | val |",
3007 "+----+-----+",
3008 "| 1 | 7 |",
3009 "| 2 | |",
3010 "| 3 | 2 |",
3011 "+----+-----+",
3012 ],
3013 &results
3014 );
3015 Ok(())
3016 }
3017
3018 #[tokio::test]
3022 async fn test_partitioned_topk_rank_nulls_first_ordering() -> Result<()> {
3023 let (schema, mut state) = build_partitioned_topk_rank_with_opts(
3024 2,
3025 SortOptions {
3026 descending: false,
3027 nulls_first: true,
3028 },
3029 true,
3030 )?;
3031
3032 let batch = nullable_pk_val_batch(
3036 &schema,
3037 vec![1, 2, 1, 3, 1, 2, 1, 3],
3038 vec![
3039 None,
3040 Some(7),
3041 Some(5),
3042 Some(3),
3043 None,
3044 None,
3045 Some(8),
3046 Some(1),
3047 ],
3048 )?;
3049 state.insert_batch(&batch)?;
3050
3051 let results: Vec<_> = state.emit()?.try_collect().await?;
3052 assert_batches_eq!(
3053 &[
3054 "+----+-----+",
3055 "| pk | val |",
3056 "+----+-----+",
3057 "| 1 | |",
3058 "| 1 | |",
3059 "| 2 | |",
3060 "| 2 | 7 |",
3061 "| 3 | 1 |",
3062 "| 3 | 3 |",
3063 "+----+-----+",
3064 ],
3065 &results
3066 );
3067 Ok(())
3068 }
3069
3070 #[tokio::test]
3075 async fn test_partitioned_topk_rank_boundary_ties_retained() -> Result<()> {
3076 let (schema, mut state) = build_partitioned_topk_rank(2)?;
3077
3078 let batch = pk_val_batch(&schema, vec![1, 1, 1, 1], vec![5, 5, 10, 5])?;
3084 state.insert_batch(&batch)?;
3085
3086 let results: Vec<_> = state.emit()?.try_collect().await?;
3087 assert_batches_eq!(
3088 &[
3089 "+----+-----+",
3090 "| pk | val |",
3091 "+----+-----+",
3092 "| 1 | 5 |",
3093 "| 1 | 5 |",
3094 "| 1 | 5 |",
3095 "+----+-----+",
3096 ],
3097 &results
3098 );
3099 Ok(())
3100 }
3101
3102 #[tokio::test]
3108 async fn test_partitioned_topk_rank_boundary_shifts_clears_ties() -> Result<()> {
3109 let (schema, mut state) = build_partitioned_topk_rank(2)?;
3110
3111 let batch = pk_val_batch(&schema, vec![1, 1, 1, 1, 1], vec![10, 10, 10, 5, 3])?;
3120 state.insert_batch(&batch)?;
3121
3122 let results: Vec<_> = state.emit()?.try_collect().await?;
3123 assert_batches_eq!(
3124 &[
3125 "+----+-----+",
3126 "| pk | val |",
3127 "+----+-----+",
3128 "| 1 | 3 |",
3129 "| 1 | 5 |",
3130 "+----+-----+",
3131 ],
3132 &results
3133 );
3134 Ok(())
3135 }
3136
3137 #[tokio::test]
3143 async fn test_partitioned_topk_rank_eviction_at_unchanged_boundary() -> Result<()> {
3144 let (schema, mut state) = build_partitioned_topk_rank(2)?;
3145
3146 let batch = pk_val_batch(&schema, vec![1, 1, 1], vec![10, 10, 5])?;
3152 state.insert_batch(&batch)?;
3153
3154 let results: Vec<_> = state.emit()?.try_collect().await?;
3155 assert_batches_eq!(
3156 &[
3157 "+----+-----+",
3158 "| pk | val |",
3159 "+----+-----+",
3160 "| 1 | 5 |",
3161 "| 1 | 10 |",
3162 "| 1 | 10 |",
3163 "+----+-----+",
3164 ],
3165 &results
3166 );
3167 Ok(())
3168 }
3169}