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