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