use arrow::{
array::{Array, AsArray},
compute::{
BatchCoalescer, FilterBuilder, interleave_record_batch, prep_null_mask_filter,
take_record_batch,
},
row::{OwnedRow, RowConverter, Rows, SortField},
};
use datafusion_expr::{ColumnarValue, Operator};
use std::mem::size_of;
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
use std::{cmp::Ordering, collections::BinaryHeap, sync::Arc};
use super::metrics::{
BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, MetricCategory,
RecordOutput,
};
use crate::spill::get_record_batch_memory_size;
use crate::{SendableRecordBatchStream, stream::RecordBatchStreamAdapter};
use arrow::array::{ArrayRef, RecordBatch, UInt32Array};
use arrow::datatypes::SchemaRef;
use datafusion_common::{
HashMap, Result, ScalarValue, internal_datafusion_err, internal_err,
};
use datafusion_execution::{
memory_pool::{MemoryConsumer, MemoryReservation},
runtime_env::RuntimeEnv,
};
use datafusion_physical_expr::{
PhysicalExpr,
expressions::{BinaryExpr, DynamicFilterPhysicalExpr, is_not_null, is_null, lit},
};
use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
use parking_lot::RwLock;
pub struct TopK {
schema: SchemaRef,
metrics: TopKMetrics,
reservation: MemoryReservation,
batch_size: usize,
expr: LexOrdering,
row_converter: RowConverter,
scratch_rows: Rows,
heap: TopKHeap,
common_sort_prefix_converter: Option<RowConverter>,
common_sort_prefix: Arc<[PhysicalSortExpr]>,
filter: Arc<RwLock<TopKDynamicFilters>>,
pub(crate) finished: bool,
}
#[derive(Debug)]
pub struct TopKDynamicFilters {
shared_threshold: Option<TopKThreshold>,
expr: Arc<DynamicFilterPhysicalExpr>,
remaining_topk_emitters: AtomicUsize,
}
#[derive(Debug, Clone)]
struct TopKThreshold {
full_sort_key_row: Vec<u8>,
common_prefix_row: Option<Vec<u8>>,
}
impl TopKThreshold {
fn new(full_sort_key_row: Vec<u8>, common_prefix_row: Option<Vec<u8>>) -> Self {
Self {
full_sort_key_row,
common_prefix_row,
}
}
fn full_sort_key_row(&self) -> &[u8] {
self.full_sort_key_row.as_slice()
}
fn common_prefix_row(&self) -> Option<&[u8]> {
self.common_prefix_row.as_deref()
}
fn is_more_selective_than(&self, current: &Self) -> bool {
self.full_sort_key_row() < current.full_sort_key_row()
}
}
#[derive(Clone, Copy)]
struct TopKHeapBoundaryRow<'a> {
row: &'a TopKRow,
}
impl<'a> TopKHeapBoundaryRow<'a> {
fn new(row: &'a TopKRow) -> Self {
Self { row }
}
fn full_sort_key_row(&self) -> &[u8] {
self.row.row()
}
fn is_more_selective_than(&self, current: Option<&TopKThreshold>) -> bool {
current
.map(|current| self.full_sort_key_row() < current.full_sort_key_row())
.unwrap_or(true)
}
}
#[derive(Clone, Copy)]
struct TopKHeapBoundary<'a> {
row: &'a TopKRow,
batch: &'a RecordBatch,
}
impl<'a> TopKHeapBoundary<'a> {
fn new(row: &'a TopKRow, batch: &'a RecordBatch) -> Self {
Self { row, batch }
}
fn threshold_values(
&self,
sort_exprs: &[PhysicalSortExpr],
) -> Result<Vec<ScalarValue>> {
let mut scalar_values = Vec::with_capacity(sort_exprs.len());
for sort_expr in sort_exprs {
let value = sort_expr
.expr
.evaluate(&self.batch.slice(self.row.index, 1))?;
let scalar = match value {
ColumnarValue::Scalar(scalar) => scalar,
ColumnarValue::Array(array) if array.len() == 1 => {
ScalarValue::try_from_array(&array, 0)?
}
array => {
return internal_err!("Expected a scalar value, got {:?}", array);
}
};
scalar_values.push(scalar);
}
Ok(scalar_values)
}
fn threshold(&self, common_prefix_row: Option<Vec<u8>>) -> TopKThreshold {
TopKThreshold::new(self.row.row().to_vec(), common_prefix_row)
}
}
impl TopKDynamicFilters {
pub fn new(expr: Arc<DynamicFilterPhysicalExpr>) -> Self {
Self::new_with_topk_emitter_count(expr, 1)
}
pub fn new_with_topk_emitter_count(
expr: Arc<DynamicFilterPhysicalExpr>,
topk_emitter_count: usize,
) -> Self {
debug_assert!(topk_emitter_count > 0);
Self {
shared_threshold: None,
expr,
remaining_topk_emitters: AtomicUsize::new(topk_emitter_count),
}
}
pub fn expr(&self) -> Arc<DynamicFilterPhysicalExpr> {
Arc::clone(&self.expr)
}
fn mark_topk_emitted(&self) {
let previous = self
.remaining_topk_emitters
.fetch_update(
AtomicOrdering::AcqRel,
AtomicOrdering::Acquire,
|remaining| remaining.checked_sub(1),
)
.unwrap_or(0);
debug_assert!(
previous > 0,
"TopK dynamic filter emitter completed more times than expected"
);
if previous == 1 {
self.expr.mark_complete();
}
}
}
const ESTIMATED_BYTES_PER_ROW: usize = 20;
#[derive(Debug, Clone)]
pub(crate) struct EvictedRow {
pub batch: RecordBatch,
pub index: usize,
pub row_bytes: Vec<u8>,
}
pub(crate) fn build_sort_fields(
ordering: &[PhysicalSortExpr],
schema: &SchemaRef,
) -> Result<Vec<SortField>> {
ordering
.iter()
.map(|e| {
Ok(SortField::new_with_options(
e.expr.data_type(schema)?,
e.options,
))
})
.collect::<Result<_>>()
}
impl TopK {
#[expect(clippy::too_many_arguments)]
#[expect(clippy::needless_pass_by_value)]
pub fn try_new(
partition_id: usize,
schema: SchemaRef,
common_sort_prefix: Vec<PhysicalSortExpr>,
expr: LexOrdering,
k: usize,
batch_size: usize,
runtime: Arc<RuntimeEnv>,
metrics: &ExecutionPlanMetricsSet,
filter: Arc<RwLock<TopKDynamicFilters>>,
) -> Result<Self> {
let reservation = MemoryConsumer::new(format!("TopK[{partition_id}]"))
.register(&runtime.memory_pool);
let sort_fields = build_sort_fields(&expr, &schema)?;
let row_converter = RowConverter::new(sort_fields)?;
let scratch_rows =
row_converter.empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size);
let common_prefix_row_converter = if common_sort_prefix.is_empty() {
None
} else {
let input_sort_fields = build_sort_fields(&common_sort_prefix, &schema)?;
Some(RowConverter::new(input_sort_fields)?)
};
Ok(Self {
schema: Arc::clone(&schema),
metrics: TopKMetrics::new(metrics, partition_id),
reservation,
batch_size,
expr,
row_converter,
scratch_rows,
heap: TopKHeap::new(k),
common_sort_prefix_converter: common_prefix_row_converter,
common_sort_prefix: Arc::from(common_sort_prefix),
finished: false,
filter,
})
}
#[expect(clippy::needless_pass_by_value)]
pub fn insert_batch(&mut self, batch: RecordBatch) -> Result<()> {
let baseline = self.metrics.baseline.clone();
let _timer = baseline.elapsed_compute().timer();
let mut sort_keys: Vec<ArrayRef> = self
.expr
.iter()
.map(|expr| {
let value = expr.expr.evaluate(&batch)?;
value.into_array(batch.num_rows())
})
.collect::<Result<Vec<_>>>()?;
let mut selected_rows = None;
let filter = self.filter.read().expr.current()?;
let filtered = filter.evaluate(&batch)?;
let num_rows = batch.num_rows();
let array = filtered.into_array(num_rows)?;
let mut filter = array.as_boolean().clone();
if !filter.has_true() {
self.attempt_early_completion(&batch)?;
return Ok(());
}
if filter.null_count() > 0 || filter.has_false() {
if filter.nulls().is_some() {
filter = prep_null_mask_filter(&filter);
}
let filter_predicate = FilterBuilder::new(&filter);
let filter_predicate = if sort_keys.len() > 1 {
filter_predicate.optimize().build()
} else {
filter_predicate.build()
};
selected_rows = Some(filter);
sort_keys = sort_keys
.iter()
.map(|key| filter_predicate.filter(key).map_err(|x| x.into()))
.collect::<Result<Vec<_>>>()?;
}
let rows = &mut self.scratch_rows;
rows.clear();
self.row_converter.append(rows, &sort_keys)?;
let mut batch_entry = self.heap.register_batch(batch.clone());
let replacements = match selected_rows {
Some(filter) => {
self.find_new_topk_items(filter.values().set_indices(), &mut batch_entry)
}
None => self.find_new_topk_items(0..sort_keys[0].len(), &mut batch_entry),
};
if replacements > 0 {
self.metrics.row_replacements.add(replacements);
self.heap.insert_batch_entry(batch_entry);
self.heap.maybe_compact()?;
self.reservation.try_resize(self.size())?;
self.attempt_early_completion(&batch)?;
self.update_filter()?;
} else {
self.attempt_early_completion(&batch)?;
}
Ok(())
}
fn find_new_topk_items(
&mut self,
items: impl Iterator<Item = usize>,
batch_entry: &mut RecordBatchEntry,
) -> usize {
let mut replacements = 0;
let rows = &mut self.scratch_rows;
for (index, row) in items.zip(rows.iter()) {
match self.heap.max() {
Some(max_row) if row.as_ref() >= max_row.row() => {}
None | Some(_) => {
self.heap.add(batch_entry, row, index);
replacements += 1;
}
}
}
replacements
}
fn current_heap_boundary_row(&self) -> Option<TopKHeapBoundaryRow<'_>> {
self.heap.max().map(TopKHeapBoundaryRow::new)
}
fn current_heap_boundary(&self) -> Result<Option<TopKHeapBoundary<'_>>> {
let Some(row) = self.heap.max() else {
return Ok(None);
};
self.heap_boundary(row).map(Some)
}
fn heap_boundary<'a>(&'a self, row: &'a TopKRow) -> Result<TopKHeapBoundary<'a>> {
let batch_entry = self
.heap
.store
.get(row.batch_id)
.ok_or_else(|| internal_datafusion_err!("Invalid batch ID in TopKRow"))?;
Ok(TopKHeapBoundary::new(row, &batch_entry.batch))
}
fn update_filter(&mut self) -> Result<()> {
let Some(boundary_row) = self.current_heap_boundary_row() else {
return Ok(());
};
let needs_update = {
let filter = self.filter.read();
boundary_row.is_more_selective_than(filter.shared_threshold.as_ref())
};
if !needs_update {
return Ok(());
}
let boundary = self.heap_boundary(boundary_row.row)?;
let thresholds = boundary.threshold_values(&self.expr)?;
let predicate = Self::build_filter_expression(&self.expr, &thresholds)?;
let new_threshold =
boundary.threshold(self.encode_topk_common_prefix_row(boundary)?);
let mut filter = self.filter.write();
let still_needs_update = filter
.shared_threshold
.as_ref()
.map(|current| new_threshold.is_more_selective_than(current))
.unwrap_or(true);
if !still_needs_update {
return Ok(());
}
filter.shared_threshold = Some(new_threshold);
if let Some(pred) = predicate
&& !pred.eq(&lit(true))
{
filter.expr.update(pred)?;
}
Ok(())
}
fn build_filter_expression(
sort_exprs: &[PhysicalSortExpr],
thresholds: &[ScalarValue],
) -> Result<Option<Arc<dyn PhysicalExpr>>> {
let mut filters: Vec<Arc<dyn PhysicalExpr>> =
Vec::with_capacity(thresholds.len());
let mut prev_sort_expr: Option<Arc<dyn PhysicalExpr>> = None;
for (sort_expr, value) in sort_exprs.iter().zip(thresholds.iter()) {
let op = if sort_expr.options.descending {
Operator::Gt
} else {
Operator::Lt
};
let value_null = value.is_null();
let comparison = Arc::new(BinaryExpr::new(
Arc::clone(&sort_expr.expr),
op,
lit(value.clone()),
));
let comparison_with_null = match (sort_expr.options.nulls_first, value_null) {
(true, true) => lit(false),
(true, false) => Arc::new(BinaryExpr::new(
is_null(Arc::clone(&sort_expr.expr))?,
Operator::Or,
comparison,
)),
(false, true) => is_not_null(Arc::clone(&sort_expr.expr))?,
(false, false) => comparison,
};
let mut eq_expr = Arc::new(BinaryExpr::new(
Arc::clone(&sort_expr.expr),
Operator::Eq,
lit(value.clone()),
));
if value_null {
eq_expr = Arc::new(BinaryExpr::new(
is_null(Arc::clone(&sort_expr.expr))?,
Operator::Or,
eq_expr,
));
}
match prev_sort_expr.take() {
None => {
prev_sort_expr = Some(eq_expr);
filters.push(comparison_with_null);
}
Some(p) => {
filters.push(Arc::new(BinaryExpr::new(
Arc::clone(&p),
Operator::And,
comparison_with_null,
)));
prev_sort_expr =
Some(Arc::new(BinaryExpr::new(p, Operator::And, eq_expr)));
}
}
}
let dynamic_predicate = filters
.into_iter()
.reduce(|a, b| Arc::new(BinaryExpr::new(a, Operator::Or, b)));
Ok(dynamic_predicate)
}
fn attempt_early_completion(&mut self, batch: &RecordBatch) -> Result<()> {
if batch.num_rows() == 0 {
return Ok(());
}
let Some(prefix_converter) = &self.common_sort_prefix_converter else {
return Ok(());
};
let last_row_idx = batch.num_rows() - 1;
let mut batch_prefix_scratch =
prefix_converter.empty_rows(1, ESTIMATED_BYTES_PER_ROW);
self.append_common_prefix_row(
prefix_converter,
batch,
last_row_idx,
&mut batch_prefix_scratch,
)?;
let batch_common_prefix_row = batch_prefix_scratch.row(0);
let batch_common_prefix = batch_common_prefix_row.as_ref();
let finished_by_shared_threshold = self
.filter
.read()
.shared_threshold
.as_ref()
.and_then(TopKThreshold::common_prefix_row)
.map(|common_prefix_row| batch_common_prefix > common_prefix_row)
.unwrap_or(false);
if finished_by_shared_threshold {
self.finished = true;
return Ok(());
}
let Some(boundary) = self.current_heap_boundary()? else {
return Ok(());
};
if self.batch_prefix_exceeds_heap_boundary(batch_common_prefix, boundary)? {
self.finished = true;
}
Ok(())
}
fn batch_prefix_exceeds_heap_boundary(
&self,
batch_common_prefix: &[u8],
boundary: TopKHeapBoundary<'_>,
) -> Result<bool> {
let Some(heap_common_prefix_row) =
self.encode_topk_common_prefix_row(boundary)?
else {
return Ok(false);
};
Ok(batch_common_prefix > heap_common_prefix_row.as_slice())
}
fn encode_topk_common_prefix_row(
&self,
boundary: TopKHeapBoundary<'_>,
) -> Result<Option<Vec<u8>>> {
let Some(prefix_converter) = &self.common_sort_prefix_converter else {
return Ok(None);
};
let mut scratch = prefix_converter.empty_rows(1, ESTIMATED_BYTES_PER_ROW);
self.append_common_prefix_row(
prefix_converter,
boundary.batch,
boundary.row.index,
&mut scratch,
)?;
Ok(Some(scratch.row(0).as_ref().to_vec()))
}
fn append_common_prefix_row(
&self,
prefix_converter: &RowConverter,
batch: &RecordBatch,
row_idx: usize,
scratch: &mut Rows,
) -> Result<()> {
let row = batch.slice(row_idx, 1);
let prefix_columns: Vec<ArrayRef> = self
.common_sort_prefix
.iter()
.map(|expr| expr.expr.evaluate(&row)?.into_array(1))
.collect::<Result<_>>()?;
prefix_converter.append(scratch, &prefix_columns)?;
Ok(())
}
pub fn emit(self) -> Result<SendableRecordBatchStream> {
let Self {
schema,
metrics,
reservation: _,
batch_size,
expr: _,
row_converter: _,
scratch_rows: _,
mut heap,
common_sort_prefix_converter: _,
common_sort_prefix: _,
finished: _,
filter,
} = self;
let _timer = metrics.baseline.elapsed_compute().timer();
filter.read().mark_topk_emitted();
let mut batches = vec![];
if let Some(mut batch) = heap.emit()? {
(&batch).record_output(&metrics.baseline);
loop {
if batch.num_rows() <= batch_size {
batches.push(Ok(batch));
break;
} else {
batches.push(Ok(batch.slice(0, batch_size)));
let remaining_length = batch.num_rows() - batch_size;
batch = batch.slice(batch_size, remaining_length);
}
}
};
Ok(Box::pin(RecordBatchStreamAdapter::new(
schema,
futures::stream::iter(batches),
)))
}
fn size(&self) -> usize {
size_of::<Self>()
+ self.row_converter.size()
+ self.scratch_rows.size()
+ self.heap.size()
}
}
struct TopKMetrics {
pub baseline: BaselineMetrics,
pub row_replacements: Count,
}
impl TopKMetrics {
fn new(metrics: &ExecutionPlanMetricsSet, partition: usize) -> Self {
Self {
baseline: BaselineMetrics::new(metrics, partition),
row_replacements: MetricBuilder::new(metrics)
.with_category(MetricCategory::Rows)
.counter("row_replacements", partition),
}
}
}
struct TopKHeap {
k: usize,
inner: BinaryHeap<TopKRow>,
store: RecordBatchStore,
owned_bytes: usize,
}
impl TopKHeap {
fn new(k: usize) -> Self {
assert!(k > 0);
Self {
k,
inner: BinaryHeap::new(),
store: RecordBatchStore::new(),
owned_bytes: 0,
}
}
pub fn register_batch(&mut self, batch: RecordBatch) -> RecordBatchEntry {
self.store.register(batch)
}
pub fn insert_batch_entry(&mut self, entry: RecordBatchEntry) {
self.store.insert(entry)
}
fn max(&self) -> Option<&TopKRow> {
if self.inner.len() < self.k {
None
} else {
self.inner.peek()
}
}
fn add(
&mut self,
batch_entry: &mut RecordBatchEntry,
row: impl AsRef<[u8]>,
index: usize,
) -> Option<EvictedRow> {
let batch_id = batch_entry.id;
batch_entry.uses += 1;
assert!(self.inner.len() <= self.k);
let row = row.as_ref();
if self.inner.len() == self.k {
let mut prev_min = self.inner.peek_mut().unwrap();
let evicted_batch = if prev_min.batch_id == batch_entry.id {
batch_entry.batch.clone()
} else {
self.store
.get(prev_min.batch_id)
.map(|entry| entry.batch.clone())
.expect("evicted row's batch must be present in the store")
};
let evicted = EvictedRow {
batch: evicted_batch,
index: prev_min.index,
row_bytes: prev_min.row.clone(),
};
if prev_min.batch_id == batch_entry.id {
batch_entry.uses -= 1;
} else {
self.store.unuse(prev_min.batch_id);
}
self.owned_bytes -= prev_min.owned_size();
prev_min.replace_with(row, batch_id, index);
self.owned_bytes += prev_min.owned_size();
Some(evicted)
} else {
let new_row = TopKRow::new(row, batch_id, index);
self.owned_bytes += new_row.owned_size();
self.inner.push(new_row);
None
}
}
pub fn emit(&mut self) -> Result<Option<RecordBatch>> {
Ok(self.emit_with_state()?.0)
}
fn emit_with_state(&mut self) -> Result<(Option<RecordBatch>, Vec<TopKRow>)> {
let topk_rows = std::mem::take(&mut self.inner).into_sorted_vec();
if self.store.is_empty() {
return Ok((None, topk_rows));
}
let mut record_batches = Vec::new();
let mut batch_id_array_pos = HashMap::new();
for (array_pos, (batch_id, batch)) in self.store.batches.iter().enumerate() {
record_batches.push(&batch.batch);
batch_id_array_pos.insert(*batch_id, array_pos);
}
let indices: Vec<_> = topk_rows
.iter()
.map(|k| (batch_id_array_pos[&k.batch_id], k.index))
.collect();
let new_batch = interleave_record_batch(&record_batches, &indices)?;
Ok((Some(new_batch), topk_rows))
}
pub fn maybe_compact(&mut self) -> Result<()> {
if self.store.len() <= 1 {
return Ok(());
}
let total_rows = self.store.total_rows;
let num_rows = self.inner.len();
if total_rows <= num_rows * 2 {
return Ok(());
}
let (new_batch, mut topk_rows) = self.emit_with_state()?;
let Some(new_batch) = new_batch else {
return Ok(());
};
self.store.clear();
let mut batch_entry = self.register_batch(new_batch);
batch_entry.uses = num_rows;
for (i, topk_row) in topk_rows.iter_mut().enumerate() {
topk_row.batch_id = batch_entry.id;
topk_row.index = i;
}
self.insert_batch_entry(batch_entry);
self.inner = BinaryHeap::from(topk_rows);
Ok(())
}
fn size(&self) -> usize {
size_of::<Self>()
+ (self.inner.capacity() * size_of::<TopKRow>())
+ self.store.size()
+ self.owned_bytes
}
}
#[derive(Debug, PartialEq)]
struct TopKRow {
row: Vec<u8>,
batch_id: u32,
index: usize,
}
impl TopKRow {
fn new(row: impl AsRef<[u8]>, batch_id: u32, index: usize) -> Self {
Self {
row: row.as_ref().to_vec(),
batch_id,
index,
}
}
fn replace_with(&mut self, new_row: impl AsRef<[u8]>, batch_id: u32, index: usize) {
self.row.clear();
self.row.extend_from_slice(new_row.as_ref());
self.batch_id = batch_id;
self.index = index;
}
fn owned_size(&self) -> usize {
self.row.capacity()
}
fn row(&self) -> &[u8] {
self.row.as_slice()
}
}
impl Eq for TopKRow {}
impl PartialOrd for TopKRow {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for TopKRow {
fn cmp(&self, other: &Self) -> Ordering {
self.row.cmp(&other.row)
}
}
#[derive(Debug)]
struct RecordBatchEntry {
id: u32,
batch: RecordBatch,
uses: usize,
}
#[derive(Debug)]
struct RecordBatchStore {
next_id: u32,
batches: HashMap<u32, RecordBatchEntry>,
batches_size: usize,
total_rows: usize,
}
impl RecordBatchStore {
fn new() -> Self {
Self {
next_id: 0,
batches: HashMap::new(),
batches_size: 0,
total_rows: 0,
}
}
pub fn register(&mut self, batch: RecordBatch) -> RecordBatchEntry {
let id = self.next_id;
self.next_id += 1;
RecordBatchEntry { id, batch, uses: 0 }
}
pub fn insert(&mut self, entry: RecordBatchEntry) {
if entry.uses > 0 {
self.batches_size += get_record_batch_memory_size(&entry.batch);
self.total_rows += entry.batch.num_rows();
self.batches.insert(entry.id, entry);
}
}
fn clear(&mut self) {
self.batches.clear();
self.batches_size = 0;
self.total_rows = 0;
}
fn get(&self, id: u32) -> Option<&RecordBatchEntry> {
self.batches.get(&id)
}
fn len(&self) -> usize {
self.batches.len()
}
fn is_empty(&self) -> bool {
self.batches.is_empty()
}
pub fn unuse(&mut self, id: u32) {
let remove = if let Some(batch_entry) = self.batches.get_mut(&id) {
batch_entry.uses = batch_entry.uses.checked_sub(1).expect("underflow");
batch_entry.uses == 0
} else {
panic!("No entry for id {id}");
};
if remove {
let old_entry = self.batches.remove(&id).unwrap();
self.batches_size = self
.batches_size
.checked_sub(get_record_batch_memory_size(&old_entry.batch))
.unwrap();
self.total_rows = self
.total_rows
.checked_sub(old_entry.batch.num_rows())
.unwrap();
}
}
pub fn size(&self) -> usize {
size_of::<Self>()
+ self.batches.capacity() * (size_of::<u32>() + size_of::<RecordBatchEntry>())
+ self.batches_size
}
}
pub(crate) struct PartitionedTopK {
schema: SchemaRef,
metrics: TopKMetrics,
reservation: MemoryReservation,
expr: LexOrdering,
row_converter: RowConverter,
scratch_rows: Rows,
partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
partition_converter: RowConverter,
heaps: HashMap<OwnedRow, TopKHeap>,
k: usize,
batch_size: usize,
}
impl PartitionedTopK {
#[expect(clippy::too_many_arguments)]
pub(crate) fn try_new(
partition_id: usize,
schema: SchemaRef,
partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
partition_sort_fields: Vec<SortField>,
order_expr: LexOrdering,
k: usize,
batch_size: usize,
runtime: &Arc<RuntimeEnv>,
metrics: &ExecutionPlanMetricsSet,
) -> Result<Self> {
assert!(k > 0, "PartitionedTopK requires k > 0");
let reservation = MemoryConsumer::new(format!("PartitionedTopK[{partition_id}]"))
.register(&runtime.memory_pool);
let order_sort_fields = build_sort_fields(&order_expr, &schema)?;
let row_converter = RowConverter::new(order_sort_fields)?;
let scratch_rows =
row_converter.empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size);
let partition_converter = RowConverter::new(partition_sort_fields)?;
Ok(Self {
schema,
metrics: TopKMetrics::new(metrics, partition_id),
reservation,
expr: order_expr,
row_converter,
scratch_rows,
partition_exprs,
partition_converter,
heaps: HashMap::new(),
k,
batch_size,
})
}
pub(crate) fn insert_batch(&mut self, batch: &RecordBatch) -> Result<()> {
let baseline = self.metrics.baseline.clone();
let _timer = baseline.elapsed_compute().timer();
let num_rows = batch.num_rows();
if num_rows == 0 {
return Ok(());
}
let pk_arrays: Vec<ArrayRef> = self
.partition_exprs
.iter()
.map(|e| e.evaluate(batch).and_then(|v| v.into_array(num_rows)))
.collect::<Result<_>>()?;
let pk_rows = self.partition_converter.convert_columns(&pk_arrays)?;
let mut groups: HashMap<OwnedRow, Vec<u32>> = HashMap::new();
for i in 0..num_rows {
groups
.entry(pk_rows.row(i).owned())
.or_default()
.push(i as u32);
}
let ob_arrays: Vec<ArrayRef> = self
.expr
.iter()
.map(|e| e.expr.evaluate(batch).and_then(|v| v.into_array(num_rows)))
.collect::<Result<_>>()?;
self.scratch_rows.clear();
self.row_converter
.append(&mut self.scratch_rows, &ob_arrays)?;
let k = self.k;
let mut replacements: usize = 0;
for (pk, indices) in groups {
let heap = self.heaps.entry(pk).or_insert_with(|| TopKHeap::new(k));
let any_qualify = indices.iter().any(|&orig_idx| {
let bytes = self.scratch_rows.row(orig_idx as usize);
match heap.max() {
Some(max_row) => bytes.as_ref() < max_row.row(),
None => true,
}
});
if !any_qualify {
continue;
}
let indices_arr = UInt32Array::from(indices);
let sub_batch = take_record_batch(batch, &indices_arr)?;
let mut entry = heap.register_batch(sub_batch);
for (sub_idx, &orig_idx) in indices_arr.values().iter().enumerate() {
let row = self.scratch_rows.row(orig_idx as usize);
match heap.max() {
Some(max_row) if row.as_ref() >= max_row.row() => {}
None | Some(_) => {
heap.add(&mut entry, row, sub_idx);
replacements += 1;
}
}
}
heap.insert_batch_entry(entry);
heap.maybe_compact()?;
}
if replacements > 0 {
self.metrics.row_replacements.add(replacements);
}
self.reservation.try_resize(self.size())?;
Ok(())
}
pub(crate) fn emit(self) -> Result<SendableRecordBatchStream> {
let Self {
schema,
metrics,
reservation: _,
expr: _,
row_converter: _,
scratch_rows: _,
partition_exprs: _,
partition_converter: _,
mut heaps,
k: _,
batch_size,
} = self;
let _timer = metrics.baseline.elapsed_compute().timer();
let mut sorted_pks: Vec<OwnedRow> = heaps.keys().cloned().collect();
sorted_pks.sort();
let mut coalescer = BatchCoalescer::new(Arc::clone(&schema), batch_size);
for pk in sorted_pks {
let mut heap = heaps.remove(&pk).expect("key from heaps.keys()");
if let Some(batch) = heap.emit()? {
(&batch).record_output(&metrics.baseline);
coalescer.push_batch(batch)?;
}
}
coalescer.finish_buffered_batch()?;
let mut out: Vec<Result<RecordBatch>> = Vec::new();
while let Some(b) = coalescer.next_completed_batch() {
out.push(Ok(b));
}
Ok(Box::pin(RecordBatchStreamAdapter::new(
schema,
futures::stream::iter(out),
)))
}
fn size(&self) -> usize {
size_of::<Self>()
+ self.row_converter.size()
+ self.partition_converter.size()
+ self.scratch_rows.size()
+ self.heaps.values().map(|h| h.size()).sum::<usize>()
+ self.heaps.capacity() * (size_of::<OwnedRow>() + size_of::<TopKHeap>())
}
}
#[derive(Debug)]
struct TieEntry {
batch: RecordBatch,
row_indices: Vec<u32>,
batch_bytes: usize,
}
struct RankPartitionState {
heap: TopKHeap,
ties: Vec<TieEntry>,
}
impl RankPartitionState {
fn size(&self) -> usize {
let ties_buffer = self.ties.capacity() * size_of::<TieEntry>();
let ties_contents: usize = self
.ties
.iter()
.map(|t| t.row_indices.capacity() * size_of::<u32>() + t.batch_bytes)
.sum();
self.heap.size() + ties_buffer + ties_contents
}
}
pub(crate) struct PartitionedTopKRank {
schema: SchemaRef,
metrics: TopKMetrics,
reservation: MemoryReservation,
expr: LexOrdering,
row_converter: RowConverter,
scratch_rows: Rows,
partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
partition_converter: RowConverter,
partition_scratch_rows: Rows,
states: HashMap<OwnedRow, RankPartitionState>,
k: usize,
batch_size: usize,
}
impl PartitionedTopKRank {
#[expect(clippy::too_many_arguments)]
pub(crate) fn try_new(
partition_id: usize,
schema: SchemaRef,
partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
partition_sort_fields: Vec<SortField>,
order_expr: LexOrdering,
k: usize,
batch_size: usize,
runtime: &Arc<RuntimeEnv>,
metrics: &ExecutionPlanMetricsSet,
) -> Result<Self> {
assert!(k > 0, "PartitionedTopKRank requires k > 0");
let reservation =
MemoryConsumer::new(format!("PartitionedTopKRank[{partition_id}]"))
.register(&runtime.memory_pool);
let order_sort_fields = build_sort_fields(&order_expr, &schema)?;
let row_converter = RowConverter::new(order_sort_fields)?;
let scratch_rows =
row_converter.empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size);
let partition_converter = RowConverter::new(partition_sort_fields)?;
let partition_scratch_rows = partition_converter
.empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size);
Ok(Self {
schema,
metrics: TopKMetrics::new(metrics, partition_id),
reservation,
expr: order_expr,
row_converter,
scratch_rows,
partition_exprs,
partition_converter,
partition_scratch_rows,
states: HashMap::new(),
k,
batch_size,
})
}
pub(crate) fn insert_batch(&mut self, batch: &RecordBatch) -> Result<()> {
let baseline = self.metrics.baseline.clone();
let _timer = baseline.elapsed_compute().timer();
let num_rows = batch.num_rows();
if num_rows == 0 {
return Ok(());
}
let input_batch_bytes = get_record_batch_memory_size(batch);
let pk_arrays: Vec<ArrayRef> = self
.partition_exprs
.iter()
.map(|e| e.evaluate(batch).and_then(|v| v.into_array(num_rows)))
.collect::<Result<_>>()?;
self.partition_scratch_rows.clear();
self.partition_converter
.append(&mut self.partition_scratch_rows, &pk_arrays)?;
let pk_rows = &self.partition_scratch_rows;
let mut groups: HashMap<OwnedRow, Vec<u32>> = HashMap::new();
for i in 0..num_rows {
groups
.entry(pk_rows.row(i).owned())
.or_default()
.push(i as u32);
}
let ob_arrays: Vec<ArrayRef> = self
.expr
.iter()
.map(|e| e.expr.evaluate(batch).and_then(|v| v.into_array(num_rows)))
.collect::<Result<_>>()?;
self.scratch_rows.clear();
self.row_converter
.append(&mut self.scratch_rows, &ob_arrays)?;
let k = self.k;
let mut replacements: usize = 0;
for (pk, indices) in groups {
let state = self.states.entry(pk).or_insert_with(|| RankPartitionState {
heap: TopKHeap::new(k),
ties: Vec::new(),
});
let mut equal_indices: Vec<u32> = Vec::new();
let mut entry: Option<RecordBatchEntry> = None;
for &orig_idx in &indices {
let row = self.scratch_rows.row(orig_idx as usize);
let classification = state
.heap
.max()
.map(|max_row| row.as_ref().cmp(max_row.row()));
match classification {
Some(Ordering::Equal) => {
equal_indices.push(orig_idx);
continue;
}
Some(Ordering::Greater) => continue,
Some(Ordering::Less) | None => {
let entry_ref = entry.get_or_insert_with(|| {
state.heap.register_batch(batch.clone())
});
if let Some(EvictedRow {
batch: evicted_batch,
index: evicted_index,
row_bytes: evicted_bytes,
}) = state.heap.add(entry_ref, row, orig_idx as usize)
{
let boundary_changed = state
.heap
.max()
.expect("heap was full to evict; must still be full")
.row()
!= evicted_bytes.as_slice();
if boundary_changed {
state.ties.clear();
equal_indices.clear();
} else {
let batch_bytes =
get_record_batch_memory_size(&evicted_batch);
state.ties.push(TieEntry {
batch: evicted_batch,
row_indices: vec![evicted_index as u32],
batch_bytes,
});
}
}
replacements += 1;
}
}
}
if let Some(e) = entry {
state.heap.insert_batch_entry(e);
state.heap.maybe_compact()?;
}
if !equal_indices.is_empty() {
state.ties.push(TieEntry {
batch: batch.clone(),
row_indices: equal_indices,
batch_bytes: input_batch_bytes,
});
}
}
if replacements > 0 {
self.metrics.row_replacements.add(replacements);
}
self.reservation.try_resize(self.size())?;
Ok(())
}
pub(crate) fn emit(self) -> Result<SendableRecordBatchStream> {
let Self {
schema,
metrics,
reservation: _,
expr: _,
row_converter: _,
scratch_rows: _,
partition_exprs: _,
partition_converter: _,
partition_scratch_rows: _,
mut states,
k: _,
batch_size,
} = self;
let _timer = metrics.baseline.elapsed_compute().timer();
let mut sorted_pks: Vec<OwnedRow> = states.keys().cloned().collect();
sorted_pks.sort();
let mut coalescer = BatchCoalescer::new(Arc::clone(&schema), batch_size);
for pk in sorted_pks {
let RankPartitionState { mut heap, ties, .. } =
states.remove(&pk).expect("key from states.keys()");
if let Some(batch) = heap.emit()? {
(&batch).record_output(&metrics.baseline);
coalescer.push_batch(batch)?;
}
for tie in ties {
let indices = UInt32Array::from(tie.row_indices);
let tie_batch = take_record_batch(&tie.batch, &indices)?;
(&tie_batch).record_output(&metrics.baseline);
coalescer.push_batch(tie_batch)?;
}
}
coalescer.finish_buffered_batch()?;
let mut out: Vec<Result<RecordBatch>> = Vec::new();
while let Some(b) = coalescer.next_completed_batch() {
out.push(Ok(b));
}
Ok(Box::pin(RecordBatchStreamAdapter::new(
schema,
futures::stream::iter(out),
)))
}
fn size(&self) -> usize {
size_of::<Self>()
+ self.row_converter.size()
+ self.partition_converter.size()
+ self.scratch_rows.size()
+ self.partition_scratch_rows.size()
+ self.states.values().map(|s| s.size()).sum::<usize>()
+ self.states.capacity()
* (size_of::<OwnedRow>() + size_of::<RankPartitionState>())
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{BooleanArray, Float64Array, Int32Array};
use arrow::datatypes::{DataType, Field, Schema};
use arrow_schema::SortOptions;
use datafusion_common::assert_batches_eq;
use datafusion_physical_expr::{DynamicFilterTracking, expressions::col};
use futures::TryStreamExt;
#[test]
fn test_record_batch_store_size() {
let schema = Arc::new(Schema::new(vec![
Field::new("ints", DataType::Int32, true),
Field::new("float64", DataType::Float64, false),
]));
let mut record_batch_store = RecordBatchStore::new();
let int_array =
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 {
id: 0,
batch: RecordBatch::try_new(
schema,
vec![Arc::new(int_array), Arc::new(float64_array)],
)
.unwrap(),
uses: 1,
};
record_batch_store.insert(record_batch_entry);
assert_eq!(record_batch_store.batches_size, 60);
record_batch_store.unuse(0);
assert_eq!(record_batch_store.batches_size, 0);
}
fn make_ab_schema() -> SchemaRef {
make_ab_schema_with_nullable_a(false)
}
fn make_ab_schema_with_nullable_a(a_nullable: bool) -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, a_nullable),
Field::new("b", DataType::Float64, false),
]))
}
fn make_topk_filter() -> Arc<RwLock<TopKDynamicFilters>> {
make_shared_topk_filter(1)
}
fn make_shared_topk_filter(
topk_emitter_count: usize,
) -> Arc<RwLock<TopKDynamicFilters>> {
Arc::new(RwLock::new(
TopKDynamicFilters::new_with_topk_emitter_count(
Arc::new(DynamicFilterPhysicalExpr::new(vec![], lit(true))),
topk_emitter_count,
),
))
}
fn make_ab_topk(
schema: SchemaRef,
filter: Arc<RwLock<TopKDynamicFilters>>,
) -> Result<TopK> {
make_ab_topk_with_options(0, schema, filter, SortOptions::default())
}
fn make_ab_topk_with_options(
partition_id: usize,
schema: SchemaRef,
filter: Arc<RwLock<TopKDynamicFilters>>,
a_options: SortOptions,
) -> Result<TopK> {
let sort_expr_a = PhysicalSortExpr {
expr: col("a", schema.as_ref())?,
options: a_options,
};
let sort_expr_b = PhysicalSortExpr {
expr: col("b", schema.as_ref())?,
options: SortOptions::default(),
};
TopK::try_new(
partition_id,
schema,
vec![sort_expr_a.clone()],
LexOrdering::from([sort_expr_a, sort_expr_b]),
3,
2,
Arc::new(RuntimeEnv::default()),
&ExecutionPlanMetricsSet::new(),
filter,
)
}
fn make_ab_batch(
schema: SchemaRef,
a: &[Option<i32>],
b: &[f64],
) -> Result<RecordBatch> {
Ok(RecordBatch::try_new(
schema,
vec![
Arc::new(Int32Array::from(a.to_vec())) as ArrayRef,
Arc::new(Float64Array::from(b.to_vec())) as ArrayRef,
],
)?)
}
type AbRow = (Option<i32>, f64);
fn make_ab_rows_batch(schema: SchemaRef, rows: &[AbRow]) -> Result<RecordBatch> {
let (a, b): (Vec<_>, Vec<_>) = rows.iter().copied().unzip();
make_ab_batch(schema, &a, &b)
}
#[tokio::test]
async fn test_early_completion_marks_finished_with_prefix() -> Result<()> {
let schema = make_ab_schema();
let mut topk = make_ab_topk(Arc::clone(&schema), make_topk_filter())?;
topk.insert_batch(make_ab_batch(
Arc::clone(&schema),
&[Some(1), Some(1), Some(2)],
&[20.0, 15.0, 30.0],
)?)?;
assert!(!topk.finished);
topk.insert_batch(make_ab_batch(
Arc::clone(&schema),
&[Some(2), Some(3)],
&[10.0, 20.0],
)?)?;
assert!(topk.finished);
let results: Vec<_> = topk.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+---+------+",
"| a | b |",
"+---+------+",
"| 1 | 15.0 |",
"| 1 | 20.0 |",
"| 2 | 10.0 |",
"+---+------+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_early_completion_fires_when_filter_rejects_entire_batch() -> Result<()>
{
let schema = make_ab_schema();
let mut topk = make_ab_topk(Arc::clone(&schema), make_topk_filter())?;
topk.insert_batch(make_ab_batch(
Arc::clone(&schema),
&[Some(1), Some(1), Some(2)],
&[20.0, 15.0, 30.0],
)?)?;
assert!(!topk.finished);
topk.insert_batch(make_ab_batch(
Arc::clone(&schema),
&[Some(3), Some(3)],
&[10.0, 20.0],
)?)?;
assert!(topk.finished);
let results: Vec<_> = topk.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+---+------+",
"| a | b |",
"+---+------+",
"| 1 | 15.0 |",
"| 1 | 20.0 |",
"| 2 | 30.0 |",
"+---+------+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_early_completion_fires_when_batch_makes_no_replacements() -> Result<()>
{
let schema = make_ab_schema();
let filter = make_topk_filter();
let mut topk = make_ab_topk(Arc::clone(&schema), Arc::clone(&filter))?;
topk.insert_batch(make_ab_batch(
Arc::clone(&schema),
&[Some(1), Some(1), Some(2)],
&[20.0, 15.0, 30.0],
)?)?;
assert!(!topk.finished);
let replacements_before = topk.metrics.row_replacements.value();
filter.read().expr().update(lit(true))?;
topk.insert_batch(make_ab_batch(
Arc::clone(&schema),
&[Some(3), Some(3)],
&[10.0, 20.0],
)?)?;
assert_eq!(topk.metrics.row_replacements.value(), replacements_before);
assert!(topk.finished);
let results: Vec<_> = topk.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+---+------+",
"| a | b |",
"+---+------+",
"| 1 | 15.0 |",
"| 1 | 20.0 |",
"| 2 | 30.0 |",
"+---+------+",
],
&results
);
Ok(())
}
struct SharedPrefixCase {
name: &'static str,
a_nullable: bool,
a_options: SortOptions,
threshold_source_rows: &'static [AbRow],
lagging_partition_rows: &'static [AbRow],
expected_finished: bool,
}
fn assert_shared_prefix_case(case: SharedPrefixCase) -> Result<()> {
let schema = make_ab_schema_with_nullable_a(case.a_nullable);
let filter = make_shared_topk_filter(2);
let mut threshold_source = make_ab_topk_with_options(
0,
Arc::clone(&schema),
Arc::clone(&filter),
case.a_options,
)?;
threshold_source.insert_batch(make_ab_rows_batch(
Arc::clone(&schema),
case.threshold_source_rows,
)?)?;
assert!(
filter
.read()
.shared_threshold
.as_ref()
.and_then(TopKThreshold::common_prefix_row)
.is_some(),
"{}: threshold-source partition should establish the shared prefix threshold",
case.name
);
let mut lagging_partition = make_ab_topk_with_options(
1,
Arc::clone(&schema),
Arc::clone(&filter),
case.a_options,
)?;
lagging_partition
.insert_batch(make_ab_rows_batch(schema, case.lagging_partition_rows)?)?;
assert!(
lagging_partition.heap.inner.is_empty(),
"{}: lagging partition's local heap should remain empty",
case.name
);
assert_eq!(
lagging_partition.finished, case.expected_finished,
"{}",
case.name
);
Ok(())
}
#[test]
fn test_shared_filter_can_finish_partition_before_local_heap_is_full() -> Result<()> {
assert_shared_prefix_case(SharedPrefixCase {
name: "shared threshold should finish lagging partition",
a_nullable: false,
a_options: SortOptions::default(),
threshold_source_rows: &[(Some(1), 20.0), (Some(1), 15.0), (Some(2), 30.0)],
lagging_partition_rows: &[(Some(3), 10.0), (Some(3), 20.0)],
expected_finished: true,
})
}
#[test]
fn test_shared_prefix_threshold_boundary_cases() -> Result<()> {
for case in [
SharedPrefixCase {
name: "equal prefix cannot prove completion",
a_nullable: false,
a_options: SortOptions::default(),
threshold_source_rows: &[
(Some(1), 20.0),
(Some(1), 15.0),
(Some(2), 30.0),
],
lagging_partition_rows: &[(Some(2), 40.0), (Some(2), 50.0)],
expected_finished: false,
},
SharedPrefixCase {
name: "descending prefix uses sort-order row encoding",
a_nullable: false,
a_options: SortOptions {
descending: true,
nulls_first: true,
},
threshold_source_rows: &[
(Some(10), 1.0),
(Some(10), 2.0),
(Some(9), 3.0),
],
lagging_partition_rows: &[(Some(8), 1.0), (Some(8), 2.0)],
expected_finished: true,
},
SharedPrefixCase {
name: "NULLS LAST prefix uses sort-order row encoding",
a_nullable: true,
a_options: SortOptions {
descending: false,
nulls_first: false,
},
threshold_source_rows: &[
(Some(1), 20.0),
(Some(1), 15.0),
(Some(2), 30.0),
],
lagging_partition_rows: &[(None, 10.0), (None, 20.0)],
expected_finished: true,
},
] {
assert_shared_prefix_case(case)?;
}
Ok(())
}
fn make_single_column_topk(
dynamic_filter: Arc<DynamicFilterPhysicalExpr>,
) -> Result<(SchemaRef, TopK)> {
make_single_column_topk_with_filter(
0,
Arc::new(RwLock::new(TopKDynamicFilters::new(dynamic_filter))),
)
}
fn make_single_column_topk_with_filter(
partition_id: usize,
filter: Arc<RwLock<TopKDynamicFilters>>,
) -> Result<(SchemaRef, TopK)> {
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
let sort_expr = PhysicalSortExpr {
expr: col("a", schema.as_ref())?,
options: SortOptions::default(),
};
let topk = TopK::try_new(
partition_id,
Arc::clone(&schema),
vec![sort_expr.clone()],
LexOrdering::from([sort_expr]),
2,
10,
Arc::new(RuntimeEnv::default()),
&ExecutionPlanMetricsSet::new(),
filter,
)?;
Ok((schema, topk))
}
#[tokio::test]
async fn test_topk_marks_filter_complete() -> Result<()> {
let dynamic_filter = Arc::new(DynamicFilterPhysicalExpr::new(vec![], lit(true)));
let dynamic_filter_clone = Arc::clone(&dynamic_filter);
let (schema, mut topk) = make_single_column_topk(dynamic_filter)?;
let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(3), Some(1), Some(2)]));
let batch = RecordBatch::try_new(Arc::clone(&schema), vec![array])?;
topk.insert_batch(batch)?;
let _results: Vec<_> = topk.emit()?.try_collect().await?;
tokio::time::timeout(
std::time::Duration::from_secs(1),
dynamic_filter_clone.wait_complete(),
)
.await
.expect("single-emitter TopK should mark the dynamic filter complete");
Ok(())
}
#[tokio::test]
async fn test_shared_topk_filter_completes_after_last_emitter() -> Result<()> {
let dynamic_filter = Arc::new(DynamicFilterPhysicalExpr::new(vec![], lit(true)));
let dynamic_filter_clone = Arc::clone(&dynamic_filter);
let shared_filter = Arc::new(RwLock::new(
TopKDynamicFilters::new_with_topk_emitter_count(dynamic_filter, 2),
));
let (schema, mut topk_0) =
make_single_column_topk_with_filter(0, Arc::clone(&shared_filter))?;
let (_, mut topk_1) =
make_single_column_topk_with_filter(1, Arc::clone(&shared_filter))?;
let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(3), Some(1), Some(2)]));
let batch = RecordBatch::try_new(Arc::clone(&schema), vec![array])?;
topk_0.insert_batch(batch)?;
let _results: Vec<_> = topk_0.emit()?.try_collect().await?;
let dynamic_filter_expr: Arc<dyn PhysicalExpr> =
Arc::<DynamicFilterPhysicalExpr>::clone(&dynamic_filter_clone);
assert!(
matches!(
DynamicFilterTracking::classify(&dynamic_filter_expr),
DynamicFilterTracking::Watching(_)
),
"the shared filter should remain watchable until every TopK emits"
);
let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(6), Some(4), Some(5)]));
let batch = RecordBatch::try_new(schema, vec![array])?;
topk_1.insert_batch(batch)?;
let _results: Vec<_> = topk_1.emit()?.try_collect().await?;
tokio::time::timeout(
std::time::Duration::from_secs(1),
dynamic_filter_clone.wait_complete(),
)
.await
.expect("the final shared TopK emitter should mark the dynamic filter complete");
Ok(())
}
#[tokio::test]
async fn test_topk_memory_compaction() -> Result<()> {
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
let sort_expr = PhysicalSortExpr {
expr: col("a", schema.as_ref())?,
options: SortOptions::default(),
};
let full_expr = LexOrdering::from([sort_expr.clone()]);
let prefix = vec![sort_expr];
let runtime = Arc::new(RuntimeEnv::default());
let metrics = ExecutionPlanMetricsSet::new();
let k = 5;
let mut topk = TopK::try_new(
0,
Arc::clone(&schema),
prefix,
full_expr,
k,
8192,
runtime,
&metrics,
Arc::new(RwLock::new(TopKDynamicFilters::new(Arc::new(
DynamicFilterPhysicalExpr::new(vec![], lit(true)),
)))),
)?;
let large_values: Vec<i32> = (1..=100_000).collect();
let array1: ArrayRef = Arc::new(Int32Array::from(large_values));
let batch1 = RecordBatch::try_new(Arc::clone(&schema), vec![array1])?;
topk.insert_batch(batch1)?;
assert_eq!(
topk.heap.store.len(),
1,
"should have 1 batch before second insert"
);
let array2: ArrayRef = Arc::new(Int32Array::from(vec![-1, 0]));
let batch2 = RecordBatch::try_new(Arc::clone(&schema), vec![array2])?;
let replacements_before = topk.metrics.row_replacements.value();
topk.insert_batch(batch2)?;
assert!(
topk.metrics.row_replacements.value() > replacements_before,
"batch2 must produce replacements so compaction is exercised"
);
assert_eq!(
topk.heap.store.len(),
1,
"store should be compacted to 1 batch"
);
let results: Vec<_> = topk.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+", "| a |", "+----+", "| -1 |", "| 0 |", "| 1 |", "| 2 |",
"| 3 |", "+----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_topk_memory_compaction_skipped_when_marginal() -> Result<()> {
let schema =
Arc::new(Schema::new(vec![Field::new("a", DataType::Boolean, false)]));
let sort_expr = PhysicalSortExpr {
expr: col("a", schema.as_ref())?,
options: SortOptions::default(),
};
let full_expr = LexOrdering::from([sort_expr.clone()]);
let prefix = vec![sort_expr];
let runtime = Arc::new(RuntimeEnv::default());
let metrics = ExecutionPlanMetricsSet::new();
let k = 10;
let mut topk = TopK::try_new(
0,
Arc::clone(&schema),
prefix,
full_expr,
k,
8192,
runtime,
&metrics,
Arc::new(RwLock::new(TopKDynamicFilters::new(Arc::new(
DynamicFilterPhysicalExpr::new(vec![], lit(true)),
)))),
)?;
let batch1 = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(BooleanArray::from(vec![false, false, true, true, true]))
as ArrayRef,
],
)?;
topk.insert_batch(batch1)?;
let batch2 = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(BooleanArray::from(vec![false, false, false, true, true]))
as ArrayRef,
],
)?;
topk.insert_batch(batch2)?;
assert_eq!(
topk.heap.store.len(),
2,
"store must keep 2 batches when savings would be marginal"
);
assert_eq!(topk.heap.inner.len(), 10, "heap should hold all 10 rows");
let results: Vec<_> = topk.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+-------+",
"| a |",
"+-------+",
"| false |",
"| false |",
"| false |",
"| false |",
"| false |",
"| true |",
"| true |",
"| true |",
"| true |",
"| true |",
"+-------+",
],
&results
);
Ok(())
}
fn build_partitioned_topk(k: usize) -> Result<(Arc<Schema>, PartitionedTopK)> {
build_partitioned_topk_with_opts(k, SortOptions::default(), false)
}
fn build_partitioned_topk_with_opts(
k: usize,
val_sort_options: SortOptions,
val_nullable: bool,
) -> Result<(Arc<Schema>, PartitionedTopK)> {
let schema = Arc::new(Schema::new(vec![
Field::new("pk", DataType::Int32, false),
Field::new("val", DataType::Int32, val_nullable),
]));
let pk_expr: Arc<dyn PhysicalExpr> = col("pk", schema.as_ref())?;
let pk_sort_expr = PhysicalSortExpr {
expr: Arc::clone(&pk_expr),
options: SortOptions::default(),
};
let val_sort_expr = PhysicalSortExpr {
expr: col("val", schema.as_ref())?,
options: val_sort_options,
};
let partition_sort_fields = build_sort_fields(&[pk_sort_expr], &schema)?;
let order_expr = LexOrdering::from([val_sort_expr]);
let state = PartitionedTopK::try_new(
0,
Arc::clone(&schema),
vec![pk_expr],
partition_sort_fields,
order_expr,
k,
8, &Arc::new(RuntimeEnv::default()),
&ExecutionPlanMetricsSet::new(),
)?;
Ok((schema, state))
}
fn pk_val_batch(
schema: &Arc<Schema>,
pks: Vec<i32>,
vals: Vec<i32>,
) -> Result<RecordBatch> {
Ok(RecordBatch::try_new(
Arc::clone(schema),
vec![
Arc::new(Int32Array::from(pks)),
Arc::new(Int32Array::from(vals)),
],
)?)
}
fn nullable_pk_val_batch(
schema: &Arc<Schema>,
pks: Vec<i32>,
vals: Vec<Option<i32>>,
) -> Result<RecordBatch> {
Ok(RecordBatch::try_new(
Arc::clone(schema),
vec![
Arc::new(Int32Array::from(pks)),
Arc::new(Int32Array::from(vals)),
],
)?)
}
#[tokio::test]
async fn test_partitioned_topk_multi_partition_within_batch() -> Result<()> {
let (schema, mut state) = build_partitioned_topk(2)?;
let batch =
pk_val_batch(&schema, vec![1, 2, 1, 2, 1, 3], vec![10, 20, 5, 15, 8, 7])?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 5 |",
"| 1 | 8 |",
"| 2 | 15 |",
"| 2 | 20 |",
"| 3 | 7 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_cross_batch_eviction() -> Result<()> {
let (schema, mut state) = build_partitioned_topk(2)?;
state.insert_batch(&pk_val_batch(&schema, vec![1, 1], vec![50, 40])?)?;
state.insert_batch(&pk_val_batch(
&schema,
vec![1, 2, 1],
vec![10, 99, 60], )?)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 10 |",
"| 1 | 40 |",
"| 2 | 99 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_empty_input() -> Result<()> {
let (_schema, state) = build_partitioned_topk(3)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert!(results.is_empty(), "empty input → empty output");
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_fetch_one() -> Result<()> {
let (schema, mut state) = build_partitioned_topk(1)?;
state.insert_batch(&pk_val_batch(
&schema,
vec![1, 1, 2, 2, 3],
vec![3, 1, 9, 4, 7],
)?)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 1 |",
"| 2 | 4 |",
"| 3 | 7 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_desc_ordering() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_with_opts(
2,
SortOptions {
descending: true,
nulls_first: false,
},
false,
)?;
let batch = pk_val_batch(
&schema,
vec![1, 2, 1, 2, 1, 1, 2],
vec![10, 20, 5, 15, 8, 12, 25],
)?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 12 |",
"| 1 | 10 |",
"| 2 | 25 |",
"| 2 | 20 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_nulls_last_ordering() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_with_opts(
1,
SortOptions {
descending: false,
nulls_first: false,
},
true,
)?;
let batch = nullable_pk_val_batch(
&schema,
vec![1, 2, 1, 1, 3, 3, 3],
vec![None, None, Some(7), None, None, Some(4), Some(2)],
)?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 7 |",
"| 2 | |",
"| 3 | 2 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_nulls_first_ordering() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_with_opts(
2,
SortOptions {
descending: false,
nulls_first: true,
},
true,
)?;
let batch = nullable_pk_val_batch(
&schema,
vec![1, 2, 1, 3, 1, 2, 1, 3],
vec![
None,
Some(7),
Some(5),
Some(3),
None,
None,
Some(8),
Some(1),
],
)?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | |",
"| 1 | |",
"| 2 | |",
"| 2 | 7 |",
"| 3 | 1 |",
"| 3 | 3 |",
"+----+-----+",
],
&results
);
Ok(())
}
fn build_partitioned_topk_rank(
k: usize,
) -> Result<(Arc<Schema>, PartitionedTopKRank)> {
build_partitioned_topk_rank_with_opts(k, SortOptions::default(), false)
}
fn build_partitioned_topk_rank_with_opts(
k: usize,
val_sort_options: SortOptions,
val_nullable: bool,
) -> Result<(Arc<Schema>, PartitionedTopKRank)> {
let schema = Arc::new(Schema::new(vec![
Field::new("pk", DataType::Int32, false),
Field::new("val", DataType::Int32, val_nullable),
]));
let pk_expr: Arc<dyn PhysicalExpr> = col("pk", schema.as_ref())?;
let pk_sort_expr = PhysicalSortExpr {
expr: Arc::clone(&pk_expr),
options: SortOptions::default(),
};
let val_sort_expr = PhysicalSortExpr {
expr: col("val", schema.as_ref())?,
options: val_sort_options,
};
let partition_sort_fields = build_sort_fields(&[pk_sort_expr], &schema)?;
let order_expr = LexOrdering::from([val_sort_expr]);
let state = PartitionedTopKRank::try_new(
0,
Arc::clone(&schema),
vec![pk_expr],
partition_sort_fields,
order_expr,
k,
8, &Arc::new(RuntimeEnv::default()),
&ExecutionPlanMetricsSet::new(),
)?;
Ok((schema, state))
}
#[tokio::test]
async fn test_partitioned_topk_rank_multi_partition_within_batch() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_rank(2)?;
let batch =
pk_val_batch(&schema, vec![1, 2, 1, 2, 1, 3], vec![10, 20, 5, 15, 8, 7])?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 5 |",
"| 1 | 8 |",
"| 2 | 15 |",
"| 2 | 20 |",
"| 3 | 7 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_rank_cross_batch_eviction() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_rank(2)?;
state.insert_batch(&pk_val_batch(&schema, vec![1, 1], vec![50, 40])?)?;
state.insert_batch(&pk_val_batch(&schema, vec![1, 2, 1], vec![10, 99, 60])?)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 10 |",
"| 1 | 40 |",
"| 2 | 99 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_rank_empty_input() -> Result<()> {
let (_schema, state) = build_partitioned_topk_rank(3)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert!(results.is_empty(), "empty input → empty output");
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_rank_fetch_one() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_rank(1)?;
state.insert_batch(&pk_val_batch(
&schema,
vec![1, 1, 2, 2, 3],
vec![3, 1, 9, 4, 7],
)?)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 1 |",
"| 2 | 4 |",
"| 3 | 7 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_rank_desc_ordering() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_rank_with_opts(
2,
SortOptions {
descending: true,
nulls_first: false,
},
false,
)?;
let batch = pk_val_batch(
&schema,
vec![1, 2, 1, 2, 1, 1, 2],
vec![10, 20, 5, 15, 8, 12, 25],
)?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 12 |",
"| 1 | 10 |",
"| 2 | 25 |",
"| 2 | 20 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_rank_nulls_last_ordering() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_rank_with_opts(
1,
SortOptions {
descending: false,
nulls_first: false,
},
true,
)?;
let batch = nullable_pk_val_batch(
&schema,
vec![1, 2, 1, 1, 3, 3, 3],
vec![None, None, Some(7), None, None, Some(4), Some(2)],
)?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 7 |",
"| 2 | |",
"| 3 | 2 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_rank_nulls_first_ordering() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_rank_with_opts(
2,
SortOptions {
descending: false,
nulls_first: true,
},
true,
)?;
let batch = nullable_pk_val_batch(
&schema,
vec![1, 2, 1, 3, 1, 2, 1, 3],
vec![
None,
Some(7),
Some(5),
Some(3),
None,
None,
Some(8),
Some(1),
],
)?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | |",
"| 1 | |",
"| 2 | |",
"| 2 | 7 |",
"| 3 | 1 |",
"| 3 | 3 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_rank_boundary_ties_retained() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_rank(2)?;
let batch = pk_val_batch(&schema, vec![1, 1, 1, 1], vec![5, 5, 10, 5])?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 5 |",
"| 1 | 5 |",
"| 1 | 5 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_rank_boundary_shifts_clears_ties() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_rank(2)?;
let batch = pk_val_batch(&schema, vec![1, 1, 1, 1, 1], vec![10, 10, 10, 5, 3])?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 3 |",
"| 1 | 5 |",
"+----+-----+",
],
&results
);
Ok(())
}
#[tokio::test]
async fn test_partitioned_topk_rank_eviction_at_unchanged_boundary() -> Result<()> {
let (schema, mut state) = build_partitioned_topk_rank(2)?;
let batch = pk_val_batch(&schema, vec![1, 1, 1], vec![10, 10, 5])?;
state.insert_batch(&batch)?;
let results: Vec<_> = state.emit()?.try_collect().await?;
assert_batches_eq!(
&[
"+----+-----+",
"| pk | val |",
"+----+-----+",
"| 1 | 5 |",
"| 1 | 10 |",
"| 1 | 10 |",
"+----+-----+",
],
&results
);
Ok(())
}
}