Skip to main content

arrow_select/
coalesce.rs

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