Skip to main content

datafusion_physical_plan/topk/
mod.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! TopK: Combination of Sort / LIMIT
19
20use 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
56/// TopK
57///
58/// # Background
59///
60/// "Top K" is a common query optimization used for queries such as
61/// "find the top 3 customers by revenue". The (simplified) SQL for
62/// such a query might be:
63///
64/// ```sql
65/// SELECT customer_id, revenue FROM 'sales.csv' ORDER BY revenue DESC limit 3;
66/// ```
67///
68/// The simple plan would be:
69///
70/// ```sql
71/// > explain SELECT customer_id, revenue FROM sales ORDER BY revenue DESC limit 3;
72/// +--------------+----------------------------------------+
73/// | plan_type    | plan                                   |
74/// +--------------+----------------------------------------+
75/// | logical_plan | Limit: 3                               |
76/// |              |   Sort: revenue DESC NULLS FIRST       |
77/// |              |     Projection: customer_id, revenue   |
78/// |              |       TableScan: sales                 |
79/// +--------------+----------------------------------------+
80/// ```
81///
82/// While this plan produces the correct answer, it will fully sorts the
83/// input before discarding everything other than the top 3 elements.
84///
85/// The same answer can be produced by simply keeping track of the top
86/// K=3 elements, reducing the total amount of required buffer memory.
87///
88/// # Partial Sort Optimization
89///
90/// This implementation additionally optimizes queries where the input is already
91/// partially sorted by a common prefix of the requested ordering. If subsequent
92/// rows are guaranteed to be strictly greater (in sort order) than a known TopK
93/// boundary on this prefix, the operator safely terminates early.
94///
95/// For a local TopK, that boundary comes from the local heap once it has K rows.
96/// For a partitioned `SortExec`, a shared dynamic-filter threshold can provide
97/// the same prefix boundary before a lagging partition has filled its local heap.
98///
99/// ## Example
100///
101/// For input sorted by `(day DESC)`, but not by `timestamp`, a query such as:
102///
103/// ```sql
104/// SELECT day, timestamp FROM sensor ORDER BY day DESC, timestamp DESC LIMIT 10;
105/// ```
106///
107/// can terminate scanning early once sufficient rows from the latest days have been
108/// collected, skipping older data.
109///
110/// # Structure
111///
112/// This operator tracks the top K items using a `TopKHeap`.
113pub struct TopK {
114    /// schema of the output (and the input)
115    schema: SchemaRef,
116    /// Runtime metrics
117    metrics: TopKMetrics,
118    /// Reservation
119    reservation: MemoryReservation,
120    /// The target number of rows for output batches
121    batch_size: usize,
122    /// sort expressions
123    expr: LexOrdering,
124    /// row converter, for sort keys
125    row_converter: RowConverter,
126    /// scratch space for converting rows
127    scratch_rows: Rows,
128    /// stores the top k values and their sort key values, in order
129    heap: TopKHeap,
130    /// row converter, for common keys between the sort keys and the input ordering
131    common_sort_prefix_converter: Option<RowConverter>,
132    /// Common sort prefix between the input and the sort expressions to allow early exit optimization
133    common_sort_prefix: Arc<[PhysicalSortExpr]>,
134    /// Filter matching the state of the `TopK` heap used for dynamic filter pushdown
135    filter: Arc<RwLock<TopKDynamicFilters>>,
136    /// If true, indicates that all rows of subsequent batches are guaranteed
137    /// to be greater (by byte order, after row conversion) than the top K,
138    /// which means the top K won't change and the computation can be finished early.
139    pub(crate) finished: bool,
140}
141
142/// For more background, please also see the [Dynamic Filters: Passing Information Between Operators During Execution for 25x Faster Queries blog]
143///
144/// [Dynamic Filters: Passing Information Between Operators During Execution for 25x Faster Queries blog]: https://datafusion.apache.org/blog/2025/09/10/dynamic-filters
145#[derive(Debug)]
146pub struct TopKDynamicFilters {
147    /// The current threshold shared by all TopK emitters that use this dynamic
148    /// filter. Any emitter may tighten it.
149    ///
150    /// The full sort-key row and common-prefix row are stored together so they
151    /// always describe the same heap row.
152    shared_threshold: Option<TopKThreshold>,
153    /// The expression used to evaluate the dynamic filter
154    /// Only updated when lock held for the duration of the update
155    expr: Arc<DynamicFilterPhysicalExpr>,
156    /// Number of local TopK emitters that have not called `emit` yet.
157    ///
158    /// A partition-preserving `SortExec` creates one local TopK per output
159    /// partition. The shared dynamic filter is complete only after every local
160    /// TopK has emitted.
161    ///
162    /// `emit` only needs a read guard on the shared filter wrapper, so
163    /// concurrent emitters use this atomic counter instead of taking an
164    /// exclusive lock just to mark their partition done.
165    remaining_topk_emitters: AtomicUsize,
166}
167
168#[derive(Debug, Clone)]
169struct TopKThreshold {
170    /// The full sort-key row bytes for efficient comparison.
171    full_sort_key_row: Vec<u8>,
172    /// The same heap row encoded with the common-prefix converter, when the
173    /// input ordering shares a prefix with the TopK ordering.
174    ///
175    /// This lets each partition stop from a shared TopK threshold even if its
176    /// local heap has not filled yet.
177    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    /// Create a new `TopKDynamicFilters` with the given expression
265    pub fn new(expr: Arc<DynamicFilterPhysicalExpr>) -> Self {
266        Self::new_with_topk_emitter_count(expr, 1)
267    }
268
269    /// Create a new `TopKDynamicFilters` with the expected number of local
270    /// TopK emitters that share it.
271    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
307// Guesstimate for memory allocation: estimated number of bytes used per row in the RowConverter
308const ESTIMATED_BYTES_PER_ROW: usize = 20;
309
310/// Owned data of a row that was just evicted from a [`TopKHeap`].
311///
312/// Returned by [`TopKHeap::add`] so that callers (e.g. rank-aware
313/// wrappers that retain boundary ties) can decide whether to retain
314/// the evicted row externally. The underlying batch is captured
315/// before the heap's internal `RecordBatchStore` decrements the
316/// batch's use count, so the data remains accessible even if the
317/// heap drops its internal reference to the batch.
318#[derive(Debug, Clone)]
319pub(crate) struct EvictedRow {
320    /// The record batch the evicted row came from.
321    pub batch: RecordBatch,
322    /// Row index within `batch`.
323    pub index: usize,
324    /// Encoded ORDER BY tuple for the evicted row, in [`arrow::row`] format.
325    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    /// Create a new [`TopK`] that stores the top `k` values, as
345    /// defined by the sort expressions in `expr`.
346    // TODO: make a builder or some other nicer API
347    #[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        // TODO there is potential to add special cases for single column sort fields
366        // to improve performance
367        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    /// Insert `batch`, remembering if any of its values are among
395    /// the top k seen so far.
396    #[expect(clippy::needless_pass_by_value)]
397    pub fn insert_batch(&mut self, batch: RecordBatch) -> Result<()> {
398        // Updates on drop
399        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        // If a filter is provided, update it with the new rows
414        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            // The heap is unchanged, but a fully rejected batch can still prove
421            // that the shared sort prefix has passed the heap boundary.
422            self.attempt_early_completion(&batch)?;
423            return Ok(());
424        }
425        // only update the keys / rows if the filter does not match all rows
426        if filter.null_count() > 0 || filter.has_false() {
427            // Indices in `set_indices` should be correct if filter contains nulls
428            // So we prepare the filter here. Note this is also done in the `FilterBuilder`
429            // so there is no overhead to do this here.
430            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                // Optimize filter when it has multiple sort keys
437                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        // reuse existing `Rows` to avoid reallocations
448        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            // conserve memory
467            self.heap.maybe_compact()?;
468
469            // update memory reservation
470            self.reservation.try_resize(self.size())?;
471
472            // flag the topK as finished if we know that all
473            // subsequent batches are guaranteed to be greater (by byte order, after row conversion) than the top K,
474            // which means the top K won't change and the computation can be finished early.
475            self.attempt_early_completion(&batch)?;
476
477            // update the filter representation of our TopK heap
478            self.update_filter()?;
479        } else {
480            // The heap did not change, but this batch's prefix may still prove
481            // that no later rows can enter the TopK.
482            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                // heap has k items, and the new row is greater than the
498                // current max in the heap ==> it is not a new topk
499                Some(max_row) if row.as_ref() >= max_row.row() => {}
500                // don't yet have k items or new item is lower than the currently k low values
501                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    /// Update the filter representation of our TopK heap.
533    /// For example, given the sort expression `ORDER BY a DESC, b ASC LIMIT 3`,
534    /// and the current heap values `[(1, 5), (1, 4), (2, 3)]`,
535    /// the filter will be updated to:
536    ///
537    /// ```sql
538    /// (a > 1 OR (a = 1 AND b < 5)) AND
539    /// (a > 1 OR (a = 1 AND b < 4)) AND
540    /// (a > 2 OR (a = 2 AND b < 3))
541    /// ```
542    fn update_filter(&mut self) -> Result<()> {
543        // If the heap doesn't have k elements yet, we can't create thresholds
544        let Some(boundary_row) = self.current_heap_boundary_row() else {
545            return Ok(());
546        };
547
548        // Fast path: check if the current value in topk is better than what is
549        // currently set in the filter with a read only lock
550        let needs_update = {
551            let filter = self.filter.read();
552            boundary_row.is_more_selective_than(filter.shared_threshold.as_ref())
553        };
554
555        // exit early if the current values are better
556        if !needs_update {
557            return Ok(());
558        }
559
560        let boundary = self.heap_boundary(boundary_row.row)?;
561
562        // Extract scalar values BEFORE acquiring lock to reduce critical section
563        let thresholds = boundary.threshold_values(&self.expr)?;
564
565        // Build the filter expression OUTSIDE any synchronization
566        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        // update the threshold. Since there was a lock gap, we must check if it is still the best
571        // may have changed while we were building the expression without the lock
572        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            // some other thread updated the threshold to a better one while we
580            // were building so there is no need to update the filter
581            return Ok(());
582        }
583        filter.shared_threshold = Some(new_threshold);
584
585        // Update the filter expression
586        if let Some(pred) = predicate
587            && !pred.eq(&lit(true))
588        {
589            filter.expr.update(pred)?;
590        }
591
592        Ok(())
593    }
594
595    /// Build the filter expression with the given thresholds.
596    /// This is now called outside of any locks to reduce critical section time.
597    fn build_filter_expression(
598        sort_exprs: &[PhysicalSortExpr],
599        thresholds: &[ScalarValue],
600    ) -> Result<Option<Arc<dyn PhysicalExpr>>> {
601        // Create filter expressions for each threshold
602        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            // Create the appropriate operator based on sort order
608            let op = if sort_expr.options.descending {
609                // For descending sort, we want col > threshold (exclude smaller values)
610                Operator::Gt
611            } else {
612                // For ascending sort, we want col < threshold (exclude larger values)
613                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                // For nulls first, transform to (threshold.value is not null) and (threshold.expr is null or comparison)
626                (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                // For nulls last, transform to (threshold.value is null and threshold.expr is not null)
633                // or (threshold.value is not null and comparison)
634                (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            // For a query like order by a, b, the filter for column `b` is only applied if
653            // the condition a = threshold.value (considering null equality) is met.
654            // Therefore, we add equality predicates for all preceding fields to the filter logic of the current field,
655            // and include the current field's equality predicate in `prev_sort_expr` for use with subsequent fields.
656            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    /// If input ordering shares a common sort prefix with the TopK,
682    /// check if the computation can be finished early.
683    ///
684    /// This is the case if the last row of the current batch is strictly
685    /// greater than either the shared dynamic-filter threshold prefix or the max
686    /// row in the local heap, comparing only on the shared prefix columns.
687    fn attempt_early_completion(&mut self, batch: &RecordBatch) -> Result<()> {
688        // Early exit if the batch is empty as there is no last row to extract from it.
689        if batch.num_rows() == 0 {
690            return Ok(());
691        }
692
693        // common_prefix_row_converter is only `Some` if the input ordering has a common prefix with the TopK,
694        // so early exit if it is `None`.
695        let Some(prefix_converter) = &self.common_sort_prefix_converter else {
696            return Ok(());
697        };
698
699        // Evaluate the prefix for the last row of the current batch.
700        let last_row_idx = batch.num_rows() - 1;
701        let mut batch_prefix_scratch =
702            prefix_converter.empty_rows(1, ESTIMATED_BYTES_PER_ROW); // 1 row with capacity ESTIMATED_BYTES_PER_ROW
703
704        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        // Early exit only from the local heap once it has a full boundary row.
727        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    /// Returns the top k results broken into `batch_size` [`RecordBatch`]es, consuming the heap
789    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(); // time updated on drop
805
806        // Mark this local TopK as emitted. For shared filters, the final
807        // local emitter marks the dynamic filter complete.
808        filter.read().mark_topk_emitted();
809
810        // break into record batches as needed
811        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    /// return the size of memory used by this operator, in bytes
833    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    /// metrics
843    pub baseline: BaselineMetrics,
844
845    /// count of how many rows were replaced in the heap
846    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
860/// This structure keeps at most the *smallest* k items, using the
861/// [arrow::row] format for sort keys. While it is called "topK" for
862/// values like `1, 2, 3, 4, 5` the "top 3" really means the
863/// *smallest* 3 , `1, 2, 3`, not the *largest* 3 `3, 4, 5`.
864///
865/// Using the `Row` format handles things such as ascending vs
866/// descending and nulls first vs nulls last.
867struct TopKHeap {
868    /// The maximum number of elements to store in this heap.
869    k: usize,
870    /// Storage for up at most `k` items using a BinaryHeap. Reversed
871    /// so that the smallest k so far is on the top
872    inner: BinaryHeap<TopKRow>,
873    /// Storage the original row values (TopKRow only has the sort key)
874    store: RecordBatchStore,
875    /// The size of all owned data held by this heap
876    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    /// Register a [`RecordBatch`] with the heap, returning the
891    /// appropriate entry
892    pub fn register_batch(&mut self, batch: RecordBatch) -> RecordBatchEntry {
893        self.store.register(batch)
894    }
895
896    /// Insert a [`RecordBatchEntry`] created by a previous call to
897    /// [`Self::register_batch`] into storage.
898    pub fn insert_batch_entry(&mut self, entry: RecordBatchEntry) {
899        self.store.insert(entry)
900    }
901
902    /// Returns the largest value stored by the heap if there are k
903    /// items, otherwise returns None. Remember this structure is
904    /// keeping the "smallest" k values
905    fn max(&self) -> Option<&TopKRow> {
906        if self.inner.len() < self.k {
907            None
908        } else {
909            self.inner.peek()
910        }
911    }
912
913    /// Adds `row` to this heap. If inserting this new item would
914    /// increase the size past `k`, removes the previously smallest
915    /// item.
916    ///
917    /// Returns `Some(EvictedRow)` if an existing row was evicted to
918    /// make room for `row`, or `None` if the row was inserted into a
919    /// non-full heap.
920    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        // Reuse storage for evicted item if possible
933        if self.inner.len() == self.k {
934            let mut prev_min = self.inner.peek_mut().unwrap();
935
936            // Capture evicted row data before `unuse` (which may GC the
937            // batch from the store) and `replace_with` (which overwrites
938            // `prev_min` in place). The batch comes from `self.store` for
939            // cross-batch evictions, or directly from `batch_entry` when
940            // a row evicts another row from the same in-flight batch
941            // (entry not yet registered in the store).
942            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            // Update batch use
957            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            // update memory accounting
964            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            // put the new row into the heap
975            self.inner.push(new_row);
976            None
977        }
978    }
979
980    /// Returns the values stored in this heap, from values low to
981    /// high, as a single [`RecordBatch`], resetting the inner heap
982    pub fn emit(&mut self) -> Result<Option<RecordBatch>> {
983        Ok(self.emit_with_state()?.0)
984    }
985
986    /// Returns the values stored in this heap, from values low to
987    /// high, as a single [`RecordBatch`], and a sorted vec of the
988    /// current heap's contents
989    fn emit_with_state(&mut self) -> Result<(Option<RecordBatch>, Vec<TopKRow>)> {
990        // generate sorted rows
991        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        // Collect the batches into a vec and store the "batch_id -> array_pos" mapping, to then
998        // build the `indices` vec below. This is needed since the batch ids are not continuous.
999        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        // At this point `indices` contains indexes within the
1012        // rows and `input_arrays` contains a reference to the
1013        // relevant RecordBatch for that index. `interleave_record_batch` pulls
1014        // them together into a single new batch
1015        let new_batch = interleave_record_batch(&record_batches, &indices)?;
1016
1017        Ok((Some(new_batch), topk_rows))
1018    }
1019
1020    /// Compact this heap, rewriting all stored batches into a single
1021    /// input batch
1022    pub fn maybe_compact(&mut self) -> Result<()> {
1023        // Don't compact if there's only one batch (compacting into itself is pointless)
1024        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        // Compact when current store memory exceeds 2x what the compacted
1032        // result would need. The multiplier avoids compacting when the
1033        // savings would be marginal.
1034        if total_rows <= num_rows * 2 {
1035            return Ok(());
1036        }
1037
1038        // at first, compact the entire thing always into a new batch
1039        // (maybe we can get fancier in the future about ignoring
1040        // batches that have a high usage ratio already
1041
1042        // Note: new batch is in the same order as inner
1043        let (new_batch, mut topk_rows) = self.emit_with_state()?;
1044        let Some(new_batch) = new_batch else {
1045            return Ok(());
1046        };
1047
1048        // clear all old entries in store (this invalidates all
1049        // store_ids in `inner`)
1050        self.store.clear();
1051
1052        let mut batch_entry = self.register_batch(new_batch);
1053        batch_entry.uses = num_rows;
1054
1055        // rewrite all existing entries to use the new batch, and
1056        // remove old entries. The sortedness and their relative
1057        // position do not change
1058        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        // restore the heap
1064        self.inner = BinaryHeap::from(topk_rows);
1065
1066        Ok(())
1067    }
1068
1069    /// return the size of memory used by this heap, in bytes
1070    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/// Represents one of the top K rows held in this heap. Orders
1079/// according to memcmp of row (e.g. the arrow Row format, but could
1080/// also be primitive values)
1081///
1082/// Reuses allocations to minimize runtime overhead of creating new Vecs
1083#[derive(Debug, PartialEq)]
1084struct TopKRow {
1085    /// the value of the sort key for this row. This contains the
1086    /// bytes that could be stored in `OwnedRow` but uses `Vec<u8>` to
1087    /// reuse allocations.
1088    row: Vec<u8>,
1089    /// the RecordBatch this row came from: an id into a [`RecordBatchStore`]
1090    batch_id: u32,
1091    /// the index in this record batch the row came from
1092    index: usize,
1093}
1094
1095impl TopKRow {
1096    /// Create a new TopKRow with new allocation
1097    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    // Replace the existing row capacity with new values
1106    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    /// Returns the number of bytes owned by this row in the heap (not
1115    /// including itself)
1116    fn owned_size(&self) -> usize {
1117        self.row.capacity()
1118    }
1119
1120    /// Returns a slice to the owned row value
1121    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        // TODO PartialOrd is not consistent with PartialEq; PartialOrd contract is violated
1131        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    // for this batch, how many times has it been used
1146    uses: usize,
1147}
1148
1149/// This structure tracks [`RecordBatch`] by an id so that:
1150///
1151/// 1. The baches can be tracked via an id that can be copied cheaply
1152/// 2. The total memory held by all batches is tracked
1153#[derive(Debug)]
1154struct RecordBatchStore {
1155    /// id generator
1156    next_id: u32,
1157    /// storage
1158    batches: HashMap<u32, RecordBatchEntry>,
1159    /// total size of all record batches tracked by this store
1160    batches_size: usize,
1161    /// row count of all the batches
1162    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    /// Register this batch with the store and assign an ID. No
1176    /// attempt is made to compare this batch to other batches
1177    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    /// Insert a record batch entry into this store, tracking its
1184    /// memory use, if it has any uses
1185    pub fn insert(&mut self, entry: RecordBatchEntry) {
1186        // uses of 0 means that none of the rows in the batch were stored in the topk
1187        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    /// Clear all values in this store, invalidating all previous batch ids
1195    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    /// returns the total number of batches stored in this store
1206    fn len(&self) -> usize {
1207        self.batches.len()
1208    }
1209
1210    /// returns true if the store has nothing stored
1211    fn is_empty(&self) -> bool {
1212        self.batches.is_empty()
1213    }
1214
1215    /// remove a use from the specified batch id. If the use count
1216    /// reaches zero the batch entry is removed from the store
1217    ///
1218    /// panics if there were no remaining uses of id
1219    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    /// returns the size of memory used by this store, including all
1242    /// referenced `RecordBatch`es, in bytes
1243    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
1250/// Top-K-per-partition operator state.
1251///
1252/// Sibling to [`TopK`]. Where `TopK` maintains a single global heap,
1253/// `PartitionedTopK` maintains one [`TopKHeap`] per distinct partition
1254/// key while sharing a single [`RowConverter`], [`MemoryReservation`],
1255/// scratch [`Rows`] buffer, and [`TopKMetrics`] across all partitions.
1256///
1257/// This sharing is the point of the type: with N distinct partition
1258/// keys, a naive `HashMap<_, TopK>` pays N × constant overhead for
1259/// `RowConverter::new`, `MemoryConsumer::register`, and metric
1260/// counter setup. `PartitionedTopK` pays it once.
1261pub(crate) struct PartitionedTopK {
1262    schema: SchemaRef,
1263    metrics: TopKMetrics,
1264    reservation: MemoryReservation,
1265    /// ORDER BY expressions (excludes PARTITION BY).
1266    expr: LexOrdering,
1267    /// Encoder for ORDER BY columns. Reused across partitions.
1268    row_converter: RowConverter,
1269    /// Scratch row buffer reused across `insert_batch` calls.
1270    scratch_rows: Rows,
1271    /// PARTITION BY expressions.
1272    partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
1273    /// Encoder for the partition key.
1274    partition_converter: RowConverter,
1275    /// One heap per distinct partition key seen so far.
1276    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    /// Demultiplex `batch` rows by partition key, encode the ORDER BY
1321    /// columns once for the whole batch, and feed each partition's
1322    /// rows into its dedicated [`TopKHeap`].
1323    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        // 1. Evaluate + encode partition columns.
1333        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        // 2. Demultiplex row indices by partition key (per-batch).
1341        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        // 3. Evaluate ORDER BY columns on the full batch and encode ONCE.
1350        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        // 4. Per-partition: take the sub-batch, walk indices, dispatch
1360        //    qualifying rows into the partition's heap.
1361        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            // Once a heap is full, most rows at high partition cardinality
1367            // are rejected. Skip the gather + batch registration entirely
1368            // when nothing in this partition group can improve the heap.
1369            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    /// Drain all heaps in partition-key order and return the rows as
1407    /// a stream of coalesced `RecordBatch`es ordered by
1408    /// `(partition_keys, order_keys)`.
1409    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    /// Total memory currently held by this operator, including all
1451    /// per-partition heaps.
1452    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/// A run of rows from a single source [`RecordBatch`] that tied at the
1463/// boundary when inserted. Stored as `(batch, indices)` and materialized
1464/// at emit time via [`take_record_batch`].
1465#[derive(Debug)]
1466struct TieEntry {
1467    batch: RecordBatch,
1468    /// Indices into `batch` of the rows tied at the (then-current)
1469    /// boundary. Always non-empty by construction.
1470    row_indices: Vec<u32>,
1471    /// `get_record_batch_memory_size(&batch)` captured at push time so
1472    /// `RankPartitionState::size()` doesn't recurse through `batch`'s
1473    /// columns on every `try_resize` call.
1474    batch_bytes: usize,
1475}
1476
1477/// Per-partition state for `RANK()` semantics.
1478///
1479/// Composes [`TopKHeap`] as the K-bounded core plus a sibling
1480/// `Vec<TieEntry>` for boundary-tied rows. `RANK ≤ K` keeps the K
1481/// best rows by ORDER BY plus every row tied at the K-th-best
1482/// ORDER BY value — the boundary. So the total retained rows can
1483/// exceed K when ties straddle the boundary.
1484struct 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
1501/// Sibling to [`PartitionedTopK`] implementing `RANK()` semantics.
1502///
1503/// Per partition, retains the K-best rows plus every row tied at the
1504/// K-th-best ORDER BY value (so `WHERE rk <= K` may keep more than K
1505/// rows when ties straddle the boundary). Like [`PartitionedTopK`],
1506/// the [`RowConverter`], [`MemoryReservation`], scratch [`Rows`]
1507/// buffer, and [`TopKMetrics`] are shared across all partitions for
1508/// this operator instance.
1509///
1510/// # Algorithm (per row)
1511///
1512/// For each incoming row, compare its encoded ORDER BY bytes against
1513/// `heap.max()` — the K-th-best row, which is by definition the
1514/// admission boundary. `heap.max()` is `None` until the heap fills
1515/// to K rows:
1516///
1517/// - heap not full (`max() == None`) → forward to the heap
1518/// - row's ob `==` max → push to ties (no heap call)
1519/// - row's ob `>` max → drop
1520/// - row's ob `<` max → forward to heap; on eviction, compare the
1521///   new `heap.max()` to the evicted row's bytes: if equal, push
1522///   evicted to ties (still tied at the new boundary's rank); else
1523///   clear ties (boundary moved up, old ties no longer satisfy
1524///   `rk ≤ K`)
1525pub(crate) struct PartitionedTopKRank {
1526    schema: SchemaRef,
1527    metrics: TopKMetrics,
1528    reservation: MemoryReservation,
1529    /// ORDER BY expressions (excludes PARTITION BY).
1530    expr: LexOrdering,
1531    /// Encoder for ORDER BY columns. Reused across partitions.
1532    row_converter: RowConverter,
1533    /// Scratch row buffer reused across `insert_batch` calls.
1534    scratch_rows: Rows,
1535    /// PARTITION BY expressions.
1536    partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
1537    /// Encoder for the partition key.
1538    partition_converter: RowConverter,
1539    /// Scratch row buffer for partition-key encoding. Reused across
1540    /// `insert_batch` calls (cleared + appended each batch) so we
1541    /// avoid allocating a fresh `Rows` buffer every batch.
1542    partition_scratch_rows: Rows,
1543    /// One rank state per distinct partition key seen so far.
1544    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    /// Demultiplex `batch` rows by partition key, encode the ORDER BY
1593    /// columns once for the whole batch, and feed each partition's
1594    /// rows through the rank classifier into its dedicated heap and
1595    /// ties Vec.
1596    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        // Captured once so the per-tie push from this batch can reuse
1606        // it (computing `get_record_batch_memory_size` is O(cols ×
1607        // buffer walk) and we'd otherwise pay it per push and again
1608        // per `try_resize` call).
1609        let input_batch_bytes = get_record_batch_memory_size(batch);
1610
1611        // 1. Evaluate + encode partition columns into the reusable
1612        //    scratch (cleared then appended).
1613        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        // 2. Demultiplex row indices by partition key (per-batch).
1624        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        // 3. Evaluate ORDER BY columns on the full batch and encode ONCE.
1633        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        // 4. Per-partition: classify each row and dispatch.
1643        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            // Equal indices for THIS batch only. Coalesced into a single
1653            // tie entry at the end of the partition's loop. Discarded if
1654            // the boundary moves up mid-loop (those rows were tied to the
1655            // old boundary, which is now strictly worse than the new K-th).
1656            let mut equal_indices: Vec<u32> = Vec::new();
1657            // Lazy-registered: only attached if at least one row reaches
1658            // the heap from this batch in this partition.
1659            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                // Classify against the current K-th-best (the heap top).
1665                // `heap.max()` returns `None` while the heap is filling,
1666                // so unclassified rows fall through to the heap path.
1667                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                        // Heap path: heap not yet full, or row strictly
1680                        // better than the current boundary.
1681                        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                            // Compare the new boundary (post-eviction heap
1691                            // top) against the evicted row's bytes — both
1692                            // already in encoded form, no clones needed.
1693                            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                                // Boundary moved up — prior ties (across
1701                                // all prior batches) and equal_indices
1702                                // accumulated earlier in THIS batch were
1703                                // tied to the old boundary, now strictly
1704                                // worse than the new K-th-best. Discard.
1705                                state.ties.clear();
1706                                equal_indices.clear();
1707                            } else {
1708                                // Boundary unchanged — evicted row is tied
1709                                // at the (unchanged) boundary; push as a
1710                                // single-row entry.
1711                                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            // Commit this batch's ties as a single entry.
1731            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    /// Drain all heaps and ties in partition-key order and return the
1748    /// rows as a stream of coalesced [`RecordBatch`]es ordered by
1749    /// `(partition_keys, order_keys)`. Within a partition, heap rows
1750    /// come first (sorted by ob), then tie rows (all sharing the
1751    /// boundary ob).
1752    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    /// Total memory currently held, including all per-partition states.
1802    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    /// This test ensures the size calculation is correct for RecordBatches with multiple columns.
1825    #[test]
1826    fn test_record_batch_store_size() {
1827        // given
1828        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)]); // 5 * 4 = 20
1835        let float64_array = Float64Array::from(vec![1.0, 2.0, 3.0, 4.0, 5.0]); // 5 * 8 = 40
1836
1837        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        // when insert record batch entry
1848        record_batch_store.insert(record_batch_entry);
1849        assert_eq!(record_batch_store.batches_size, 60);
1850
1851        // when unuse record batch entry
1852        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    // Local TopK tests use one emitter; shared-filter cases pass the partition count explicitly.
1868    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    /// Builds the `(a, b)` fixture used by prefix-completion tests:
1884    /// full sort `(a, b)`, input prefix `[a]`, `k = 3`, and batch size 2.
1885    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    /// Regression test for #22849: a batch whose rows are entirely rejected by the
1978    /// heap's dynamic filter must still trigger `attempt_early_completion` when its
1979    /// last row's prefix is worse than the heap's worst.
1980    #[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        // Keep the dynamic filter permissive so the second batch reaches
2034        // `find_new_topk_items`; all of its rows are worse than the heap max,
2035        // so this specifically exercises the `replacements == 0` path.
2036        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    /// Tests that memory-based compaction triggers when a large batch
2282    /// has very few rows referenced by the top-k heap.
2283    #[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        // Insert a large batch (100,000 rows) with values 1..=100_000.
2314        // Only the smallest 5 values (1..=5) will end up in the heap.
2315        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        // After the first batch, store has 1 batch — compaction should
2321        // not trigger (guard: store.len() <= 1).
2322        assert_eq!(
2323            topk.heap.store.len(),
2324            1,
2325            "should have 1 batch before second insert"
2326        );
2327
2328        // Insert a second batch whose values displace entries in the heap.
2329        // -1 and 0 are smaller than the current top-5 (1..=5), so they
2330        // produce 2 replacements. With replacements > 0, `insert_batch`
2331        // calls `insert_batch_entry` (briefly making store.len() == 2)
2332        // and then `maybe_compact`, which should collapse it back to 1.
2333        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        // Sanity check: batch2 was actually integrated. Without
2339        // replacements, `maybe_compact` is never called and the
2340        // store-length assertion below would pass vacuously.
2341        assert!(
2342            topk.metrics.row_replacements.value() > replacements_before,
2343            "batch2 must produce replacements so compaction is exercised"
2344        );
2345
2346        // The compacted-estimate guard is `total_rows <= num_rows * 2`,
2347        // i.e. 100_002 <= 10, which is false, so compaction fires and
2348        // collapses the two stored batches back into one.
2349        assert_eq!(
2350            topk.heap.store.len(),
2351            1,
2352            "store should be compacted to 1 batch"
2353        );
2354
2355        // Verify the emitted results are correct (top 5 ascending).
2356        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    /// Negative path: when stored rows are close to the heap size,
2369    /// compaction must NOT fire even with multiple batches present,
2370    /// because the savings would be marginal
2371    /// (guard: `total_rows <= num_rows * 2`).
2372    ///
2373    /// Uses a bit-packed `BooleanArray` so that future changes to the
2374    /// compaction heuristic that reintroduce a per-byte estimate
2375    /// (where integer truncation could misbehave on sub-byte types)
2376    /// are caught here.
2377    #[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        // Two small batches; every row from both batches ends up referenced
2408        // by the heap, so total_rows == num_rows == 10.
2409        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        // Guard `total_rows <= num_rows * 2` should hold (10 <= 20),
2428        // so compaction is skipped and BOTH batches remain in the store.
2429        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        // Output is still correct (5 falses then 5 trues ascending).
2437        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    /// Builds a `(pk Int32, val Int32)` schema and a `PartitionedTopK`
2462    /// partitioned by `pk` with order `val ASC`. Helper for the
2463    /// `PartitionedTopK` tests below.
2464    fn build_partitioned_topk(k: usize) -> Result<(Arc<Schema>, PartitionedTopK)> {
2465        build_partitioned_topk_with_opts(k, SortOptions::default(), false)
2466    }
2467
2468    /// Variant of [`build_partitioned_topk`] that lets the test pick the
2469    /// `val` column's `SortOptions` (direction, null ordering) and
2470    /// nullability. Used by tests that exercise the shared encoder
2471    /// across `ASC`/`DESC` and `NULLS FIRST/LAST` paths.
2472    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, // batch_size
2503            &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    /// Variant of [`pk_val_batch`] that accepts nullable `val`s. Used by
2524    /// tests that exercise null-ordering through the shared encoder.
2525    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    /// Multiple distinct partition keys interleaved within a single
2540    /// input batch — the per-batch demux, per-partition heap eviction,
2541    /// and partition-key-ordered emit must all behave correctly.
2542    #[tokio::test]
2543    async fn test_partitioned_topk_multi_partition_within_batch() -> Result<()> {
2544        let (schema, mut state) = build_partitioned_topk(2)?;
2545
2546        // pk=1 vals: 10, 5, 8 → top-2 ASC = [5, 8]
2547        // pk=2 vals: 20, 15   → top-2 ASC = [15, 20]
2548        // pk=3 vals: 7        → top-2 ASC = [7]
2549        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    /// State must accumulate across `insert_batch` calls: a partition
2572    /// key seen in batch 1 should still own its heap when batch 2
2573    /// arrives, and a row in batch 2 that beats the existing K-th
2574    /// best should evict the loser.
2575    #[tokio::test]
2576    async fn test_partitioned_topk_cross_batch_eviction() -> Result<()> {
2577        let (schema, mut state) = build_partitioned_topk(2)?;
2578
2579        // Batch 1: pk=1 fills the heap with [50, 40].
2580        state.insert_batch(&pk_val_batch(&schema, vec![1, 1], vec![50, 40])?)?;
2581
2582        // Batch 2: pk=1 sees a smaller value (10) — it must evict 50.
2583        // pk=2 appears for the first time mid-stream.
2584        state.insert_batch(&pk_val_batch(
2585            &schema,
2586            vec![1, 2, 1],
2587            vec![10, 99, 60], // 60 > 40 stays on top, gets discarded
2588        )?)?;
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    /// Empty input must produce an empty output stream, not panic.
2607    #[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    /// `fetch = 1` is a common case (rn = 1 filter). The heap should
2616    /// hold exactly one row per partition: the partition's minimum.
2617    #[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    /// `ORDER BY val DESC` exercises the shared encoder's sort-direction
2643    /// handling: the row converter must flip the sort sign for `val` so
2644    /// that larger values compare smaller in row-encoded form. Each
2645    /// partition should keep its top-K *largest* values.
2646    #[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        // pk=1 vals: 10, 5, 8, 12 → top-2 DESC = [12, 10]
2658        // pk=2 vals: 20, 15, 25   → top-2 DESC = [25, 20]
2659        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    /// NULL sort values exercise the shared encoder's null-ordering
2684    /// handling. With `ASC NULLS LAST`, NULLs sort *after* every
2685    /// non-NULL value, so a partition whose only non-NULL value beats
2686    /// a NULL must evict the NULL when `K = 1`. A partition that holds
2687    /// only NULLs must still emit them.
2688    #[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        // pk=1 vals: NULL, 7, NULL → top-1 ASC NULLS LAST = [7]
2700        // pk=2 vals: NULL          → top-1                 = [NULL]
2701        // pk=3 vals: NULL, 4, 2    → top-1                 = [2]
2702        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    /// `ASC NULLS FIRST` (the `SortOptions::default()`) sorts NULLs
2726    /// *before* every non-NULL value, so under `fetch = K` a partition's
2727    /// NULLs are kept preferentially over larger non-NULL values.
2728    #[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        // pk=1 vals: NULL, 5, NULL, 8 → top-2 ASC NULLS FIRST = [NULL, NULL]
2740        // pk=2 vals: 7, NULL          → top-2                  = [NULL, 7]
2741        // pk=3 vals: 3, 1             → top-2                  = [1, 3]
2742        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    // ====================================================================
2778    // PartitionedTopKRank operator tests
2779    //
2780    // These mirror the PartitionedTopK tests above plus three RANK-specific
2781    // cases for the Equal / boundary-shift / boundary-unchanged-eviction
2782    // arms in `PartitionedTopKRank::insert_batch`.
2783    // ====================================================================
2784
2785    /// Builds a `(pk Int32, val Int32)` schema and a `PartitionedTopKRank`
2786    /// keyed on `pk ASC` (partition) and `val ASC` (ORDER BY).
2787    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    /// Variant of [`build_partitioned_topk_rank`] that lets the test pick
2794    /// the `val` column's `SortOptions` (direction, null ordering) and
2795    /// nullability.
2796    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, // batch_size
2827            &Arc::new(RuntimeEnv::default()),
2828            &ExecutionPlanMetricsSet::new(),
2829        )?;
2830        Ok((schema, state))
2831    }
2832
2833    /// Multiple distinct partition keys interleaved within a single
2834    /// input batch — the per-batch demux, per-partition heap eviction,
2835    /// and partition-key-ordered emit must all behave correctly. No
2836    /// ties: result should match a `ROW_NUMBER` top-K under the same K.
2837    #[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        // pk=1 vals: 10, 5, 8 → top-2 ASC = [5, 8]
2842        // pk=2 vals: 20, 15   → top-2 ASC = [15, 20]
2843        // pk=3 vals: 7        → top-2 ASC = [7]
2844        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    /// State must accumulate across `insert_batch` calls. A row in
2867    /// batch 2 that's strictly better than the existing K-th must
2868    /// evict it; an evicted row whose bytes match the new boundary
2869    /// becomes a `TieEntry` pinned to the prior batch.
2870    #[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        // Batch 1: pk=1 fills the heap with [50, 40].
2875        state.insert_batch(&pk_val_batch(&schema, vec![1, 1], vec![50, 40])?)?;
2876
2877        // Batch 2: pk=1 sees a smaller value (10) — it must evict 50;
2878        // 60 > 40 so it's dropped. pk=2 appears mid-stream.
2879        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    /// Empty input must produce an empty output stream, not panic.
2898    #[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    /// `fetch = 1` is a common case (rk = 1 filter) and exercises the
2907    /// boundary-defined-immediately path: after the first admission per
2908    /// partition, `heap.max()` is `Some`, so every subsequent row goes
2909    /// through full Equal/Greater/Less classification.
2910    #[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    /// `ORDER BY val DESC` exercises the shared encoder's sort-direction
2936    /// handling: the row converter flips the sort sign for `val` so
2937    /// larger values compare smaller in row-encoded form. Each
2938    /// partition keeps its top-K *largest* values.
2939    #[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        // pk=1 vals: 10, 5, 8, 12 → top-2 DESC = [12, 10]
2951        // pk=2 vals: 20, 15, 25   → top-2 DESC = [25, 20]
2952        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    /// NULL sort values exercise the shared encoder's null-ordering
2977    /// handling. With `ASC NULLS LAST`, NULLs sort *after* every
2978    /// non-NULL value, so a partition whose only non-NULL value beats
2979    /// a NULL must evict the NULL when `K = 1`. A partition that holds
2980    /// only NULLs must still emit them.
2981    #[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        // pk=1 vals: NULL, 7, NULL → top-1 ASC NULLS LAST = [7]
2993        // pk=2 vals: NULL          → top-1                 = [NULL]
2994        // pk=3 vals: NULL, 4, 2    → top-1                 = [2]
2995        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    /// `ASC NULLS FIRST` (the `SortOptions::default()`) sorts NULLs
3019    /// *before* every non-NULL value, so under `fetch = K` a partition's
3020    /// NULLs are kept preferentially over larger non-NULL values.
3021    #[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        // pk=1 vals: NULL, 5, NULL, 8 → top-2 ASC NULLS FIRST = [NULL, NULL]
3033        // pk=2 vals: 7, NULL          → top-2                  = [NULL, 7]
3034        // pk=3 vals: 3, 1             → top-2                  = [1, 3]
3035        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    /// RANK-specific: heap fills with K rows tied at the same OB value,
3071    /// then more rows at that same value arrive. They take the Equal arm
3072    /// (heap is full, `heap.max() == row`) and accumulate as ties, while
3073    /// strictly-greater rows are dropped. All retained rows have rank 1.
3074    #[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        // pk=1 vals: 5, 5, 10, 5
3079        //   - first two 5s fill the heap (max=None until heap reaches K=2)
3080        //   - third row 10 > 5 → drop (Greater)
3081        //   - fourth row 5 == 5 → push to ties (Equal)
3082        // Sorted RANKs: 5→1, 5→1, 5→1, 10→4. WHERE rk ≤ 2 keeps the three 5s.
3083        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    /// RANK-specific: heap fills with K rows tied at value V, equal_indices
3103    /// accumulate at V, then a strictly-better row arrives whose admission
3104    /// shifts the boundary strictly below V. The boundary-changed branch
3105    /// must clear both `state.ties` and the in-flight `equal_indices` —
3106    /// otherwise the now-rank-> K rows at value V would leak into output.
3107    #[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        // pk=1 vals: 10, 10, 10, 5, 3
3112        //   - first two 10s fill heap (max=10)
3113        //   - third 10 → Equal → equal_indices=[2]
3114        //   - 5 < 10 → admit, evict 10 → heap={5,10}, max=10 (unchanged).
3115        //       Push evicted to ties: ties=[10@curr_batch[ev_idx]].
3116        //   - 3 < 10 → admit, evict 10 → heap={3,5}, max=5 (CHANGED).
3117        //       Clear ties AND equal_indices.
3118        // Sorted RANKs: 3→1, 5→2, 10→3, 10→3, 10→3. WHERE rk ≤ 2 → [3, 5].
3119        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    /// RANK-specific: heap has multiple rows at boundary value V, then a
3138    /// strictly-better row arrives. The heap evicts one V (popping
3139    /// `prev_min`), but `heap.max()` is still V — boundary unchanged.
3140    /// The evicted V row must be pushed as a `TieEntry`; without that
3141    /// branch a `rk <= K` query would silently lose a tied row.
3142    #[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        // pk=1 vals: 10, 10, 5
3147        //   - first two 10s fill the heap (max=10)
3148        //   - 5 < 10 → admit, evict 10. New heap={5,10}, max=10 (unchanged).
3149        //       Push the evicted 10 to ties.
3150        // Sorted RANKs: 5→1, 10→2, 10→2. WHERE rk ≤ 2 → all 3 rows.
3151        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}