Skip to main content

arrow_select/
coalesce.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//! [`BatchCoalescer`]  concatenates multiple [`RecordBatch`]es after
19//! operations such as [`filter`] and [`take`].
20//!
21//! [`filter`]: crate::filter::filter
22//! [`take`]: crate::take::take
23use crate::filter::{FilterBuilder, FilterPredicate, FilterSelection};
24use crate::take::take_record_batch;
25use arrow_array::types::{BinaryViewType, StringViewType};
26use arrow_array::{Array, ArrayRef, BooleanArray, RecordBatch, downcast_primitive};
27use arrow_schema::{ArrowError, DataType, SchemaRef};
28use std::collections::VecDeque;
29use std::sync::Arc;
30// Originally From DataFusion's coalesce module:
31// https://github.com/apache/datafusion/blob/9d2f04996604e709ee440b65f41e7b882f50b788/datafusion/physical-plan/src/coalesce/mod.rs#L26-L25
32
33mod byte_view;
34mod fixed_size_binary;
35mod generic;
36mod primitive;
37
38use byte_view::InProgressByteViewArray;
39use fixed_size_binary::InProgressFixedSizeBinaryArray;
40use generic::GenericInProgressArray;
41use primitive::InProgressPrimitiveArray;
42
43fn has_sparse_filter_copy(data_type: &DataType) -> bool {
44    data_type.is_primitive() || matches!(data_type, DataType::Utf8View | DataType::BinaryView)
45}
46
47/// Maximum selected row fraction for the fused sparse-filter copy path.
48///
49/// Shared benchmark results show this path helps low-selectivity filters, but
50/// can regress once the filter becomes denser. Keep this as a cheap integer
51/// threshold on the hot path: `selected_count <= filter_len / 16`.
52const SPARSE_FILTER_COPY_MAX_SELECTIVITY_DENOMINATOR: usize = 16;
53
54fn should_use_sparse_filter_copy(filter_len: usize, selected_count: usize) -> bool {
55    selected_count <= filter_len / SPARSE_FILTER_COPY_MAX_SELECTIVITY_DENOMINATOR
56}
57
58/// Concatenate multiple [`RecordBatch`]es
59///
60/// Implements the common pattern of incrementally creating output
61/// [`RecordBatch`]es of a specific size from an input stream of
62/// [`RecordBatch`]es.
63///
64/// This is useful after operations such as [`filter`] and [`take`] that produce
65/// smaller batches, and we want to coalesce them into larger batches for
66/// further processing.
67///
68/// # Motivation
69///
70/// If we use [`concat_batches`] to implement the same functionality, there are 2 potential issues:
71/// 1. At least 2x peak memory (holding the input and output of concat)
72/// 2. 2 copies of the data (to create the output of filter and then create the output of concat)
73///
74/// See: <https://github.com/apache/arrow-rs/issues/6692> for more discussions
75/// about the motivation.
76///
77/// [`filter`]: crate::filter::filter
78/// [`take`]: crate::take::take
79/// [`concat_batches`]: crate::concat::concat_batches
80///
81/// # Example
82/// ```
83/// use arrow_array::record_batch;
84/// use arrow_select::coalesce::{BatchCoalescer};
85/// let batch1 = record_batch!(("a", Int32, [1, 2, 3])).unwrap();
86/// let batch2 = record_batch!(("a", Int32, [4, 5])).unwrap();
87///
88/// // Create a `BatchCoalescer` that will produce batches with at least 4 rows
89/// let target_batch_size = 4;
90/// let mut coalescer = BatchCoalescer::new(batch1.schema(), 4);
91///
92/// // push the batches
93/// coalescer.push_batch(batch1).unwrap();
94/// // only pushed 3 rows (not yet 4, enough to produce a batch)
95/// assert!(coalescer.next_completed_batch().is_none());
96/// coalescer.push_batch(batch2).unwrap();
97/// // now we have 5 rows, so we can produce a batch
98/// let finished = coalescer.next_completed_batch().unwrap();
99/// // 4 rows came out (target batch size is 4)
100/// let expected = record_batch!(("a", Int32, [1, 2, 3, 4])).unwrap();
101/// assert_eq!(finished, expected);
102///
103/// // Have no more input, but still have an in-progress batch
104/// assert!(coalescer.next_completed_batch().is_none());
105/// // We can finish the batch, which will produce the remaining rows
106/// coalescer.finish_buffered_batch().unwrap();
107/// let expected = record_batch!(("a", Int32, [5])).unwrap();
108/// assert_eq!(coalescer.next_completed_batch().unwrap(), expected);
109///
110/// // The coalescer is now empty
111/// assert!(coalescer.next_completed_batch().is_none());
112/// ```
113///
114/// # Background
115///
116/// Generally speaking, larger [`RecordBatch`]es are more efficient to process
117/// than smaller [`RecordBatch`]es (until the CPU cache is exceeded) because
118/// there is fixed processing overhead per batch. This coalescer builds up these
119/// larger batches incrementally.
120///
121/// ```text
122/// ┌────────────────────┐
123/// │    RecordBatch     │
124/// │   num_rows = 100   │
125/// └────────────────────┘                 ┌────────────────────┐
126///                                        │                    │
127/// ┌────────────────────┐     Coalesce    │                    │
128/// │                    │      Batches    │                    │
129/// │    RecordBatch     │                 │                    │
130/// │   num_rows = 200   │  ─ ─ ─ ─ ─ ─ ▶  │                    │
131/// │                    │                 │    RecordBatch     │
132/// │                    │                 │   num_rows = 400   │
133/// └────────────────────┘                 │                    │
134///                                        │                    │
135/// ┌────────────────────┐                 │                    │
136/// │                    │                 │                    │
137/// │    RecordBatch     │                 │                    │
138/// │   num_rows = 100   │                 └────────────────────┘
139/// │                    │
140/// └────────────────────┘
141/// ```
142///
143/// # Notes:
144///
145/// 1. Output rows are produced in the same order as the input rows
146///
147/// 2. The output is a sequence of batches, with all but the last being at exactly
148///    `target_batch_size` rows.
149#[derive(Debug)]
150pub struct BatchCoalescer {
151    /// The input schema
152    schema: SchemaRef,
153    /// The target batch size (and thus size for views allocation). This is a
154    /// hard limit: the output batch will be exactly `target_batch_size`,
155    /// rather than possibly being slightly above.
156    target_batch_size: usize,
157    /// In-progress arrays
158    in_progress_arrays: Vec<Box<dyn InProgressArray>>,
159    /// True if some column still needs the materialized filter path.
160    has_non_specialized_filter_columns: bool,
161    /// Buffered row count. Always less than `batch_size`
162    buffered_rows: usize,
163    /// Completed batches
164    completed: VecDeque<RecordBatch>,
165    /// Biggest coalesce batch size. See [`Self::with_biggest_coalesce_batch_size`]
166    biggest_coalesce_batch_size: Option<usize>,
167}
168
169impl BatchCoalescer {
170    /// Create a new `BatchCoalescer`
171    ///
172    /// # Arguments
173    /// - `schema` - the schema of the output batches
174    /// - `target_batch_size` - the number of rows in each output batch.
175    ///   Typical values are `4096` or `8192` rows.
176    ///
177    pub fn new(schema: SchemaRef, target_batch_size: usize) -> Self {
178        let has_non_specialized_filter_columns = schema
179            .fields()
180            .iter()
181            .any(|field| !has_sparse_filter_copy(field.data_type()));
182        let in_progress_arrays = schema
183            .fields()
184            .iter()
185            .map(|field| create_in_progress_array(field.data_type(), target_batch_size))
186            .collect::<Vec<_>>();
187
188        Self {
189            schema,
190            target_batch_size,
191            in_progress_arrays,
192            has_non_specialized_filter_columns,
193            // We will for sure store at least one completed batch
194            completed: VecDeque::with_capacity(1),
195            buffered_rows: 0,
196            biggest_coalesce_batch_size: None,
197        }
198    }
199
200    /// Set the coalesce batch size limit (default `None`)
201    ///
202    /// This limit determine when batches should bypass coalescing. Intuitively,
203    /// batches that are already large are costly to coalesce and are efficient
204    /// enough to process directly without coalescing.
205    ///
206    /// If `Some(limit)`, batches larger than this limit will bypass coalescing
207    /// when there is no buffered data, or when the previously buffered data
208    /// already exceeds this limit.
209    ///
210    /// If `None`, all batches will be coalesced according to the
211    /// target_batch_size.
212    pub fn with_biggest_coalesce_batch_size(mut self, limit: Option<usize>) -> Self {
213        self.biggest_coalesce_batch_size = limit;
214        self
215    }
216
217    /// Get the current biggest coalesce batch size limit
218    ///
219    /// See [`Self::with_biggest_coalesce_batch_size`] for details
220    pub fn biggest_coalesce_batch_size(&self) -> Option<usize> {
221        self.biggest_coalesce_batch_size
222    }
223
224    /// Set the biggest coalesce batch size limit
225    ///
226    /// See [`Self::with_biggest_coalesce_batch_size`] for details
227    pub fn set_biggest_coalesce_batch_size(&mut self, limit: Option<usize>) {
228        self.biggest_coalesce_batch_size = limit;
229    }
230
231    /// Return the schema of the output batches
232    pub fn schema(&self) -> SchemaRef {
233        Arc::clone(&self.schema)
234    }
235
236    /// Push a batch into the Coalescer after applying a filter
237    ///
238    /// This is semantically equivalent of calling [`Self::push_batch`]
239    /// with the results from [`crate::filter::filter_record_batch`]
240    ///
241    /// # Example
242    /// ```
243    /// # use arrow_array::{record_batch, BooleanArray};
244    /// # use arrow_select::coalesce::BatchCoalescer;
245    /// let batch1 = record_batch!(("a", Int32, [1, 2, 3])).unwrap();
246    /// let batch2 = record_batch!(("a", Int32, [4, 5, 6])).unwrap();
247    /// // Apply a filter to each batch to pick the first and last row
248    /// let filter = BooleanArray::from(vec![true, false, true]);
249    /// // create a new Coalescer that targets creating 1000 row batches
250    /// let mut coalescer = BatchCoalescer::new(batch1.schema(), 1000);
251    /// coalescer.push_batch_with_filter(batch1, &filter);
252    /// coalescer.push_batch_with_filter(batch2, &filter);
253    /// // finish and retrieve the created batch
254    /// coalescer.finish_buffered_batch().unwrap();
255    /// let completed_batch = coalescer.next_completed_batch().unwrap();
256    /// // filtered out 2 and 5:
257    /// let expected_batch = record_batch!(("a", Int32, [1, 3, 4, 6])).unwrap();
258    /// assert_eq!(completed_batch, expected_batch);
259    /// ```
260    pub fn push_batch_with_filter(
261        &mut self,
262        batch: RecordBatch,
263        filter: &BooleanArray,
264    ) -> Result<(), ArrowError> {
265        self.push_batch_with_filtered_columns(batch, filter)
266    }
267
268    /// Push a batch into the Coalescer after applying a set of indices
269    /// This is semantically equivalent of calling [`Self::push_batch`]
270    /// with the results from  [`take_record_batch`]
271    ///
272    /// # Example
273    /// ```
274    /// # use arrow_array::{record_batch, UInt64Array};
275    /// # use arrow_select::coalesce::BatchCoalescer;
276    /// let batch1 = record_batch!(("a", Int32, [0, 0, 0])).unwrap();
277    /// let batch2 = record_batch!(("a", Int32, [1, 1, 4, 5, 1, 4])).unwrap();
278    /// // Sorted indices to create a sorted output, this can be obtained with
279    /// // `arrow-ord`'s sort_to_indices operation
280    /// let indices = UInt64Array::from(vec![0, 1, 4, 2, 5, 3]);
281    /// // create a new Coalescer that targets creating 1000 row batches
282    /// let mut coalescer = BatchCoalescer::new(batch1.schema(), 1000);
283    /// coalescer.push_batch(batch1);
284    /// coalescer.push_batch_with_indices(batch2, &indices);
285    /// // finish and retrieve the created batch
286    /// coalescer.finish_buffered_batch().unwrap();
287    /// let completed_batch = coalescer.next_completed_batch().unwrap();
288    /// let expected_batch = record_batch!(("a", Int32, [0, 0, 0, 1, 1, 1, 4, 4, 5])).unwrap();
289    /// assert_eq!(completed_batch, expected_batch);
290    /// ```
291    pub fn push_batch_with_indices(
292        &mut self,
293        batch: RecordBatch,
294        indices: &dyn Array,
295    ) -> Result<(), ArrowError> {
296        // todo: optimize this to avoid materializing (copying the results of take indices to a new batch)
297        let taken_batch = take_record_batch(&batch, indices)?;
298        self.push_batch(taken_batch)
299    }
300
301    /// Push all the rows from `batch` into the Coalescer
302    ///
303    /// When buffered data plus incoming rows reach `target_batch_size` ,
304    /// completed batches are generated eagerly and can be retrieved via
305    /// [`Self::next_completed_batch()`].
306    /// Output batches contain exactly `target_batch_size` rows, so the tail of
307    /// the input batch may remain buffered.
308    /// Remaining partial data either waits for future input batches or can be
309    /// materialized immediately by calling [`Self::finish_buffered_batch()`].
310    ///
311    /// # Example
312    /// ```
313    /// # use arrow_array::record_batch;
314    /// # use arrow_select::coalesce::BatchCoalescer;
315    /// let batch1 = record_batch!(("a", Int32, [1, 2, 3])).unwrap();
316    /// let batch2 = record_batch!(("a", Int32, [4, 5, 6])).unwrap();
317    /// // create a new Coalescer that targets creating 1000 row batches
318    /// let mut coalescer = BatchCoalescer::new(batch1.schema(), 1000);
319    /// coalescer.push_batch(batch1);
320    /// coalescer.push_batch(batch2);
321    /// // finish and retrieve the created batch
322    /// coalescer.finish_buffered_batch().unwrap();
323    /// let completed_batch = coalescer.next_completed_batch().unwrap();
324    /// let expected_batch = record_batch!(("a", Int32, [1, 2, 3, 4, 5, 6])).unwrap();
325    /// assert_eq!(completed_batch, expected_batch);
326    /// ```
327    pub fn push_batch(&mut self, batch: RecordBatch) -> Result<(), ArrowError> {
328        // Large batch bypass optimization:
329        // When biggest_coalesce_batch_size is configured and a batch exceeds this limit,
330        // we can avoid expensive split-and-merge operations by passing it through directly.
331        //
332        // IMPORTANT: This optimization is OPTIONAL and only active when biggest_coalesce_batch_size
333        // is explicitly set via with_biggest_coalesce_batch_size(Some(limit)).
334        // If not set (None), ALL batches follow normal coalescing behavior regardless of size.
335
336        // =============================================================================
337        // CASE 1: No buffer + large batch → Direct bypass
338        // =============================================================================
339        // Example scenario (target_batch_size=1000, biggest_coalesce_batch_size=Some(500)):
340        // Input sequence: [600, 1200, 300]
341        //
342        // With biggest_coalesce_batch_size=Some(500) (optimization enabled):
343        //   600 → large batch detected! buffered_rows=0 → Case 1: direct bypass
344        //        → output: [600] (bypass, preserves large batch)
345        //   1200 → large batch detected! buffered_rows=0 → Case 1: direct bypass
346        //         → output: [1200] (bypass, preserves large batch)
347        //   300 → normal batch, buffer: [300]
348        //   Result: [600], [1200], [300] - large batches preserved, mixed sizes
349
350        // =============================================================================
351        // CASE 2: Buffer too large + large batch → Flush first, then bypass
352        // =============================================================================
353        // This case prevents creating extremely large merged batches that would
354        // significantly exceed both target_batch_size and biggest_coalesce_batch_size.
355        //
356        // Example 1: Buffer exceeds limit before large batch arrives
357        // target_batch_size=1000, biggest_coalesce_batch_size=Some(400)
358        // Input: [350, 200, 800]
359        //
360        // Step 1: push_batch([350])
361        //   → batch_size=350 <= 400, normal path
362        //   → buffer: [350], buffered_rows=350
363        //
364        // Step 2: push_batch([200])
365        //   → batch_size=200 <= 400, normal path
366        //   → buffer: [350, 200], buffered_rows=550
367        //
368        // Step 3: push_batch([800])
369        //   → batch_size=800 > 400, large batch path
370        //   → buffered_rows=550 > 400 → Case 2: flush first
371        //   → flush: output [550] (combined [350, 200])
372        //   → then bypass: output [800]
373        //   Result: [550], [800] - buffer flushed to prevent oversized merge
374        //
375        // Example 2: Multiple small batches accumulate before large batch
376        // target_batch_size=1000, biggest_coalesce_batch_size=Some(300)
377        // Input: [150, 100, 80, 900]
378        //
379        // Step 1-3: Accumulate small batches
380        //   150 → buffer: [150], buffered_rows=150
381        //   100 → buffer: [150, 100], buffered_rows=250
382        //   80  → buffer: [150, 100, 80], buffered_rows=330
383        //
384        // Step 4: push_batch([900])
385        //   → batch_size=900 > 300, large batch path
386        //   → buffered_rows=330 > 300 → Case 2: flush first
387        //   → flush: output [330] (combined [150, 100, 80])
388        //   → then bypass: output [900]
389        //   Result: [330], [900] - prevents merge into [1230] which would be too large
390
391        // =============================================================================
392        // CASE 3: Small buffer + large batch → Normal coalescing (no bypass)
393        // =============================================================================
394        // When buffer is small enough, we still merge to maintain efficiency
395        // Example: target_batch_size=1000, biggest_coalesce_batch_size=Some(500)
396        // Input: [300, 1200]
397        //
398        // Step 1: push_batch([300])
399        //   → batch_size=300 <= 500, normal path
400        //   → buffer: [300], buffered_rows=300
401        //
402        // Step 2: push_batch([1200])
403        //   → batch_size=1200 > 500, large batch path
404        //   → buffered_rows=300 <= 500 → Case 3: normal merge
405        //   → buffer: [300, 1200] (1500 total)
406        //   → 1500 > target_batch_size → split: output [1000], buffer [500]
407        //   Result: [1000], [500] - normal split/merge behavior maintained
408
409        // =============================================================================
410        // Comparison: Default vs Optimized Behavior
411        // =============================================================================
412        // target_batch_size=1000, biggest_coalesce_batch_size=Some(500)
413        // Input: [600, 1200, 300]
414        //
415        // DEFAULT BEHAVIOR (biggest_coalesce_batch_size=None):
416        //   600 → buffer: [600]
417        //   1200 → buffer: [600, 1200] (1800 rows total)
418        //         → split: output [1000 rows], buffer [800 rows remaining]
419        //   300 → buffer: [800, 300] (1100 rows total)
420        //        → split: output [1000 rows], buffer [100 rows remaining]
421        //   Result: [1000], [1000], [100] - all outputs respect target_batch_size
422        //
423        // OPTIMIZED BEHAVIOR (biggest_coalesce_batch_size=Some(500)):
424        //   600 → Case 1: direct bypass → output: [600]
425        //   1200 → Case 1: direct bypass → output: [1200]
426        //   300 → normal path → buffer: [300]
427        //   Result: [600], [1200], [300] - large batches preserved
428
429        // =============================================================================
430        // Benefits and Trade-offs
431        // =============================================================================
432        // Benefits of the optimization:
433        // - Large batches stay intact (better for downstream vectorized processing)
434        // - Fewer split/merge operations (better CPU performance)
435        // - More predictable memory usage patterns
436        // - Maintains streaming efficiency while preserving batch boundaries
437        //
438        // Trade-offs:
439        // - Output batch sizes become variable (not always target_batch_size)
440        // - May produce smaller partial batches when flushing before large batches
441        // - Requires tuning biggest_coalesce_batch_size parameter for optimal performance
442
443        // TODO, for unsorted batches, we may can filter all large batches, and coalesce all
444        // small batches together?
445
446        let batch_size = batch.num_rows();
447
448        // Fast path: skip empty batches
449        if batch_size == 0 {
450            return Ok(());
451        }
452
453        // Large batch optimization: bypass coalescing for oversized batches
454        if let Some(limit) = self.biggest_coalesce_batch_size
455            && batch_size > limit
456        {
457            // Case 1: No buffered data - emit large batch directly
458            // Example: [] + [1200] → output [1200], buffer []
459            if self.buffered_rows == 0 {
460                self.completed.push_back(batch);
461                return Ok(());
462            }
463
464            // Case 2: Buffer too large - flush then emit to avoid oversized merge
465            // Example: [850] + [1200] → output [850], then output [1200]
466            // This prevents creating batches much larger than both target_batch_size
467            // and biggest_coalesce_batch_size, which could cause memory issues
468            if self.buffered_rows > limit {
469                self.finish_buffered_batch()?;
470                self.completed.push_back(batch);
471                return Ok(());
472            }
473
474            // Case 3: Small buffer - proceed with normal coalescing
475            // Example: [300] + [1200] → split and merge normally
476            // This ensures small batches still get properly coalesced
477            // while allowing some controlled growth beyond the limit
478        }
479
480        let (_schema, arrays, mut num_rows) = batch.into_parts();
481
482        // Validate column count matches the expected schema
483        if arrays.len() != self.in_progress_arrays.len() {
484            return Err(ArrowError::InvalidArgumentError(format!(
485                "Batch has {} columns but BatchCoalescer expects {}",
486                arrays.len(),
487                self.in_progress_arrays.len()
488            )));
489        }
490        self.in_progress_arrays
491            .iter_mut()
492            .zip(arrays)
493            .for_each(|(in_progress, array)| {
494                in_progress.set_source(Some(array));
495            });
496
497        // If pushing this batch would exceed the target batch size,
498        // finish the current batch and start a new one
499        let mut offset = 0;
500        while num_rows > (self.target_batch_size - self.buffered_rows) {
501            let remaining_rows = self.target_batch_size - self.buffered_rows;
502            debug_assert!(remaining_rows > 0);
503
504            // Copy remaining_rows from each array
505            for in_progress in &mut self.in_progress_arrays {
506                in_progress.copy_rows(offset, remaining_rows)?;
507            }
508
509            self.buffered_rows += remaining_rows;
510            offset += remaining_rows;
511            num_rows -= remaining_rows;
512
513            self.finish_buffered_batch()?;
514        }
515
516        // Add any the remaining rows to the buffer
517        self.buffered_rows += num_rows;
518        if num_rows > 0 {
519            for in_progress in &mut self.in_progress_arrays {
520                in_progress.copy_rows(offset, num_rows)?;
521            }
522        }
523
524        // If we have reached the target batch size, finalize the buffered batch
525        if self.buffered_rows >= self.target_batch_size {
526            self.finish_buffered_batch()?;
527        }
528
529        // clear in progress sources (to allow the memory to be freed)
530        for in_progress in &mut self.in_progress_arrays {
531            in_progress.set_source(None);
532        }
533
534        Ok(())
535    }
536
537    /// Returns the number of buffered rows
538    pub fn get_buffered_rows(&self) -> usize {
539        self.buffered_rows
540    }
541
542    /// Concatenates any buffered batches into a single `RecordBatch` and
543    /// clears any output buffers
544    ///
545    /// Normally this is called when the input stream is exhausted, and
546    /// we want to finalize the last batch of rows.
547    ///
548    /// See [`Self::next_completed_batch()`] for the completed batches.
549    pub fn finish_buffered_batch(&mut self) -> Result<(), ArrowError> {
550        if self.buffered_rows == 0 {
551            return Ok(());
552        }
553        let new_arrays = self
554            .in_progress_arrays
555            .iter_mut()
556            .map(|array| array.finish())
557            .collect::<Result<Vec<_>, ArrowError>>()?;
558
559        for (array, field) in new_arrays.iter().zip(self.schema.fields().iter()) {
560            debug_assert_eq!(array.data_type(), field.data_type());
561            debug_assert_eq!(array.len(), self.buffered_rows);
562        }
563
564        // SAFETY: each array was created of the correct type and length.
565        let batch = unsafe {
566            RecordBatch::new_unchecked(Arc::clone(&self.schema), new_arrays, self.buffered_rows)
567        };
568
569        self.buffered_rows = 0;
570        self.completed.push_back(batch);
571        Ok(())
572    }
573
574    /// Returns true if there is any buffered data
575    pub fn is_empty(&self) -> bool {
576        self.buffered_rows == 0 && self.completed.is_empty()
577    }
578
579    /// Returns true if there are any completed batches
580    pub fn has_completed_batch(&self) -> bool {
581        !self.completed.is_empty()
582    }
583
584    /// Removes and returns the next completed batch, if any.
585    pub fn next_completed_batch(&mut self) -> Option<RecordBatch> {
586        self.completed.pop_front()
587    }
588
589    /// Returns the number of bytes used by this data structure.
590    pub fn size(&self) -> usize {
591        self.in_progress_arrays.capacity() * size_of::<Box<dyn InProgressArray>>()
592            + self
593                .in_progress_arrays
594                .iter()
595                .map(|array| array.size())
596                .sum::<usize>()
597            + self.completed.capacity() * size_of::<RecordBatch>()
598            + self
599                .completed
600                .iter()
601                .map(|batch| batch.get_array_memory_size())
602                .sum::<usize>()
603    }
604}
605
606impl BatchCoalescer {
607    fn filter_predicate_for_batch(
608        batch: &RecordBatch,
609        filter: &BooleanArray,
610        selected_count: usize,
611    ) -> FilterPredicate {
612        let mut filter_builder = FilterBuilder::new_with_count(filter, selected_count);
613        if batch.num_columns() > 1
614            || (batch.num_columns() > 0
615                && FilterBuilder::is_optimize_beneficial(batch.schema_ref().field(0).data_type()))
616        {
617            filter_builder = filter_builder.optimize();
618        }
619        filter_builder.build()
620    }
621
622    fn push_batch_with_filtered_columns(
623        &mut self,
624        batch: RecordBatch,
625        filter: &BooleanArray,
626    ) -> Result<(), ArrowError> {
627        let filter_len = filter.len();
628        let batch_num_rows = batch.num_rows();
629        let batch_num_columns = batch.num_columns();
630
631        if filter_len > batch_num_rows {
632            return Err(ArrowError::InvalidArgumentError(format!(
633                "Filter predicate of length {filter_len} is larger than target array of length {batch_num_rows}"
634            )));
635        }
636
637        let selected_count = filter.true_count();
638        if selected_count == 0 {
639            return Ok(());
640        }
641
642        if selected_count == batch_num_rows && filter_len == batch_num_rows {
643            return self.push_batch(batch);
644        }
645
646        if batch_num_columns != self.in_progress_arrays.len() {
647            return Err(ArrowError::InvalidArgumentError(format!(
648                "Batch has {} columns but BatchCoalescer expects {}",
649                batch_num_columns,
650                self.in_progress_arrays.len()
651            )));
652        }
653
654        let exceeds_coalesce_limit = self
655            .biggest_coalesce_batch_size
656            .is_some_and(|limit| selected_count > limit);
657        let does_not_fit_buffer = selected_count > self.target_batch_size - self.buffered_rows;
658        let should_materialize_filter = exceeds_coalesce_limit
659            || self.has_non_specialized_filter_columns
660            || does_not_fit_buffer
661            || !should_use_sparse_filter_copy(filter_len, selected_count);
662
663        if should_materialize_filter {
664            // Use materialized filtering when sparse per-column copying is unavailable.
665            let predicate = Self::filter_predicate_for_batch(&batch, filter, selected_count);
666            let filtered_batch = predicate.filter_record_batch(&batch)?;
667            return self.push_batch(filtered_batch);
668        }
669
670        let predicate = Self::filter_predicate_for_batch(&batch, filter, selected_count);
671        let (_schema, arrays, _num_rows) = batch.into_parts();
672
673        for (in_progress, array) in self.in_progress_arrays.iter_mut().zip(arrays) {
674            in_progress.copy_rows_by_filter_from(array, &predicate)?;
675        }
676
677        self.buffered_rows += selected_count;
678        if self.buffered_rows >= self.target_batch_size {
679            self.finish_buffered_batch()?;
680        }
681
682        Ok(())
683    }
684}
685
686/// Return a new `InProgressArray` for the given data type
687fn create_in_progress_array(data_type: &DataType, batch_size: usize) -> Box<dyn InProgressArray> {
688    macro_rules! instantiate_primitive {
689        ($t:ty) => {
690            Box::new(InProgressPrimitiveArray::<$t>::new(
691                batch_size,
692                data_type.clone(),
693            ))
694        };
695    }
696
697    downcast_primitive! {
698        // Instantiate InProgressPrimitiveArray for each primitive type
699        data_type => (instantiate_primitive),
700        DataType::Utf8View => Box::new(InProgressByteViewArray::<StringViewType>::new(batch_size)),
701        DataType::BinaryView => {
702            Box::new(InProgressByteViewArray::<BinaryViewType>::new(batch_size))
703        }
704        DataType::FixedSizeBinary(size) => {
705            Box::new(InProgressFixedSizeBinaryArray::new(*size, batch_size))
706        }
707        _ => Box::new(GenericInProgressArray::new()),
708    }
709}
710
711/// Incrementally builds up arrays
712///
713/// [`GenericInProgressArray`] is the default implementation that buffers
714/// arrays, uses other kernels, and concatenates them when finished.
715///
716/// Some types have specialized, faster implementations (e.g.,
717/// [`StringViewArray`], etc.).
718///
719/// [`StringViewArray`]: arrow_array::StringViewArray
720trait InProgressArray: std::fmt::Debug + Send + Sync {
721    /// Set the source array.
722    ///
723    /// Calls to [`Self::copy_rows`] will copy rows from this array into the
724    /// current in-progress array
725    fn set_source(&mut self, source: Option<ArrayRef>);
726
727    /// Copy rows from the current source array into the in-progress array
728    ///
729    /// Note: The source array is set by [`Self::set_source`].
730    ///
731    /// Return an error if the source array is not set
732    fn copy_rows(&mut self, offset: usize, len: usize) -> Result<(), ArrowError>;
733
734    /// Copy rows selected by `filter` from the current source array.
735    ///
736    /// The default implementation calls [`Self::copy_rows_by_selection`]
737    fn copy_rows_by_filter(&mut self, filter: &FilterPredicate) -> Result<(), ArrowError> {
738        self.copy_rows_by_selection(filter.selection())
739    }
740
741    /// Copy rows selected by a [`FilterPredicate`] from `source`.
742    ///
743    /// Unlike the other copy methods, the source array is passed in directly,
744    /// which allows implementations more flexibility. The default
745    /// implementation simply sets `source` via [`Self::set_source`] and then
746    /// calls [`Self::copy_rows_by_filter`].
747    fn copy_rows_by_filter_from(
748        &mut self,
749        source: ArrayRef,
750        filter: &FilterPredicate,
751    ) -> Result<(), ArrowError> {
752        self.set_source(Some(source));
753        let result = self.copy_rows_by_filter(filter);
754        self.set_source(None);
755        result
756    }
757
758    /// Copy rows described by a [`FilterSelection`] from the current source array.
759    ///
760    /// You typically get a [`FilterSelection`] from [`FilterPredicate::selection`].
761    ///
762    /// Note: The source array is set by [`Self::set_source`].
763    fn copy_rows_by_selection(&mut self, selection: FilterSelection<'_>) -> Result<(), ArrowError> {
764        match selection {
765            FilterSelection::None => Ok(()),
766            FilterSelection::All { len } => self.copy_rows(0, len),
767            FilterSelection::Slices(slices) => {
768                slices.try_for_each(|(start, end)| self.copy_rows(start, end - start))
769            }
770            FilterSelection::Indices(indices) => indices.try_for_each(|idx| self.copy_rows(idx, 1)),
771        }
772    }
773
774    /// Finish the currently in-progress array and return it as an `ArrayRef`
775    fn finish(&mut self) -> Result<ArrayRef, ArrowError>;
776
777    /// Get the number of bytes this array is using
778    fn size(&self) -> usize;
779}
780
781#[cfg(test)]
782mod tests {
783    use super::*;
784    use crate::concat::concat_batches;
785    use crate::filter::filter_record_batch;
786    use arrow_array::builder::StringViewBuilder;
787    use arrow_array::cast::AsArray;
788    use arrow_array::types::Int32Type;
789    use arrow_array::{
790        BinaryViewArray, Int32Array, Int64Array, RecordBatchOptions, StringArray, StringViewArray,
791        TimestampNanosecondArray, UInt32Array, UInt64Array, make_array,
792    };
793    use arrow_buffer::BooleanBufferBuilder;
794    use arrow_schema::{DataType, Field, Schema};
795    use rand::{RngExt, SeedableRng};
796    use std::ops::Range;
797
798    #[test]
799    fn test_coalesce() {
800        let batch = uint32_batch(0..8);
801        Test::new("coalesce")
802            .with_batches(std::iter::repeat_n(batch, 10))
803            // expected output is exactly 21 rows (except for the final batch)
804            .with_batch_size(21)
805            .with_expected_output_sizes(vec![21, 21, 21, 17])
806            .run();
807    }
808
809    #[test]
810    fn test_coalesce_one_by_one() {
811        let batch = uint32_batch(0..1); // single row input
812        Test::new("coalesce_one_by_one")
813            .with_batches(std::iter::repeat_n(batch, 97))
814            // expected output is exactly 20 rows (except for the final batch)
815            .with_batch_size(20)
816            .with_expected_output_sizes(vec![20, 20, 20, 20, 17])
817            .run();
818    }
819
820    #[test]
821    fn test_coalesce_empty() {
822        let schema = Arc::new(Schema::new(vec![Field::new("c0", DataType::UInt32, false)]));
823
824        Test::new("coalesce_empty")
825            .with_batches(vec![])
826            .with_schema(schema)
827            .with_batch_size(21)
828            .with_expected_output_sizes(vec![])
829            .run();
830    }
831
832    #[test]
833    fn test_sparse_filter_copy_threshold() {
834        assert!(should_use_sparse_filter_copy(8192, 8));
835        assert!(should_use_sparse_filter_copy(8192, 81));
836        assert!(!should_use_sparse_filter_copy(8192, 819));
837        assert!(!should_use_sparse_filter_copy(8192, 6553));
838    }
839
840    #[test]
841    fn test_single_large_batch_greater_than_target() {
842        // test a single large batch
843        let batch = uint32_batch(0..4096);
844        Test::new("coalesce_single_large_batch_greater_than_target")
845            .with_batch(batch)
846            .with_batch_size(1000)
847            .with_expected_output_sizes(vec![1000, 1000, 1000, 1000, 96])
848            .run();
849    }
850
851    #[test]
852    fn test_single_large_batch_smaller_than_target() {
853        // test a single large batch
854        let batch = uint32_batch(0..4096);
855        Test::new("coalesce_single_large_batch_smaller_than_target")
856            .with_batch(batch)
857            .with_batch_size(8192)
858            .with_expected_output_sizes(vec![4096])
859            .run();
860    }
861
862    #[test]
863    fn test_single_large_batch_equal_to_target() {
864        // test a single large batch
865        let batch = uint32_batch(0..4096);
866        Test::new("coalesce_single_large_batch_equal_to_target")
867            .with_batch(batch)
868            .with_batch_size(4096)
869            .with_expected_output_sizes(vec![4096])
870            .run();
871    }
872
873    #[test]
874    fn test_single_large_batch_equally_divisible_in_target() {
875        // test a single large batch
876        let batch = uint32_batch(0..4096);
877        Test::new("coalesce_single_large_batch_equally_divisible_in_target")
878            .with_batch(batch)
879            .with_batch_size(1024)
880            .with_expected_output_sizes(vec![1024, 1024, 1024, 1024])
881            .run();
882    }
883
884    #[test]
885    fn test_empty_schema() {
886        let schema = Schema::empty();
887        let batch = RecordBatch::new_empty(schema.into());
888        Test::new("coalesce_empty_schema")
889            .with_batch(batch)
890            .with_expected_output_sizes(vec![])
891            .run();
892    }
893
894    /// Coalesce multiple batches, 80k rows, with a 0.1% selectivity filter
895    #[test]
896    #[cfg_attr(miri, ignore)] // Takes too long
897    fn test_coalesce_filtered_001() {
898        let mut filter_builder = RandomFilterBuilder {
899            num_rows: 8000,
900            selectivity: 0.001,
901            seed: 0,
902        };
903
904        // add 10 batches of 8000 rows each
905        // 80k rows, selecting 0.1% means 80 rows
906        // not exactly 80 as the rows are random;
907        let mut test = Test::new("coalesce_filtered_001");
908        for _ in 0..10 {
909            test = test
910                .with_batch(multi_column_batch(0..8000))
911                .with_filter(filter_builder.next_filter())
912        }
913        test.with_batch_size(15)
914            .with_expected_output_sizes(vec![15, 15, 15, 13])
915            .run();
916    }
917
918    /// Coalesce multiple batches, 80k rows, with a 1% selectivity filter
919    #[test]
920    #[cfg_attr(miri, ignore)] // Takes too long
921    fn test_coalesce_filtered_01() {
922        let mut filter_builder = RandomFilterBuilder {
923            num_rows: 8000,
924            selectivity: 0.01,
925            seed: 0,
926        };
927
928        // add 10 batches of 8000 rows each
929        // 80k rows, selecting 1% means 800 rows
930        // not exactly 800 as the rows are random;
931        let mut test = Test::new("coalesce_filtered_01");
932        for _ in 0..10 {
933            test = test
934                .with_batch(multi_column_batch(0..8000))
935                .with_filter(filter_builder.next_filter())
936        }
937        test.with_batch_size(128)
938            .with_expected_output_sizes(vec![128, 128, 128, 128, 128, 128, 15])
939            .run();
940    }
941
942    /// Coalesce multiple batches, 80k rows, with a 10% selectivity filter
943    #[test]
944    #[cfg_attr(miri, ignore)] // Takes too long
945    fn test_coalesce_filtered_10() {
946        let mut filter_builder = RandomFilterBuilder {
947            num_rows: 8000,
948            selectivity: 0.1,
949            seed: 0,
950        };
951
952        // add 10 batches of 8000 rows each
953        // 80k rows, selecting 10% means 8000 rows
954        // not exactly 800 as the rows are random;
955        let mut test = Test::new("coalesce_filtered_10");
956        for _ in 0..10 {
957            test = test
958                .with_batch(multi_column_batch(0..8000))
959                .with_filter(filter_builder.next_filter())
960        }
961        test.with_batch_size(1024)
962            .with_expected_output_sizes(vec![1024, 1024, 1024, 1024, 1024, 1024, 1024, 840])
963            .run();
964    }
965
966    /// Coalesce multiple batches, 8k rows, with a 90% selectivity filter
967    #[test]
968    #[cfg_attr(miri, ignore)] // Takes too long
969    fn test_coalesce_filtered_90() {
970        let mut filter_builder = RandomFilterBuilder {
971            num_rows: 800,
972            selectivity: 0.90,
973            seed: 0,
974        };
975
976        // add 10 batches of 800 rows each
977        // 8k rows, selecting 99% means 7200 rows
978        // not exactly 7200 as the rows are random;
979        let mut test = Test::new("coalesce_filtered_90");
980        for _ in 0..10 {
981            test = test
982                .with_batch(multi_column_batch(0..800))
983                .with_filter(filter_builder.next_filter())
984        }
985        test.with_batch_size(1024)
986            .with_expected_output_sizes(vec![1024, 1024, 1024, 1024, 1024, 1024, 1024, 13])
987            .run();
988    }
989
990    /// Coalesce multiple batches, 8k rows, with mixed filers, including 100%
991    #[test]
992    #[cfg_attr(miri, ignore)] // Takes too long
993    fn test_coalesce_filtered_mixed() {
994        let mut filter_builder = RandomFilterBuilder {
995            num_rows: 800,
996            selectivity: 0.90,
997            seed: 0,
998        };
999
1000        let mut test = Test::new("coalesce_filtered_mixed");
1001        for _ in 0..3 {
1002            // also add in a batch that selects almost all rows and when
1003            // sliced will have some batches that are entirely used
1004            let mut all_filter_builder = BooleanBufferBuilder::new(1000);
1005            all_filter_builder.append_n(500, true);
1006            all_filter_builder.append_n(1, false);
1007            all_filter_builder.append_n(499, false);
1008            let all_filter = all_filter_builder.build();
1009
1010            test = test
1011                .with_batch(multi_column_batch(0..1000))
1012                .with_filter(BooleanArray::from(all_filter))
1013                .with_batch(multi_column_batch(0..800))
1014                .with_filter(filter_builder.next_filter());
1015            // decrease selectivity
1016            filter_builder.selectivity *= 0.6;
1017        }
1018
1019        // use a small batch size to ensure the filter is appended in slices
1020        // and some of those slides will select the entire thing.
1021        test.with_batch_size(250)
1022            .with_expected_output_sizes(vec![
1023                250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 179,
1024            ])
1025            .run();
1026    }
1027
1028    #[test]
1029    fn test_coalesce_non_null() {
1030        Test::new("coalesce_non_null")
1031            // 4040 rows of unit32
1032            .with_batch(uint32_batch_non_null(0..3000))
1033            .with_batch(uint32_batch_non_null(0..1040))
1034            .with_batch_size(1024)
1035            .with_expected_output_sizes(vec![1024, 1024, 1024, 968])
1036            .run();
1037    }
1038    #[test]
1039    #[cfg_attr(miri, ignore)] // Takes too long
1040    fn test_utf8_split() {
1041        Test::new("coalesce_utf8")
1042            // 4040 rows of utf8 strings in total, split into batches of 1024
1043            .with_batch(utf8_batch(0..3000))
1044            .with_batch(utf8_batch(0..1040))
1045            .with_batch_size(1024)
1046            .with_expected_output_sizes(vec![1024, 1024, 1024, 968])
1047            .run();
1048    }
1049
1050    #[test]
1051    fn test_string_view_no_views() {
1052        let output_batches = Test::new("coalesce_string_view_no_views")
1053            // both input batches have no views, so no need to compact
1054            .with_batch(stringview_batch([Some("foo"), Some("bar")]))
1055            .with_batch(stringview_batch([Some("baz"), Some("qux")]))
1056            .with_expected_output_sizes(vec![4])
1057            .run();
1058
1059        expect_buffer_layout(
1060            col_as_string_view("c0", output_batches.first().unwrap()),
1061            vec![],
1062        );
1063    }
1064
1065    #[test]
1066    fn test_string_view_batch_small_no_compact() {
1067        // view with only short strings (no buffers) --> no need to compact
1068        let batch = stringview_batch_repeated(1000, [Some("a"), Some("b"), Some("c")]);
1069        let output_batches = Test::new("coalesce_string_view_batch_small_no_compact")
1070            .with_batch(batch.clone())
1071            .with_expected_output_sizes(vec![1000])
1072            .run();
1073
1074        let array = col_as_string_view("c0", &batch);
1075        let gc_array = col_as_string_view("c0", output_batches.first().unwrap());
1076        assert_eq!(array.data_buffers().len(), 0);
1077        assert_eq!(array.data_buffers().len(), gc_array.data_buffers().len()); // no compaction
1078
1079        expect_buffer_layout(gc_array, vec![]);
1080    }
1081
1082    #[test]
1083    #[cfg_attr(miri, ignore)] // Takes too long
1084    fn test_string_view_batch_large_no_compact() {
1085        // view with large strings (has buffers) but full --> no need to compact
1086        let batch = stringview_batch_repeated(1000, [Some("This string is longer than 12 bytes")]);
1087        let output_batches = Test::new("coalesce_string_view_batch_large_no_compact")
1088            .with_batch(batch.clone())
1089            .with_batch_size(1000)
1090            .with_expected_output_sizes(vec![1000])
1091            .run();
1092
1093        let array = col_as_string_view("c0", &batch);
1094        let gc_array = col_as_string_view("c0", output_batches.first().unwrap());
1095        assert_eq!(array.data_buffers().len(), 5);
1096        assert_eq!(array.data_buffers().len(), gc_array.data_buffers().len()); // no compaction
1097
1098        expect_buffer_layout(
1099            gc_array,
1100            vec![
1101                ExpectedLayout {
1102                    len: 8190,
1103                    capacity: 8192,
1104                },
1105                ExpectedLayout {
1106                    len: 8190,
1107                    capacity: 8192,
1108                },
1109                ExpectedLayout {
1110                    len: 8190,
1111                    capacity: 8192,
1112                },
1113                ExpectedLayout {
1114                    len: 8190,
1115                    capacity: 8192,
1116                },
1117                ExpectedLayout {
1118                    len: 2240,
1119                    capacity: 8192,
1120                },
1121            ],
1122        );
1123    }
1124
1125    #[test]
1126    fn test_string_view_batch_small_with_buffers_no_compact() {
1127        // view with buffers but only short views
1128        let short_strings = std::iter::repeat(Some("SmallString"));
1129        let long_strings = std::iter::once(Some("This string is longer than 12 bytes"));
1130        // 20 short strings, then a long ones
1131        let values = short_strings.take(20).chain(long_strings);
1132        let batch = stringview_batch_repeated(1000, values)
1133            // take only 10 short strings (no long ones)
1134            .slice(5, 10);
1135        let output_batches = Test::new("coalesce_string_view_batch_small_with_buffers_no_compact")
1136            .with_batch(batch.clone())
1137            .with_batch_size(1000)
1138            .with_expected_output_sizes(vec![10])
1139            .run();
1140
1141        let array = col_as_string_view("c0", &batch);
1142        let gc_array = col_as_string_view("c0", output_batches.first().unwrap());
1143        assert_eq!(array.data_buffers().len(), 1); // input has one buffer
1144        assert_eq!(gc_array.data_buffers().len(), 0); // output has no buffers as only short strings
1145    }
1146
1147    #[test]
1148    fn test_string_view_batch_large_slice_compact() {
1149        // view with large strings (has buffers) and only partially used  --> no need to compact
1150        let batch = stringview_batch_repeated(1000, [Some("This string is longer than 12 bytes")])
1151            // slice only 22 rows, so most of the buffer is not used
1152            .slice(11, 22);
1153
1154        let output_batches = Test::new("coalesce_string_view_batch_large_slice_compact")
1155            .with_batch(batch.clone())
1156            .with_batch_size(1000)
1157            .with_expected_output_sizes(vec![22])
1158            .run();
1159
1160        let array = col_as_string_view("c0", &batch);
1161        let gc_array = col_as_string_view("c0", output_batches.first().unwrap());
1162        assert_eq!(array.data_buffers().len(), 5);
1163
1164        expect_buffer_layout(
1165            gc_array,
1166            vec![ExpectedLayout {
1167                len: 770,
1168                capacity: 8192,
1169            }],
1170        );
1171    }
1172
1173    #[test]
1174    #[cfg_attr(miri, ignore)] // Takes too long
1175    fn test_string_view_mixed() {
1176        let large_view_batch =
1177            stringview_batch_repeated(1000, [Some("This string is longer than 12 bytes")]);
1178        let small_view_batch = stringview_batch_repeated(1000, [Some("SmallString")]);
1179        let mixed_batch = stringview_batch_repeated(
1180            1000,
1181            [Some("This string is longer than 12 bytes"), Some("Small")],
1182        );
1183        let mixed_batch_nulls = stringview_batch_repeated(
1184            1000,
1185            [
1186                Some("This string is longer than 12 bytes"),
1187                Some("Small"),
1188                None,
1189            ],
1190        );
1191
1192        // Several batches with mixed inline / non inline
1193        // 4k rows in
1194        let output_batches = Test::new("coalesce_string_view_mixed")
1195            .with_batch(large_view_batch.clone())
1196            .with_batch(small_view_batch)
1197            // this batch needs to be compacted (less than 1/2 full)
1198            .with_batch(large_view_batch.slice(10, 20))
1199            .with_batch(mixed_batch_nulls)
1200            // this batch needs to be compacted (less than 1/2 full)
1201            .with_batch(large_view_batch.slice(10, 20))
1202            .with_batch(mixed_batch)
1203            .with_expected_output_sizes(vec![1024, 1024, 1024, 968])
1204            .run();
1205
1206        expect_buffer_layout(
1207            col_as_string_view("c0", output_batches.first().unwrap()),
1208            vec![
1209                ExpectedLayout {
1210                    len: 8190,
1211                    capacity: 8192,
1212                },
1213                ExpectedLayout {
1214                    len: 8190,
1215                    capacity: 8192,
1216                },
1217                ExpectedLayout {
1218                    len: 8190,
1219                    capacity: 8192,
1220                },
1221                ExpectedLayout {
1222                    len: 8190,
1223                    capacity: 8192,
1224                },
1225                ExpectedLayout {
1226                    len: 2240,
1227                    capacity: 8192,
1228                },
1229            ],
1230        );
1231    }
1232
1233    #[test]
1234    #[cfg_attr(miri, ignore)] // Takes too long
1235    fn test_string_view_many_small_compact() {
1236        // 200 rows alternating long (28) and short (≤12) strings.
1237        // Only the 100 long strings go into data buffers: 100 × 28 = 2800.
1238        let batch = stringview_batch_repeated(
1239            200,
1240            [Some("This string is 28 bytes long"), Some("small string")],
1241        );
1242        let output_batches = Test::new("coalesce_string_view_many_small_compact")
1243            // First allocated buffer is 8kb.
1244            // Appending 10 batches of 2800 bytes will use 2800 * 10 = 14kb (8kb, an 16kb and 32kbkb)
1245            .with_batch(batch.clone())
1246            .with_batch(batch.clone())
1247            .with_batch(batch.clone())
1248            .with_batch(batch.clone())
1249            .with_batch(batch.clone())
1250            .with_batch(batch.clone())
1251            .with_batch(batch.clone())
1252            .with_batch(batch.clone())
1253            .with_batch(batch.clone())
1254            .with_batch(batch.clone())
1255            .with_batch_size(8000)
1256            .with_expected_output_sizes(vec![2000]) // only 1000 rows total
1257            .run();
1258
1259        // expect a nice even distribution of buffers
1260        expect_buffer_layout(
1261            col_as_string_view("c0", output_batches.first().unwrap()),
1262            vec![
1263                ExpectedLayout {
1264                    len: 8176,
1265                    capacity: 8192,
1266                },
1267                ExpectedLayout {
1268                    len: 16380,
1269                    capacity: 16384,
1270                },
1271                ExpectedLayout {
1272                    len: 3444,
1273                    capacity: 32768,
1274                },
1275            ],
1276        );
1277    }
1278
1279    #[test]
1280    #[cfg_attr(miri, ignore)] // Takes too long
1281    fn test_string_view_many_small_boundary() {
1282        // The strings are designed to exactly fit into buffers that are powers of 2 long
1283        let batch = stringview_batch_repeated(100, [Some("This string is a power of two=32")]);
1284        let output_batches = Test::new("coalesce_string_view_many_small_boundary")
1285            .with_batches(std::iter::repeat_n(batch, 20))
1286            .with_batch_size(900)
1287            .with_expected_output_sizes(vec![900, 900, 200])
1288            .run();
1289
1290        // expect each buffer to be entirely full except the last one
1291        expect_buffer_layout(
1292            col_as_string_view("c0", output_batches.first().unwrap()),
1293            vec![
1294                ExpectedLayout {
1295                    len: 8192,
1296                    capacity: 8192,
1297                },
1298                ExpectedLayout {
1299                    len: 16384,
1300                    capacity: 16384,
1301                },
1302                ExpectedLayout {
1303                    len: 4224,
1304                    capacity: 32768,
1305                },
1306            ],
1307        );
1308    }
1309
1310    #[test]
1311    #[cfg_attr(miri, ignore)] // Takes too long
1312    fn test_string_view_large_small() {
1313        // The strings are 37 bytes long, so each batch has 100 * 28 = 2800 bytes
1314        let mixed_batch = stringview_batch_repeated(
1315            200,
1316            [Some("This string is 28 bytes long"), Some("small string")],
1317        );
1318        // These strings aren't copied, this array has an 8k buffer
1319        let all_large = stringview_batch_repeated(
1320            50,
1321            [Some(
1322                "This buffer has only large strings in it so there are no buffer copies",
1323            )],
1324        );
1325
1326        let output_batches = Test::new("coalesce_string_view_large_small")
1327            // First allocated buffer is 8kb.
1328            // Appending five batches of 2800 bytes will use 2800 * 10 = 28kb (8kb, an 16kb and 32kbkb)
1329            .with_batch(mixed_batch.clone())
1330            .with_batch(mixed_batch.clone())
1331            .with_batch(all_large.clone())
1332            .with_batch(mixed_batch.clone())
1333            .with_batch(all_large.clone())
1334            .with_batch(mixed_batch.clone())
1335            .with_batch(mixed_batch.clone())
1336            .with_batch(all_large.clone())
1337            .with_batch(mixed_batch.clone())
1338            .with_batch(all_large.clone())
1339            .with_batch_size(8000)
1340            .with_expected_output_sizes(vec![1400])
1341            .run();
1342
1343        expect_buffer_layout(
1344            col_as_string_view("c0", output_batches.first().unwrap()),
1345            vec![
1346                ExpectedLayout {
1347                    len: 8190,
1348                    capacity: 8192,
1349                },
1350                ExpectedLayout {
1351                    len: 16366,
1352                    capacity: 16384,
1353                },
1354                ExpectedLayout {
1355                    len: 6244,
1356                    capacity: 32768,
1357                },
1358            ],
1359        );
1360    }
1361
1362    #[test]
1363    #[cfg_attr(miri, ignore)] // Takes too long
1364    fn test_binary_view() {
1365        let values: Vec<Option<&[u8]>> = vec![
1366            Some(b"foo"),
1367            None,
1368            Some(b"A longer string that is more than 12 bytes"),
1369        ];
1370
1371        let binary_view =
1372            BinaryViewArray::from_iter(std::iter::repeat(values.iter()).flatten().take(1000));
1373        let batch =
1374            RecordBatch::try_from_iter(vec![("c0", Arc::new(binary_view) as ArrayRef)]).unwrap();
1375
1376        Test::new("coalesce_binary_view")
1377            .with_batch(batch.clone())
1378            .with_batch(batch.clone())
1379            .with_batch_size(512)
1380            .with_expected_output_sizes(vec![512, 512, 512, 464])
1381            .run();
1382    }
1383
1384    #[test]
1385    fn test_binary_view_filtered() {
1386        let values: Vec<Option<&[u8]>> = vec![
1387            Some(b"foo"),
1388            None,
1389            Some(b"A longer string that is more than 12 bytes"),
1390        ];
1391
1392        let binary_view =
1393            BinaryViewArray::from_iter(std::iter::repeat(values.iter()).flatten().take(1000));
1394        let batch =
1395            RecordBatch::try_from_iter(vec![("c0", Arc::new(binary_view) as ArrayRef)]).unwrap();
1396        let filter = sparse_filter(1000);
1397
1398        Test::new("coalesce_binary_view_filtered")
1399            .with_batch(batch.clone())
1400            .with_filter(filter.clone())
1401            .with_batch(batch)
1402            .with_filter(filter)
1403            .with_batch_size(256)
1404            .with_expected_output_sizes(vec![250])
1405            .run();
1406    }
1407
1408    #[test]
1409    fn test_binary_view_filtered_inline() {
1410        let values: Vec<Option<&[u8]>> = vec![Some(b"foo"), None, Some(b"barbaz")];
1411
1412        let binary_view =
1413            BinaryViewArray::from_iter(std::iter::repeat(values.iter()).flatten().take(1000));
1414        let batch =
1415            RecordBatch::try_from_iter(vec![("c0", Arc::new(binary_view) as ArrayRef)]).unwrap();
1416        let filter = sparse_filter(1000);
1417
1418        Test::new("coalesce_binary_view_filtered_inline")
1419            .with_batch(batch.clone())
1420            .with_filter(filter.clone())
1421            .with_batch(batch)
1422            .with_filter(filter)
1423            .with_batch_size(300)
1424            .with_expected_output_sizes(vec![250])
1425            .run();
1426    }
1427
1428    #[test]
1429    fn test_string_view_filtered_inline() {
1430        let values: Vec<Option<&str>> = vec![Some("foo"), None, Some("barbaz")];
1431
1432        let string_view =
1433            StringViewArray::from_iter(std::iter::repeat(values.iter()).flatten().take(1000));
1434        let batch =
1435            RecordBatch::try_from_iter(vec![("c0", Arc::new(string_view) as ArrayRef)]).unwrap();
1436        let filter = sparse_filter(1000);
1437
1438        Test::new("coalesce_string_view_filtered_inline")
1439            .with_batch(batch.clone())
1440            .with_filter(filter.clone())
1441            .with_batch(batch)
1442            .with_filter(filter)
1443            .with_batch_size(300)
1444            .with_expected_output_sizes(vec![250])
1445            .run();
1446    }
1447
1448    #[test]
1449    fn test_mixed_inline_binary_view_filtered() {
1450        let int_values =
1451            Int32Array::from_iter((0..1000).map(|v| if v % 5 == 0 { None } else { Some(v) }));
1452        let float_values = arrow_array::Float64Array::from_iter((0..1000).map(|v| Some(v as f64)));
1453        let binary_values: Vec<Option<&[u8]>> = vec![Some(b"foo"), None, Some(b"barbaz")];
1454        let binary_view = BinaryViewArray::from_iter(
1455            std::iter::repeat(binary_values.iter()).flatten().take(1000),
1456        );
1457
1458        let batch = RecordBatch::try_from_iter(vec![
1459            ("i", Arc::new(int_values) as ArrayRef),
1460            ("f", Arc::new(float_values) as ArrayRef),
1461            ("b", Arc::new(binary_view) as ArrayRef),
1462        ])
1463        .unwrap();
1464
1465        let filter = sparse_filter(1000);
1466
1467        Test::new("coalesce_mixed_inline_binary_view_filtered")
1468            .with_batch(batch.clone())
1469            .with_filter(filter.clone())
1470            .with_batch(batch)
1471            .with_filter(filter)
1472            .with_batch_size(300)
1473            .with_expected_output_sizes(vec![250])
1474            .run();
1475    }
1476
1477    #[test]
1478    fn test_mixed_inline_string_view_filtered() {
1479        let int_values =
1480            Int32Array::from_iter((0..1000).map(|v| if v % 5 == 0 { None } else { Some(v) }));
1481        let float_values = arrow_array::Float64Array::from_iter((0..1000).map(|v| Some(v as f64)));
1482        let string_values: Vec<Option<&str>> = vec![Some("foo"), None, Some("barbaz")];
1483        let string_view = StringViewArray::from_iter(
1484            std::iter::repeat(string_values.iter()).flatten().take(1000),
1485        );
1486
1487        let batch = RecordBatch::try_from_iter(vec![
1488            ("i", Arc::new(int_values) as ArrayRef),
1489            ("f", Arc::new(float_values) as ArrayRef),
1490            ("s", Arc::new(string_view) as ArrayRef),
1491        ])
1492        .unwrap();
1493
1494        let filter = sparse_filter(1000);
1495
1496        Test::new("coalesce_mixed_inline_string_view_filtered")
1497            .with_batch(batch.clone())
1498            .with_filter(filter.clone())
1499            .with_batch(batch)
1500            .with_filter(filter)
1501            .with_batch_size(300)
1502            .with_expected_output_sizes(vec![250])
1503            .run();
1504    }
1505
1506    #[test]
1507    fn test_inline_binary_view_sparse() {
1508        // All-inline binary views (<=12 bytes) + 5% selectivity
1509        let values: Vec<Option<&[u8]>> = vec![Some(b"foo"), None, Some(b"barbaz")];
1510        let binary_view =
1511            BinaryViewArray::from_iter(std::iter::repeat(values.iter()).flatten().take(1000));
1512        let batch =
1513            RecordBatch::try_from_iter(vec![("c0", Arc::new(binary_view) as ArrayRef)]).unwrap();
1514        let filter = very_sparse_filter(1000);
1515
1516        Test::new("inline_binary_view_sparse")
1517            .with_batch(batch.clone())
1518            .with_filter(filter.clone())
1519            .with_batch(batch)
1520            .with_filter(filter)
1521            .with_batch_size(1024)
1522            .with_expected_output_sizes(vec![100])
1523            .run();
1524    }
1525
1526    #[test]
1527    fn test_inline_string_view_sparse() {
1528        let values: Vec<Option<&str>> = vec![Some("foo"), None, Some("barbaz")];
1529        let string_view =
1530            StringViewArray::from_iter(std::iter::repeat(values.iter()).flatten().take(1000));
1531        let batch =
1532            RecordBatch::try_from_iter(vec![("c0", Arc::new(string_view) as ArrayRef)]).unwrap();
1533        let filter = very_sparse_filter(1000);
1534
1535        Test::new("inline_string_view_sparse")
1536            .with_batch(batch.clone())
1537            .with_filter(filter.clone())
1538            .with_batch(batch)
1539            .with_filter(filter)
1540            .with_batch_size(1024)
1541            .with_expected_output_sizes(vec![100])
1542            .run();
1543    }
1544
1545    #[test]
1546    fn test_inline_mixed_sparse() {
1547        // Mixing primitives with inline views exercises materialized `Indices`
1548        // selection for both the primitive and the byte-view.
1549        let int_values =
1550            Int32Array::from_iter((0..1000).map(|v| if v % 5 == 0 { None } else { Some(v) }));
1551        let float_values = arrow_array::Float64Array::from_iter((0..1000).map(|v| Some(v as f64)));
1552        let string_values: Vec<Option<&str>> = vec![Some("foo"), None, Some("barbaz")];
1553        let string_view = StringViewArray::from_iter(
1554            std::iter::repeat(string_values.iter()).flatten().take(1000),
1555        );
1556        let binary_values: Vec<Option<&[u8]>> = vec![Some(b"x"), None, Some(b"abcdef")];
1557        let binary_view = BinaryViewArray::from_iter(
1558            std::iter::repeat(binary_values.iter()).flatten().take(1000),
1559        );
1560
1561        let batch = RecordBatch::try_from_iter(vec![
1562            ("i", Arc::new(int_values) as ArrayRef),
1563            ("f", Arc::new(float_values) as ArrayRef),
1564            ("s", Arc::new(string_view) as ArrayRef),
1565            ("b", Arc::new(binary_view) as ArrayRef),
1566        ])
1567        .unwrap();
1568        let filter = very_sparse_filter(1000);
1569
1570        Test::new("inline_mixed_sparse")
1571            .with_batch(batch.clone())
1572            .with_filter(filter.clone())
1573            .with_batch(batch)
1574            .with_filter(filter)
1575            .with_batch_size(1024)
1576            .with_expected_output_sizes(vec![100])
1577            .run();
1578    }
1579
1580    #[test]
1581    fn test_inline_crosses_target_batch_size() {
1582        // Each 1000-row batch selects 50 rows; target_batch_size 100
1583        // exercises intermediate batch flushing batch (not just the final
1584        // flush).
1585        let values: Vec<Option<&[u8]>> = vec![Some(b"foo"), None, Some(b"barbaz")];
1586        let make_batch = || {
1587            let binary_view =
1588                BinaryViewArray::from_iter(std::iter::repeat(values.iter()).flatten().take(1000));
1589            RecordBatch::try_from_iter(vec![("c0", Arc::new(binary_view) as ArrayRef)]).unwrap()
1590        };
1591        let filter = very_sparse_filter(1000);
1592
1593        Test::new("inline_crosses_target_batch_size")
1594            .with_batch(make_batch())
1595            .with_filter(filter.clone())
1596            .with_batch(make_batch())
1597            .with_filter(filter.clone())
1598            .with_batch(make_batch())
1599            .with_filter(filter)
1600            .with_batch_size(100)
1601            .with_expected_output_sizes(vec![100, 50])
1602            .run();
1603    }
1604
1605    #[test]
1606    fn test_inline_filter_rejects_filter_longer_than_batch() {
1607        let values: Vec<Option<&[u8]>> = vec![Some(b"foo"), Some(b"bar")];
1608        let binary_view = BinaryViewArray::from_iter(values);
1609        let batch =
1610            RecordBatch::try_from_iter(vec![("c0", Arc::new(binary_view) as ArrayRef)]).unwrap();
1611        let filter = BooleanArray::from(vec![true, false, true]);
1612
1613        let mut coalescer = BatchCoalescer::new(batch.schema(), 100);
1614        let result = coalescer.push_batch_with_filter(batch, &filter);
1615        assert!(result.is_err());
1616        let err = result.unwrap_err().to_string();
1617        assert!(
1618            err.contains("Filter predicate of length 3 is larger than target array of length 2"),
1619            "unexpected error: {err}"
1620        );
1621    }
1622
1623    #[test]
1624    fn test_mixed_boolean_inline_string_view_filtered() {
1625        let bool_values = BooleanArray::from_iter((0..1000).map(|v| Some(v % 3 == 0)));
1626        let string_values: Vec<Option<&str>> = vec![Some("foo"), None, Some("barbaz")];
1627        let string_view = StringViewArray::from_iter(
1628            std::iter::repeat(string_values.iter()).flatten().take(1000),
1629        );
1630
1631        let batch = RecordBatch::try_from_iter(vec![
1632            ("b", Arc::new(bool_values) as ArrayRef),
1633            ("s", Arc::new(string_view) as ArrayRef),
1634        ])
1635        .unwrap();
1636
1637        let filter = sparse_filter(1000);
1638
1639        Test::new("coalesce_mixed_boolean_inline_string_view_filtered")
1640            .with_batch(batch.clone())
1641            .with_filter(filter.clone())
1642            .with_batch(batch)
1643            .with_filter(filter)
1644            .with_batch_size(300)
1645            .with_expected_output_sizes(vec![250])
1646            .run();
1647    }
1648
1649    #[test]
1650    fn test_filter_fast_path_schema_capability() {
1651        let supported = Arc::new(Schema::new(vec![
1652            Field::new("primitive", DataType::UInt32, false),
1653            Field::new("utf8_view", DataType::Utf8View, true),
1654            Field::new("binary_view", DataType::BinaryView, true),
1655        ]));
1656        let coalescer = BatchCoalescer::new(supported, 100);
1657        assert!(!coalescer.has_non_specialized_filter_columns);
1658
1659        let utf8 = Arc::new(Schema::new(vec![Field::new("utf8", DataType::Utf8, true)]));
1660        let coalescer = BatchCoalescer::new(utf8, 100);
1661        assert!(coalescer.has_non_specialized_filter_columns);
1662
1663        let boolean = Arc::new(Schema::new(vec![Field::new(
1664            "boolean",
1665            DataType::Boolean,
1666            true,
1667        )]));
1668        let coalescer = BatchCoalescer::new(boolean, 100);
1669        assert!(coalescer.has_non_specialized_filter_columns);
1670    }
1671
1672    #[derive(Debug, Clone, PartialEq)]
1673    struct ExpectedLayout {
1674        len: usize,
1675        capacity: usize,
1676    }
1677
1678    /// Asserts that the buffer layout of the specified StringViewArray matches the expected layout
1679    fn expect_buffer_layout(array: &StringViewArray, expected: Vec<ExpectedLayout>) {
1680        let actual = array
1681            .data_buffers()
1682            .iter()
1683            .map(|b| ExpectedLayout {
1684                len: b.len(),
1685                capacity: b.capacity(),
1686            })
1687            .collect::<Vec<_>>();
1688
1689        assert_eq!(
1690            actual, expected,
1691            "Expected buffer layout {expected:#?} but got {actual:#?}"
1692        );
1693    }
1694
1695    /// Test for [`BatchCoalescer`]
1696    ///
1697    /// Pushes the input batches to the coalescer and verifies that the resulting
1698    /// batches have the
1699    /// 1. expected number of rows
1700    /// 2. The same results when the batches are filtered using the filter kernel
1701    #[derive(Debug, Clone)]
1702    struct Test {
1703        /// A human readable name to assist in debugging
1704        name: String,
1705        /// Batches to feed to the coalescer.
1706        input_batches: Vec<RecordBatch>,
1707        /// Filters to apply to the corresponding input batches.
1708        ///
1709        /// If there are no filters for the input batches, the batch will be
1710        /// pushed as is.
1711        filters: Vec<BooleanArray>,
1712        /// The schema. If not provided, the first batch's schema is used.
1713        schema: Option<SchemaRef>,
1714        /// Expected output sizes of the resulting batches
1715        expected_output_sizes: Vec<usize>,
1716        /// target batch size (default to 1024)
1717        target_batch_size: usize,
1718    }
1719
1720    impl Default for Test {
1721        fn default() -> Self {
1722            Self {
1723                name: String::new(),
1724                input_batches: vec![],
1725                filters: vec![],
1726                schema: None,
1727                expected_output_sizes: vec![],
1728                target_batch_size: 1024,
1729            }
1730        }
1731    }
1732
1733    impl Test {
1734        fn new(name: impl Into<String>) -> Self {
1735            Self {
1736                name: name.into(),
1737                ..Self::default()
1738            }
1739        }
1740
1741        /// Append the description to the test name
1742        fn with_description(mut self, description: &str) -> Self {
1743            self.name.push_str(": ");
1744            self.name.push_str(description);
1745            self
1746        }
1747
1748        /// Set the target batch size
1749        fn with_batch_size(mut self, target_batch_size: usize) -> Self {
1750            self.target_batch_size = target_batch_size;
1751            self
1752        }
1753
1754        /// Extend the input batches with `batch`
1755        fn with_batch(mut self, batch: RecordBatch) -> Self {
1756            self.input_batches.push(batch);
1757            self
1758        }
1759
1760        /// Extend the filters with `filter`
1761        fn with_filter(mut self, filter: BooleanArray) -> Self {
1762            self.filters.push(filter);
1763            self
1764        }
1765
1766        /// Replaces the input batches with `batches`
1767        fn with_batches(mut self, batches: impl IntoIterator<Item = RecordBatch>) -> Self {
1768            self.input_batches = batches.into_iter().collect();
1769            self
1770        }
1771
1772        /// Specifies the schema for the test
1773        fn with_schema(mut self, schema: SchemaRef) -> Self {
1774            self.schema = Some(schema);
1775            self
1776        }
1777
1778        /// Extends `sizes` to expected output sizes
1779        fn with_expected_output_sizes(mut self, sizes: impl IntoIterator<Item = usize>) -> Self {
1780            self.expected_output_sizes.extend(sizes);
1781            self
1782        }
1783
1784        /// Runs the test -- see documentation on [`Test`] for details
1785        ///
1786        /// Returns the resulting output batches
1787        fn run(self) -> Vec<RecordBatch> {
1788            // Test several permutations of input batches:
1789            // 1. Removing nulls from some batches (test non-null fast paths)
1790            // 2. Empty batches
1791            // 3. One column (from the batch)
1792            let mut extra_tests = vec![];
1793            extra_tests.push(self.clone().make_half_non_nullable());
1794            extra_tests.push(self.clone().insert_empty_batches());
1795            let single_column_tests = self.make_single_column_tests();
1796            for test in single_column_tests {
1797                extra_tests.push(test.clone().make_half_non_nullable());
1798                extra_tests.push(test);
1799            }
1800
1801            // Run original test case first, so any obvious errors are caught
1802            // by an easier to understand test case
1803            let results = self.run_inner();
1804            // Run the extra cases to expand coverage
1805            for extra in extra_tests {
1806                extra.run_inner();
1807            }
1808
1809            results
1810        }
1811
1812        /// Runs the current test instance
1813        fn run_inner(self) -> Vec<RecordBatch> {
1814            let expected_output = self.expected_output();
1815            let schema = self.schema();
1816
1817            let Self {
1818                name,
1819                input_batches,
1820                filters,
1821                schema: _,
1822                target_batch_size,
1823                expected_output_sizes,
1824            } = self;
1825
1826            println!("Running test '{name}'");
1827
1828            let had_input = input_batches.iter().any(|b| b.num_rows() > 0);
1829
1830            let mut coalescer = BatchCoalescer::new(Arc::clone(&schema), target_batch_size);
1831
1832            // feed input batches and filters to the coalescer
1833            let mut filters = filters.into_iter();
1834            for batch in input_batches {
1835                if let Some(filter) = filters.next() {
1836                    coalescer.push_batch_with_filter(batch, &filter).unwrap();
1837                } else {
1838                    coalescer.push_batch(batch).unwrap();
1839                }
1840            }
1841            assert_eq!(schema, coalescer.schema());
1842
1843            if had_input {
1844                assert!(!coalescer.is_empty(), "Coalescer should not be empty");
1845            } else {
1846                assert!(coalescer.is_empty(), "Coalescer should be empty");
1847            }
1848
1849            coalescer.finish_buffered_batch().unwrap();
1850            if had_input {
1851                assert!(
1852                    coalescer.has_completed_batch(),
1853                    "Coalescer should have completed batches"
1854                );
1855            }
1856
1857            let mut output_batches = vec![];
1858            while let Some(batch) = coalescer.next_completed_batch() {
1859                output_batches.push(batch);
1860            }
1861
1862            // make sure we got the expected number of output batches and content
1863            let mut starting_idx = 0;
1864            let actual_output_sizes: Vec<usize> =
1865                output_batches.iter().map(|b| b.num_rows()).collect();
1866            assert_eq!(
1867                expected_output_sizes, actual_output_sizes,
1868                "Unexpected number of rows in output batches\n\
1869                Expected\n{expected_output_sizes:#?}\nActual:{actual_output_sizes:#?}"
1870            );
1871            let iter = expected_output_sizes
1872                .iter()
1873                .zip(output_batches.iter())
1874                .enumerate();
1875
1876            // Verify that the actual contents of each output batch matches the expected output
1877            for (i, (expected_size, batch)) in iter {
1878                // compare the contents of the batch after normalization (using
1879                // `==` compares the underlying memory layout too)
1880                let expected_batch = expected_output.slice(starting_idx, *expected_size);
1881                let expected_batch = normalize_batch(expected_batch);
1882                let batch = normalize_batch(batch.clone());
1883                assert_eq!(
1884                    expected_batch, batch,
1885                    "Unexpected content in batch {i}:\
1886                    \n\nExpected:\n{expected_batch:#?}\n\nActual:\n{batch:#?}"
1887                );
1888                starting_idx += *expected_size;
1889            }
1890            output_batches
1891        }
1892
1893        /// Return the expected output schema. If not overridden by `with_schema`, it
1894        /// returns the schema of the first input batch.
1895        fn schema(&self) -> SchemaRef {
1896            self.schema
1897                .clone()
1898                .unwrap_or_else(|| Arc::clone(&self.input_batches[0].schema()))
1899        }
1900
1901        /// Returns the expected output as a single `RecordBatch`
1902        fn expected_output(&self) -> RecordBatch {
1903            let schema = self.schema();
1904            if self.filters.is_empty() {
1905                return concat_batches(&schema, &self.input_batches).unwrap();
1906            }
1907
1908            let mut filters = self.filters.iter();
1909            let filtered_batches = self
1910                .input_batches
1911                .iter()
1912                .map(|batch| {
1913                    if let Some(filter) = filters.next() {
1914                        filter_record_batch(batch, filter).unwrap()
1915                    } else {
1916                        batch.clone()
1917                    }
1918                })
1919                .collect::<Vec<_>>();
1920            concat_batches(&schema, &filtered_batches).unwrap()
1921        }
1922
1923        /// Return a copy of self where every other batch has had its nulls removed
1924        /// (there are often fast paths that are used when there are no nulls)
1925        fn make_half_non_nullable(mut self) -> Self {
1926            // remove the nulls from every other batch
1927            self.input_batches = self
1928                .input_batches
1929                .iter()
1930                .enumerate()
1931                .map(|(i, batch)| {
1932                    if i % 2 == 1 {
1933                        batch.clone()
1934                    } else {
1935                        Self::remove_nulls_from_batch(batch)
1936                    }
1937                })
1938                .collect();
1939            self.with_description("non-nullable")
1940        }
1941
1942        /// Insert several empty batches into the input before each existing input
1943        fn insert_empty_batches(mut self) -> Self {
1944            let empty_batch = RecordBatch::new_empty(self.schema());
1945            self.input_batches = self
1946                .input_batches
1947                .into_iter()
1948                .flat_map(|batch| [empty_batch.clone(), batch])
1949                .collect();
1950            let empty_filters = BooleanArray::builder(0).finish();
1951            self.filters = self
1952                .filters
1953                .into_iter()
1954                .flat_map(|filter| [empty_filters.clone(), filter])
1955                .collect();
1956            self.with_description("empty batches inserted")
1957        }
1958
1959        /// Sets one batch to be non-nullable by removing nulls from all columns
1960        fn remove_nulls_from_batch(batch: &RecordBatch) -> RecordBatch {
1961            let new_columns = batch
1962                .columns()
1963                .iter()
1964                .map(Self::remove_nulls_from_array)
1965                .collect::<Vec<_>>();
1966            let options = RecordBatchOptions::new().with_row_count(Some(batch.num_rows()));
1967            RecordBatch::try_new_with_options(batch.schema(), new_columns, &options).unwrap()
1968        }
1969
1970        fn remove_nulls_from_array(array: &ArrayRef) -> ArrayRef {
1971            make_array(array.to_data().into_builder().nulls(None).build().unwrap())
1972        }
1973
1974        /// Returns a set of tests where each test that is the sae as self, but
1975        /// has a single column from the original input batch
1976        ///
1977        /// This can be useful to single column optimizations, specifically
1978        /// filter optimization.
1979        fn make_single_column_tests(&self) -> Vec<Self> {
1980            let original_schema = self.schema();
1981            let mut new_tests = vec![];
1982            for column in original_schema.fields() {
1983                let single_column_schema = Arc::new(Schema::new(vec![column.clone()]));
1984
1985                let single_column_batches = self.input_batches.iter().map(|batch| {
1986                    let single_column = batch.column_by_name(column.name()).unwrap();
1987                    RecordBatch::try_new(
1988                        Arc::clone(&single_column_schema),
1989                        vec![single_column.clone()],
1990                    )
1991                    .unwrap()
1992                });
1993
1994                let single_column_test = self
1995                    .clone()
1996                    .with_schema(Arc::clone(&single_column_schema))
1997                    .with_batches(single_column_batches)
1998                    .with_description("single column")
1999                    .with_description(column.name());
2000
2001                new_tests.push(single_column_test);
2002            }
2003            new_tests
2004        }
2005    }
2006
2007    /// Return a RecordBatch with a UInt32Array with the specified range and
2008    /// every third value is null.
2009    fn uint32_batch<T: std::iter::Iterator<Item = u32>>(range: T) -> RecordBatch {
2010        let schema = Arc::new(Schema::new(vec![Field::new("c0", DataType::UInt32, true)]));
2011
2012        let array = UInt32Array::from_iter(range.map(|i| if i % 3 == 0 { None } else { Some(i) }));
2013        RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(array)]).unwrap()
2014    }
2015
2016    /// Return a RecordBatch with a UInt32Array with no nulls specified range
2017    fn uint32_batch_non_null<T: std::iter::Iterator<Item = u32>>(range: T) -> RecordBatch {
2018        let schema = Arc::new(Schema::new(vec![Field::new("c0", DataType::UInt32, false)]));
2019
2020        let array = UInt32Array::from_iter_values(range);
2021        RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(array)]).unwrap()
2022    }
2023
2024    /// Return a RecordBatch with a UInt64Array with no nulls specified range
2025    fn uint64_batch_non_null<T: std::iter::Iterator<Item = u64>>(range: T) -> RecordBatch {
2026        let schema = Arc::new(Schema::new(vec![Field::new("c0", DataType::UInt64, false)]));
2027
2028        let array = UInt64Array::from_iter_values(range);
2029        RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(array)]).unwrap()
2030    }
2031
2032    /// Return a RecordBatch with a StringArray with values `value0`, `value1`, ...
2033    /// and every third value is `None`.
2034    fn utf8_batch(range: Range<u32>) -> RecordBatch {
2035        let schema = Arc::new(Schema::new(vec![Field::new("c0", DataType::Utf8, true)]));
2036
2037        let array = StringArray::from_iter(range.map(|i| {
2038            if i % 3 == 0 {
2039                None
2040            } else {
2041                Some(format!("value{i}"))
2042            }
2043        }));
2044
2045        RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(array)]).unwrap()
2046    }
2047
2048    /// Return a RecordBatch with a StringViewArray with (only) the specified values
2049    fn stringview_batch<'a>(values: impl IntoIterator<Item = Option<&'a str>>) -> RecordBatch {
2050        let schema = Arc::new(Schema::new(vec![Field::new(
2051            "c0",
2052            DataType::Utf8View,
2053            false,
2054        )]));
2055
2056        let array = StringViewArray::from_iter(values);
2057        RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(array)]).unwrap()
2058    }
2059
2060    /// Return a RecordBatch with a StringViewArray with num_rows by repeating
2061    /// values over and over.
2062    fn stringview_batch_repeated<'a>(
2063        num_rows: usize,
2064        values: impl IntoIterator<Item = Option<&'a str>>,
2065    ) -> RecordBatch {
2066        let schema = Arc::new(Schema::new(vec![Field::new(
2067            "c0",
2068            DataType::Utf8View,
2069            true,
2070        )]));
2071
2072        // Repeat the values to a total of num_rows
2073        let values: Vec<_> = values.into_iter().collect();
2074        let values_iter = std::iter::repeat(values.iter())
2075            .flatten()
2076            .copied()
2077            .take(num_rows);
2078
2079        let mut builder = StringViewBuilder::with_capacity(100).with_fixed_block_size(8192);
2080        for val in values_iter {
2081            builder.append_option(val);
2082        }
2083
2084        let array = builder.finish();
2085        RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(array)]).unwrap()
2086    }
2087
2088    /// Return a RecordBatch of 100 rows
2089    fn multi_column_batch(range: Range<i32>) -> RecordBatch {
2090        let int64_array = Int64Array::from_iter(
2091            range
2092                .clone()
2093                .map(|v| if v % 5 == 0 { None } else { Some(v as i64) }),
2094        );
2095        let string_view_array = StringViewArray::from_iter(range.clone().map(|v| {
2096            if v % 5 == 0 {
2097                None
2098            } else if v % 7 == 0 {
2099                Some(format!("This is a string longer than 12 bytes{v}"))
2100            } else {
2101                Some(format!("Short {v}"))
2102            }
2103        }));
2104        let string_array = StringArray::from_iter(range.clone().map(|v| {
2105            if v % 11 == 0 {
2106                None
2107            } else {
2108                Some(format!("Value {v}"))
2109            }
2110        }));
2111        let timestamp_array = TimestampNanosecondArray::from_iter(range.map(|v| {
2112            if v % 3 == 0 {
2113                None
2114            } else {
2115                Some(v as i64 * 1000) // simulate a timestamp in milliseconds
2116            }
2117        }))
2118        .with_timezone("America/New_York");
2119
2120        RecordBatch::try_from_iter(vec![
2121            ("int64", Arc::new(int64_array) as ArrayRef),
2122            ("stringview", Arc::new(string_view_array) as ArrayRef),
2123            ("string", Arc::new(string_array) as ArrayRef),
2124            ("timestamp", Arc::new(timestamp_array) as ArrayRef),
2125        ])
2126        .unwrap()
2127    }
2128
2129    /// Return a boolean array that filters out randomly selected rows
2130    /// from the input batch with a `selectivity`.
2131    ///
2132    /// For example a `selectivity` of 0.1 will filter out
2133    /// 90% of the rows.
2134    #[derive(Debug)]
2135    struct RandomFilterBuilder {
2136        /// Number of rows to add to each filter
2137        num_rows: usize,
2138        /// selectivity of the filter (between 0.0 and 1.0)
2139        /// 0 selects no rows, 1.0 selects all rows
2140        selectivity: f64,
2141        /// seed for random number generator, increases by one each time
2142        /// `next_filter` is called
2143        seed: u64,
2144    }
2145    impl RandomFilterBuilder {
2146        /// Build the next filter with the current seed and increment the seed
2147        /// by one.
2148        fn next_filter(&mut self) -> BooleanArray {
2149            assert!(self.selectivity >= 0.0 && self.selectivity <= 1.0);
2150            let mut rng = rand::rngs::StdRng::seed_from_u64(self.seed);
2151            self.seed += 1;
2152            BooleanArray::from_iter(
2153                (0..self.num_rows)
2154                    .map(|_| rng.random_bool(self.selectivity))
2155                    .map(Some),
2156            )
2157        }
2158    }
2159
2160    /// Returns the named column as a StringViewArray
2161    fn col_as_string_view<'b>(name: &str, batch: &'b RecordBatch) -> &'b StringViewArray {
2162        batch
2163            .column_by_name(name)
2164            .expect("column not found")
2165            .as_string_view_opt()
2166            .expect("column is not a string view")
2167    }
2168
2169    /// Filter that selects 12.5% of the rows (`idx % 8 == 0`)
2170    fn sparse_filter(len: usize) -> BooleanArray {
2171        BooleanArray::from_iter((0..len).map(|idx| Some(idx % 8 == 0)))
2172    }
2173
2174    /// Filter that selects 5% of the rows (`idx % 20 == 0`)
2175    ///
2176    /// Different code paths are taken for very sparse filters
2177    fn very_sparse_filter(len: usize) -> BooleanArray {
2178        BooleanArray::from_iter((0..len).map(|idx| Some(idx % 20 == 0)))
2179    }
2180
2181    /// Normalize the `RecordBatch` so that the memory layout is consistent
2182    /// (e.g. StringArray is compacted).
2183    fn normalize_batch(batch: RecordBatch) -> RecordBatch {
2184        // Only need to normalize StringViews (as == also tests for memory layout)
2185        let (schema, mut columns, row_count) = batch.into_parts();
2186
2187        for column in &mut columns {
2188            if let Some(string_view) = column.as_string_view_opt() {
2189                // Re-create the StringViewArray to ensure memory layout is
2190                // consistent
2191                let mut builder = StringViewBuilder::new();
2192                for s in string_view {
2193                    builder.append_option(s);
2194                }
2195                *column = Arc::new(builder.finish());
2196                continue;
2197            }
2198
2199            if let Some(binary_view) = column.as_binary_view_opt() {
2200                *column = Arc::new(BinaryViewArray::from_iter(binary_view.iter()));
2201            }
2202        }
2203
2204        let options = RecordBatchOptions::new().with_row_count(Some(row_count));
2205        RecordBatch::try_new_with_options(schema, columns, &options).unwrap()
2206    }
2207
2208    /// Helper function to create a test batch with specified number of rows
2209    fn create_test_batch(num_rows: usize) -> RecordBatch {
2210        let schema = Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)]));
2211        let array = Int32Array::from_iter_values(0..num_rows as i32);
2212        RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap()
2213    }
2214    #[test]
2215    fn test_biggest_coalesce_batch_size_none_default() {
2216        // Test that default behavior (None) coalesces all batches
2217        let mut coalescer = BatchCoalescer::new(
2218            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2219            100,
2220        );
2221
2222        // Push a large batch (1000 rows) - should be coalesced normally
2223        let large_batch = create_test_batch(1000);
2224        coalescer.push_batch(large_batch).unwrap();
2225
2226        // Should produce multiple batches of target size (100)
2227        let mut output_batches = vec![];
2228        while let Some(batch) = coalescer.next_completed_batch() {
2229            output_batches.push(batch);
2230        }
2231
2232        coalescer.finish_buffered_batch().unwrap();
2233        while let Some(batch) = coalescer.next_completed_batch() {
2234            output_batches.push(batch);
2235        }
2236
2237        // Should have 10 batches of 100 rows each
2238        assert_eq!(output_batches.len(), 10);
2239        for batch in output_batches {
2240            assert_eq!(batch.num_rows(), 100);
2241        }
2242    }
2243
2244    #[test]
2245    fn test_biggest_coalesce_batch_size_bypass_large_batch() {
2246        // Test that batches larger than biggest_coalesce_batch_size bypass coalescing
2247        let mut coalescer = BatchCoalescer::new(
2248            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2249            100,
2250        );
2251        coalescer.set_biggest_coalesce_batch_size(Some(500));
2252
2253        // Push a large batch (1000 rows) - should bypass coalescing
2254        let large_batch = create_test_batch(1000);
2255        coalescer.push_batch(large_batch.clone()).unwrap();
2256
2257        // Should have one completed batch immediately (the original large batch)
2258        assert!(coalescer.has_completed_batch());
2259        let output_batch = coalescer.next_completed_batch().unwrap();
2260        assert_eq!(output_batch.num_rows(), 1000);
2261
2262        // Should be no more completed batches
2263        assert!(!coalescer.has_completed_batch());
2264        assert_eq!(coalescer.get_buffered_rows(), 0);
2265    }
2266
2267    #[test]
2268    fn test_biggest_coalesce_batch_size_coalesce_small_batch() {
2269        // Test that batches smaller than biggest_coalesce_batch_size are coalesced normally
2270        let mut coalescer = BatchCoalescer::new(
2271            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2272            100,
2273        )
2274        .with_biggest_coalesce_batch_size(Some(500));
2275
2276        // Push small batches that should be coalesced
2277        let small_batch = create_test_batch(50);
2278        coalescer.push_batch(small_batch.clone()).unwrap();
2279
2280        // Should not have completed batch yet (only 50 rows, target is 100)
2281        assert!(!coalescer.has_completed_batch());
2282        assert_eq!(coalescer.get_buffered_rows(), 50);
2283
2284        // Push another small batch
2285        coalescer.push_batch(small_batch).unwrap();
2286
2287        // Now should have a completed batch (100 rows total)
2288        assert!(coalescer.has_completed_batch());
2289        let output_batch = coalescer.next_completed_batch().unwrap();
2290        let size = output_batch
2291            .column(0)
2292            .as_primitive::<Int32Type>()
2293            .get_buffer_memory_size();
2294        assert_eq!(size, 400); // 100 rows * 4 bytes each
2295        assert_eq!(output_batch.num_rows(), 100);
2296
2297        assert_eq!(coalescer.get_buffered_rows(), 0);
2298    }
2299
2300    #[test]
2301    fn test_biggest_coalesce_batch_size_equal_boundary() {
2302        // Test behavior when batch size equals biggest_coalesce_batch_size
2303        let mut coalescer = BatchCoalescer::new(
2304            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2305            100,
2306        );
2307        coalescer.set_biggest_coalesce_batch_size(Some(500));
2308
2309        // Push a batch exactly equal to the limit
2310        let boundary_batch = create_test_batch(500);
2311        coalescer.push_batch(boundary_batch).unwrap();
2312
2313        // Should be coalesced (not bypass) since it's equal, not greater
2314        let mut output_count = 0;
2315        while coalescer.next_completed_batch().is_some() {
2316            output_count += 1;
2317        }
2318
2319        coalescer.finish_buffered_batch().unwrap();
2320        while coalescer.next_completed_batch().is_some() {
2321            output_count += 1;
2322        }
2323
2324        // Should have 5 batches of 100 rows each
2325        assert_eq!(output_count, 5);
2326    }
2327
2328    #[test]
2329    fn test_biggest_coalesce_batch_size_first_large_then_consecutive_bypass() {
2330        // Test the new consecutive large batch bypass behavior
2331        // Pattern: small batches -> first large batch (coalesced) -> consecutive large batches (bypass)
2332        let mut coalescer = BatchCoalescer::new(
2333            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2334            100,
2335        );
2336        coalescer.set_biggest_coalesce_batch_size(Some(200));
2337
2338        let small_batch = create_test_batch(50);
2339
2340        // Push small batch first to create buffered data
2341        coalescer.push_batch(small_batch).unwrap();
2342        assert_eq!(coalescer.get_buffered_rows(), 50);
2343        assert!(!coalescer.has_completed_batch());
2344
2345        // Push first large batch - should go through normal coalescing due to buffered data
2346        let large_batch1 = create_test_batch(250);
2347        coalescer.push_batch(large_batch1).unwrap();
2348
2349        // 50 + 250 = 300 -> 3 complete batches of 100, 0 rows buffered
2350        let mut completed_batches = vec![];
2351        while let Some(batch) = coalescer.next_completed_batch() {
2352            completed_batches.push(batch);
2353        }
2354        assert_eq!(completed_batches.len(), 3);
2355        assert_eq!(coalescer.get_buffered_rows(), 0);
2356
2357        // Now push consecutive large batches - they should bypass
2358        let large_batch2 = create_test_batch(300);
2359        let large_batch3 = create_test_batch(400);
2360
2361        // Push second large batch - should bypass since it's consecutive and buffer is empty
2362        coalescer.push_batch(large_batch2).unwrap();
2363        assert!(coalescer.has_completed_batch());
2364        let output = coalescer.next_completed_batch().unwrap();
2365        assert_eq!(output.num_rows(), 300); // bypassed with original size
2366        assert_eq!(coalescer.get_buffered_rows(), 0);
2367
2368        // Push third large batch - should also bypass
2369        coalescer.push_batch(large_batch3).unwrap();
2370        assert!(coalescer.has_completed_batch());
2371        let output = coalescer.next_completed_batch().unwrap();
2372        assert_eq!(output.num_rows(), 400); // bypassed with original size
2373        assert_eq!(coalescer.get_buffered_rows(), 0);
2374    }
2375
2376    #[test]
2377    fn test_biggest_coalesce_batch_size_empty_batch() {
2378        // Test that empty batches don't trigger the bypass logic
2379        let mut coalescer = BatchCoalescer::new(
2380            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2381            100,
2382        );
2383        coalescer.set_biggest_coalesce_batch_size(Some(50));
2384
2385        let empty_batch = create_test_batch(0);
2386        coalescer.push_batch(empty_batch).unwrap();
2387
2388        // Empty batch should be handled normally (no effect)
2389        assert!(!coalescer.has_completed_batch());
2390        assert_eq!(coalescer.get_buffered_rows(), 0);
2391    }
2392
2393    #[test]
2394    fn test_biggest_coalesce_batch_size_with_buffered_data_no_bypass() {
2395        // Test that when there is buffered data, large batches do NOT bypass (unless consecutive)
2396        let mut coalescer = BatchCoalescer::new(
2397            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2398            100,
2399        );
2400        coalescer.set_biggest_coalesce_batch_size(Some(200));
2401
2402        // Add some buffered data first
2403        let small_batch = create_test_batch(30);
2404        coalescer.push_batch(small_batch.clone()).unwrap();
2405        coalescer.push_batch(small_batch).unwrap();
2406        assert_eq!(coalescer.get_buffered_rows(), 60);
2407
2408        // Push large batch that would normally bypass, but shouldn't because buffered_rows > 0
2409        let large_batch = create_test_batch(250);
2410        coalescer.push_batch(large_batch).unwrap();
2411
2412        // The large batch should be processed through normal coalescing logic
2413        // Total: 60 (buffered) + 250 (new) = 310 rows
2414        // Output: 3 complete batches of 100 rows each, 10 rows remain buffered
2415
2416        let mut completed_batches = vec![];
2417        while let Some(batch) = coalescer.next_completed_batch() {
2418            completed_batches.push(batch);
2419        }
2420
2421        assert_eq!(completed_batches.len(), 3);
2422        for batch in &completed_batches {
2423            assert_eq!(batch.num_rows(), 100);
2424        }
2425        assert_eq!(coalescer.get_buffered_rows(), 10);
2426    }
2427
2428    #[test]
2429    fn test_biggest_coalesce_batch_size_zero_limit() {
2430        // Test edge case where limit is 0 (all batches bypass when no buffered data)
2431        let mut coalescer = BatchCoalescer::new(
2432            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2433            100,
2434        );
2435        coalescer.set_biggest_coalesce_batch_size(Some(0));
2436
2437        // Even a 1-row batch should bypass when there's no buffered data
2438        let tiny_batch = create_test_batch(1);
2439        coalescer.push_batch(tiny_batch).unwrap();
2440
2441        assert!(coalescer.has_completed_batch());
2442        let output = coalescer.next_completed_batch().unwrap();
2443        assert_eq!(output.num_rows(), 1);
2444    }
2445
2446    #[test]
2447    fn test_biggest_coalesce_batch_size_bypass_only_when_no_buffer() {
2448        // Test that bypass only occurs when buffered_rows == 0
2449        let mut coalescer = BatchCoalescer::new(
2450            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2451            100,
2452        );
2453        coalescer.set_biggest_coalesce_batch_size(Some(200));
2454
2455        // First, push a large batch with no buffered data - should bypass
2456        let large_batch = create_test_batch(300);
2457        coalescer.push_batch(large_batch.clone()).unwrap();
2458
2459        assert!(coalescer.has_completed_batch());
2460        let output = coalescer.next_completed_batch().unwrap();
2461        assert_eq!(output.num_rows(), 300); // bypassed
2462        assert_eq!(coalescer.get_buffered_rows(), 0);
2463
2464        // Now add some buffered data
2465        let small_batch = create_test_batch(50);
2466        coalescer.push_batch(small_batch).unwrap();
2467        assert_eq!(coalescer.get_buffered_rows(), 50);
2468
2469        // Push the same large batch again - should NOT bypass this time (not consecutive)
2470        coalescer.push_batch(large_batch).unwrap();
2471
2472        // Should process through normal coalescing: 50 + 300 = 350 rows
2473        // Output: 3 complete batches of 100 rows, 50 rows buffered
2474        let mut completed_batches = vec![];
2475        while let Some(batch) = coalescer.next_completed_batch() {
2476            completed_batches.push(batch);
2477        }
2478
2479        assert_eq!(completed_batches.len(), 3);
2480        for batch in &completed_batches {
2481            assert_eq!(batch.num_rows(), 100);
2482        }
2483        assert_eq!(coalescer.get_buffered_rows(), 50);
2484    }
2485
2486    #[test]
2487    fn test_biggest_coalesce_batch_size_consecutive_large_batches_scenario() {
2488        // Test your exact scenario: 20, 20, 30, 700, 600, 700, 900, 700, 600
2489        let mut coalescer = BatchCoalescer::new(
2490            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2491            1000,
2492        );
2493        coalescer.set_biggest_coalesce_batch_size(Some(500));
2494
2495        // Push small batches first
2496        coalescer.push_batch(create_test_batch(20)).unwrap();
2497        coalescer.push_batch(create_test_batch(20)).unwrap();
2498        coalescer.push_batch(create_test_batch(30)).unwrap();
2499
2500        assert_eq!(coalescer.get_buffered_rows(), 70);
2501        assert!(!coalescer.has_completed_batch());
2502
2503        // Push first large batch (700) - should coalesce due to buffered data
2504        coalescer.push_batch(create_test_batch(700)).unwrap();
2505
2506        // 70 + 700 = 770 rows, not enough for 1000, so all stay buffered
2507        assert_eq!(coalescer.get_buffered_rows(), 770);
2508        assert!(!coalescer.has_completed_batch());
2509
2510        // Push second large batch (600) - should bypass since previous was large
2511        coalescer.push_batch(create_test_batch(600)).unwrap();
2512
2513        // Should flush buffer (770 rows) and bypass the 600
2514        let mut outputs = vec![];
2515        while let Some(batch) = coalescer.next_completed_batch() {
2516            outputs.push(batch);
2517        }
2518        assert_eq!(outputs.len(), 2); // one flushed buffer batch (770) + one bypassed (600)
2519        assert_eq!(outputs[0].num_rows(), 770);
2520        assert_eq!(outputs[1].num_rows(), 600);
2521        assert_eq!(coalescer.get_buffered_rows(), 0);
2522
2523        // Push remaining large batches - should all bypass
2524        let remaining_batches = [700, 900, 700, 600];
2525        for &size in &remaining_batches {
2526            coalescer.push_batch(create_test_batch(size)).unwrap();
2527
2528            assert!(coalescer.has_completed_batch());
2529            let output = coalescer.next_completed_batch().unwrap();
2530            assert_eq!(output.num_rows(), size);
2531            assert_eq!(coalescer.get_buffered_rows(), 0);
2532        }
2533    }
2534
2535    #[test]
2536    fn test_biggest_coalesce_batch_size_truly_consecutive_large_bypass() {
2537        // Test truly consecutive large batches that should all bypass
2538        // This test ensures buffer is completely empty between large batches
2539        let mut coalescer = BatchCoalescer::new(
2540            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2541            100,
2542        );
2543        coalescer.set_biggest_coalesce_batch_size(Some(200));
2544
2545        // Push consecutive large batches with no prior buffered data
2546        let large_batches = vec![
2547            create_test_batch(300),
2548            create_test_batch(400),
2549            create_test_batch(350),
2550            create_test_batch(500),
2551        ];
2552
2553        let mut all_outputs = vec![];
2554
2555        for (i, large_batch) in large_batches.into_iter().enumerate() {
2556            let expected_size = large_batch.num_rows();
2557
2558            // Buffer should be empty before each large batch
2559            assert_eq!(
2560                coalescer.get_buffered_rows(),
2561                0,
2562                "Buffer should be empty before batch {i}"
2563            );
2564
2565            coalescer.push_batch(large_batch).unwrap();
2566
2567            // Each large batch should bypass and produce exactly one output batch
2568            assert!(
2569                coalescer.has_completed_batch(),
2570                "Should have completed batch after pushing batch {i}"
2571            );
2572
2573            let output = coalescer.next_completed_batch().unwrap();
2574            assert_eq!(
2575                output.num_rows(),
2576                expected_size,
2577                "Batch {i} should have bypassed with original size"
2578            );
2579
2580            // Should be no more batches and buffer should be empty
2581            assert!(
2582                !coalescer.has_completed_batch(),
2583                "Should have no more completed batches after batch {i}"
2584            );
2585            assert_eq!(
2586                coalescer.get_buffered_rows(),
2587                0,
2588                "Buffer should be empty after batch {i}"
2589            );
2590
2591            all_outputs.push(output);
2592        }
2593
2594        // Verify we got exactly 4 output batches with original sizes
2595        assert_eq!(all_outputs.len(), 4);
2596        assert_eq!(all_outputs[0].num_rows(), 300);
2597        assert_eq!(all_outputs[1].num_rows(), 400);
2598        assert_eq!(all_outputs[2].num_rows(), 350);
2599        assert_eq!(all_outputs[3].num_rows(), 500);
2600    }
2601
2602    #[test]
2603    fn test_biggest_coalesce_batch_size_reset_consecutive_on_small_batch() {
2604        // Test that small batches reset the consecutive large batch tracking
2605        let mut coalescer = BatchCoalescer::new(
2606            Arc::new(Schema::new(vec![Field::new("c0", DataType::Int32, false)])),
2607            100,
2608        );
2609        coalescer.set_biggest_coalesce_batch_size(Some(200));
2610
2611        // Push first large batch - should bypass (no buffered data)
2612        coalescer.push_batch(create_test_batch(300)).unwrap();
2613        let output = coalescer.next_completed_batch().unwrap();
2614        assert_eq!(output.num_rows(), 300);
2615
2616        // Push second large batch - should bypass (consecutive)
2617        coalescer.push_batch(create_test_batch(400)).unwrap();
2618        let output = coalescer.next_completed_batch().unwrap();
2619        assert_eq!(output.num_rows(), 400);
2620
2621        // Push small batch - resets consecutive tracking
2622        coalescer.push_batch(create_test_batch(50)).unwrap();
2623        assert_eq!(coalescer.get_buffered_rows(), 50);
2624
2625        // Push large batch again - should NOT bypass due to buffered data
2626        coalescer.push_batch(create_test_batch(350)).unwrap();
2627
2628        // Should coalesce: 50 + 350 = 400 -> 4 complete batches of 100
2629        let mut outputs = vec![];
2630        while let Some(batch) = coalescer.next_completed_batch() {
2631            outputs.push(batch);
2632        }
2633        assert_eq!(outputs.len(), 4);
2634        for batch in outputs {
2635            assert_eq!(batch.num_rows(), 100);
2636        }
2637        assert_eq!(coalescer.get_buffered_rows(), 0);
2638    }
2639
2640    #[test]
2641    fn test_coalasce_push_batch_with_indices() {
2642        const MID_POINT: u32 = 2333;
2643        const TOTAL_ROWS: u32 = 23333;
2644        let batch1 = uint32_batch_non_null(0..MID_POINT);
2645        let batch2 = uint32_batch_non_null((MID_POINT..TOTAL_ROWS).rev());
2646
2647        let mut coalescer = BatchCoalescer::new(
2648            Arc::new(Schema::new(vec![Field::new("c0", DataType::UInt32, false)])),
2649            TOTAL_ROWS as usize,
2650        );
2651        coalescer.push_batch(batch1).unwrap();
2652
2653        let rev_indices = (0..((TOTAL_ROWS - MID_POINT) as u64)).rev();
2654        let reversed_indices_batch = uint64_batch_non_null(rev_indices);
2655
2656        let reverse_indices = UInt64Array::from(reversed_indices_batch.column(0).to_data());
2657        coalescer
2658            .push_batch_with_indices(batch2, &reverse_indices)
2659            .unwrap();
2660
2661        coalescer.finish_buffered_batch().unwrap();
2662        let actual = coalescer.next_completed_batch().unwrap();
2663
2664        let expected = uint32_batch_non_null(0..TOTAL_ROWS);
2665
2666        assert_eq!(expected, actual);
2667    }
2668
2669    #[test]
2670    fn test_push_batch_schema_mismatch_fewer_columns() {
2671        // Coalescer expects 0 columns, batch has 1
2672        let empty_schema = Arc::new(Schema::empty());
2673        let mut coalescer = BatchCoalescer::new(empty_schema, 100);
2674        let batch = uint32_batch(0..5);
2675        let result = coalescer.push_batch(batch);
2676        assert!(result.is_err());
2677        let err = result.unwrap_err().to_string();
2678        assert!(
2679            err.contains("Batch has 1 columns but BatchCoalescer expects 0"),
2680            "unexpected error: {err}"
2681        );
2682    }
2683
2684    #[test]
2685    fn test_push_batch_schema_mismatch_more_columns() {
2686        // Coalescer expects 2 columns, batch has 1
2687        let schema = Arc::new(Schema::new(vec![
2688            Field::new("c0", DataType::UInt32, false),
2689            Field::new("c1", DataType::UInt32, false),
2690        ]));
2691        let mut coalescer = BatchCoalescer::new(schema, 100);
2692        let batch = uint32_batch(0..5);
2693        let result = coalescer.push_batch(batch);
2694        assert!(result.is_err());
2695        let err = result.unwrap_err().to_string();
2696        assert!(
2697            err.contains("Batch has 1 columns but BatchCoalescer expects 2"),
2698            "unexpected error: {err}"
2699        );
2700    }
2701
2702    #[test]
2703    fn test_push_batch_schema_mismatch_two_vs_zero() {
2704        // Coalescer expects 0 columns, batch has 2
2705        let empty_schema = Arc::new(Schema::empty());
2706        let mut coalescer = BatchCoalescer::new(empty_schema, 100);
2707        let schema = Arc::new(Schema::new(vec![
2708            Field::new("c0", DataType::UInt32, false),
2709            Field::new("c1", DataType::UInt32, false),
2710        ]));
2711        let batch = RecordBatch::try_new(
2712            schema,
2713            vec![
2714                Arc::new(UInt32Array::from(vec![1, 2, 3])),
2715                Arc::new(UInt32Array::from(vec![4, 5, 6])),
2716            ],
2717        )
2718        .unwrap();
2719        let result = coalescer.push_batch(batch);
2720        assert!(result.is_err());
2721        let err = result.unwrap_err().to_string();
2722        assert!(
2723            err.contains("Batch has 2 columns but BatchCoalescer expects 0"),
2724            "unexpected error: {err}"
2725        );
2726    }
2727
2728    #[test]
2729    fn test_size_grows_with_buffering_and_shrinks_when_draining() {
2730        let batch = uint32_batch(0..8);
2731        let mut coalescer = BatchCoalescer::new(batch.schema(), 21);
2732        let baseline = coalescer.size();
2733        assert!(baseline > 0, "size includes container capacities");
2734
2735        // Buffer rows without completing a batch
2736        coalescer.push_batch(batch.clone()).unwrap();
2737        assert!(coalescer.next_completed_batch().is_none());
2738        let buffered = coalescer.size();
2739        assert!(
2740            buffered > baseline,
2741            "buffering rows should grow size ({buffered} > {baseline})"
2742        );
2743
2744        // Push enough to complete several batches
2745        for _ in 0..10 {
2746            coalescer.push_batch(batch.clone()).unwrap();
2747        }
2748        let peak = coalescer.size();
2749        assert!(peak > buffered);
2750
2751        // Draining completed batches must never grow the reported size
2752        let mut prev = peak;
2753        let mut drained_any = false;
2754        while coalescer.next_completed_batch().is_some() {
2755            drained_any = true;
2756            let now = coalescer.size();
2757            assert!(now <= prev, "size grew while draining: {now} > {prev}");
2758            prev = now;
2759        }
2760        assert!(drained_any);
2761        assert!(
2762            prev < peak,
2763            "draining completed batches should release memory"
2764        );
2765    }
2766
2767    #[test]
2768    fn test_size_string_view_buffers_released_after_drain() {
2769        // Long strings spill into external data buffers, exercising the byte-view
2770        // size accounting through the real coalescer path (including compaction).
2771        let batch = stringview_batch_repeated(
2772            1000,
2773            [Some("this string is definitely longer than 12 bytes")],
2774        );
2775        let mut coalescer = BatchCoalescer::new(batch.schema(), 4096);
2776        let baseline = coalescer.size();
2777
2778        for _ in 0..20 {
2779            coalescer.push_batch(batch.clone()).unwrap();
2780        }
2781        let peak = coalescer.size();
2782        assert!(
2783            peak > baseline,
2784            "buffered string view data should grow size ({peak} > {baseline})"
2785        );
2786
2787        coalescer.finish_buffered_batch().unwrap();
2788        while coalescer.next_completed_batch().is_some() {}
2789
2790        // Once fully drained the in-progress byte-view buffers are released.
2791        let drained = coalescer.size();
2792        assert!(
2793            drained < peak,
2794            "draining should release buffered data ({drained} < {peak})"
2795        );
2796    }
2797
2798    /// Every byte added to the accounting must eventually be removed: running the
2799    /// exact same push/finish/drain sequence twice must report identical sizes.
2800    /// This catches accounting leaks and drift without hard-coding magic numbers.
2801    #[test]
2802    fn test_size_accounting_conserved_across_cycles() {
2803        // Primitive column: internal capacities stabilize after the first cycle
2804        // (unlike byte-view, whose buffer sizer keeps growing), so the readings
2805        // are deterministic across cycles.
2806        let batch = uint32_batch(0..8);
2807        let mut coalescer = BatchCoalescer::new(batch.schema(), 4096);
2808
2809        let run_cycle = |coalescer: &mut BatchCoalescer| {
2810            for _ in 0..20 {
2811                coalescer.push_batch(batch.clone()).unwrap();
2812            }
2813            coalescer.finish_buffered_batch().unwrap();
2814            let peak = coalescer.size();
2815            while coalescer.next_completed_batch().is_some() {}
2816            (peak, coalescer.size())
2817        };
2818
2819        let (peak1, drained1) = run_cycle(&mut coalescer);
2820        let (peak2, drained2) = run_cycle(&mut coalescer);
2821
2822        assert_eq!(
2823            peak1, peak2,
2824            "identical work must report identical peak size"
2825        );
2826        assert_eq!(
2827            drained1, drained2,
2828            "fully-drained size must be stable across cycles (no accounting leak)"
2829        );
2830        assert!(
2831            drained1 < peak1,
2832            "draining must release the accounted memory"
2833        );
2834    }
2835}