This is an automated email from the ASF dual-hosted git repository.

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new f9bf62845c feat(coalesce): add size function (#10331)
f9bf62845c is described below

commit f9bf62845ca459c16938359e9378b34a4d8c51d9
Author: Raz Luvaton <[email protected]>
AuthorDate: Fri Jul 24 05:09:06 2026 +0300

    feat(coalesce): add size function (#10331)
    
    # Which issue does this PR close?
    
    N/A
    
    But this is prerequisite for:
    - https://github.com/apache/datafusion/issues/23385
    
    # Rationale for this change
    
    We want to be able to reserve memory for the data that `BatchCoalescer`
    is using in DataFusion and other places
    
    # What changes are included in this PR?
    
    added `size` functions and implement sizing + tests
    
    # Are these changes tested?
    
    Yes
    
    # Are there any user-facing changes?
    yes, new function
    
    ---------
    
    Co-authored-by: Mikhail Zabaluev <[email protected]>
---
 arrow-select/src/coalesce.rs           | 127 +++++++++++++++++++++++++++++
 arrow-select/src/coalesce/byte_view.rs | 145 +++++++++++++++++++++++++++++++--
 arrow-select/src/coalesce/generic.rs   | 139 ++++++++++++++++++++++++++++++-
 arrow-select/src/coalesce/primitive.rs |  77 +++++++++++++++++
 4 files changed, 478 insertions(+), 10 deletions(-)

diff --git a/arrow-select/src/coalesce.rs b/arrow-select/src/coalesce.rs
index 0f1086be82..ad1fe2ab92 100644
--- a/arrow-select/src/coalesce.rs
+++ b/arrow-select/src/coalesce.rs
@@ -583,6 +583,22 @@ impl BatchCoalescer {
     pub fn next_completed_batch(&mut self) -> Option<RecordBatch> {
         self.completed.pop_front()
     }
+
+    /// Returns the number of bytes used by this data structure.
+    pub fn size(&self) -> usize {
+        self.in_progress_arrays.capacity() * size_of::<Box<dyn 
InProgressArray>>()
+            + self
+                .in_progress_arrays
+                .iter()
+                .map(|array| array.size())
+                .sum::<usize>()
+            + self.completed.capacity() * size_of::<RecordBatch>()
+            + self
+                .completed
+                .iter()
+                .map(|batch| batch.get_array_memory_size())
+                .sum::<usize>()
+    }
 }
 
 impl BatchCoalescer {
@@ -742,6 +758,9 @@ trait InProgressArray: std::fmt::Debug + Send + Sync {
 
     /// Finish the currently in-progress array and return it as an `ArrayRef`
     fn finish(&mut self) -> Result<ArrayRef, ArrowError>;
+
+    /// Get the number of bytes this array is using
+    fn size(&self) -> usize;
 }
 
 #[cfg(test)]
@@ -2683,4 +2702,112 @@ mod tests {
             "unexpected error: {err}"
         );
     }
+
+    #[test]
+    fn test_size_grows_with_buffering_and_shrinks_when_draining() {
+        let batch = uint32_batch(0..8);
+        let mut coalescer = BatchCoalescer::new(batch.schema(), 21);
+        let baseline = coalescer.size();
+        assert!(baseline > 0, "size includes container capacities");
+
+        // Buffer rows without completing a batch
+        coalescer.push_batch(batch.clone()).unwrap();
+        assert!(coalescer.next_completed_batch().is_none());
+        let buffered = coalescer.size();
+        assert!(
+            buffered > baseline,
+            "buffering rows should grow size ({buffered} > {baseline})"
+        );
+
+        // Push enough to complete several batches
+        for _ in 0..10 {
+            coalescer.push_batch(batch.clone()).unwrap();
+        }
+        let peak = coalescer.size();
+        assert!(peak > buffered);
+
+        // Draining completed batches must never grow the reported size
+        let mut prev = peak;
+        let mut drained_any = false;
+        while coalescer.next_completed_batch().is_some() {
+            drained_any = true;
+            let now = coalescer.size();
+            assert!(now <= prev, "size grew while draining: {now} > {prev}");
+            prev = now;
+        }
+        assert!(drained_any);
+        assert!(
+            prev < peak,
+            "draining completed batches should release memory"
+        );
+    }
+
+    #[test]
+    fn test_size_string_view_buffers_released_after_drain() {
+        // Long strings spill into external data buffers, exercising the 
byte-view
+        // size accounting through the real coalescer path (including 
compaction).
+        let batch = stringview_batch_repeated(
+            1000,
+            [Some("this string is definitely longer than 12 bytes")],
+        );
+        let mut coalescer = BatchCoalescer::new(batch.schema(), 4096);
+        let baseline = coalescer.size();
+
+        for _ in 0..20 {
+            coalescer.push_batch(batch.clone()).unwrap();
+        }
+        let peak = coalescer.size();
+        assert!(
+            peak > baseline,
+            "buffered string view data should grow size ({peak} > {baseline})"
+        );
+
+        coalescer.finish_buffered_batch().unwrap();
+        while coalescer.next_completed_batch().is_some() {}
+
+        // Once fully drained the in-progress byte-view buffers are released.
+        let drained = coalescer.size();
+        assert!(
+            drained < peak,
+            "draining should release buffered data ({drained} < {peak})"
+        );
+    }
+
+    /// Every byte added to the accounting must eventually be removed: running 
the
+    /// exact same push/finish/drain sequence twice must report identical 
sizes.
+    /// This catches accounting leaks and drift without hard-coding magic 
numbers.
+    #[test]
+    fn test_size_accounting_conserved_across_cycles() {
+        // Primitive column: internal capacities stabilize after the first 
cycle
+        // (unlike byte-view, whose buffer sizer keeps growing), so the 
readings
+        // are deterministic across cycles.
+        let batch = uint32_batch(0..8);
+        let mut coalescer = BatchCoalescer::new(batch.schema(), 4096);
+
+        let run_cycle = |coalescer: &mut BatchCoalescer| {
+            for _ in 0..20 {
+                coalescer.push_batch(batch.clone()).unwrap();
+            }
+            coalescer.finish_buffered_batch().unwrap();
+            let peak = coalescer.size();
+            while coalescer.next_completed_batch().is_some() {}
+            (peak, coalescer.size())
+        };
+
+        let (peak1, drained1) = run_cycle(&mut coalescer);
+        let (peak2, drained2) = run_cycle(&mut coalescer);
+
+        assert_eq!(
+            peak1, peak2,
+            "identical work must report identical peak size"
+        );
+        assert_eq!(
+            drained1, drained2,
+            "fully-drained size must be stable across cycles (no accounting 
leak)"
+        );
+        assert!(
+            drained1 < peak1,
+            "draining must release the accounted memory"
+        );
+    }
 }
diff --git a/arrow-select/src/coalesce/byte_view.rs 
b/arrow-select/src/coalesce/byte_view.rs
index 32f92d044e..f43572b37c 100644
--- a/arrow-select/src/coalesce/byte_view.rs
+++ b/arrow-select/src/coalesce/byte_view.rs
@@ -54,6 +54,10 @@ pub(crate) struct InProgressByteViewArray<B: ByteViewType> {
     /// Phantom so we can use the same struct for both StringViewArray and
     /// BinaryViewArray
     _phantom: PhantomData<B>,
+    /// The size in bytes the [`Buffer`]s in [`Self::completed`] is taking
+    completed_buffers_size: usize,
+    /// The size in bytes from [`Self::source`] that it is being used in 
[`Self::completed`]
+    size_of_completed_buffers_from_current_source: usize,
 }
 
 struct Source {
@@ -89,6 +93,8 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
             nulls: NullBufferBuilder::new(batch_size), // no allocation
             current: None,
             completed: vec![],
+            completed_buffers_size: 0,
+            size_of_completed_buffers_from_current_source: 0,
             buffer_source,
             _phantom: PhantomData,
         }
@@ -109,7 +115,10 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
         let Some(next_buffer) = self.current.take() else {
             return;
         };
-        self.completed.push(next_buffer.into());
+        let buffer: Buffer = next_buffer.into();
+
+        self.completed_buffers_size += buffer.capacity();
+        self.completed.push(buffer);
     }
 
     fn append_views_by_filter(&mut self, views: &[u128], filter: 
&FilterPredicate) {
@@ -169,10 +178,26 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
 
     /// Append views to self.views, updating the buffer index if necessary
     #[inline(never)]
-    fn append_views_and_update_buffer_index(&mut self, views: &[u128], 
buffers: &[Buffer]) {
+    fn append_views_and_update_buffer_index(
+        &mut self,
+        views: &[u128],
+        buffers: &[Buffer],
+        is_reused: bool,
+    ) {
         if let Some(buffer) = self.current.take() {
-            self.completed.push(buffer.into());
+            let buffer: Buffer = buffer.into();
+            self.completed_buffers_size += buffer.capacity();
+            self.completed.push(buffer);
         }
+
+        let buffers_size = buffers.iter().map(|b| b.capacity()).sum::<usize>();
+        if !is_reused {
+            self.completed_buffers_size += buffers_size;
+        } else if self.size_of_completed_buffers_from_current_source == 0 {
+            // Don't double count buffers size if already counted that
+            self.size_of_completed_buffers_from_current_source += buffers_size;
+        }
+
         let starting_buffer: u32 = self.completed.len().try_into().expect("too 
many buffers");
         self.completed.extend_from_slice(buffers);
 
@@ -251,8 +276,9 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
         let remaining_view_buffer_size = view_buffer_size - 
string_bytes_to_copy;
 
         self.append_views_and_copy_strings_inner(first_views, current, 
buffers);
-        let completed = self.current.take().expect("completed");
-        self.completed.push(completed.into());
+        let completed: Buffer = self.current.take().expect("completed").into();
+        self.completed_buffers_size += completed.capacity();
+        self.completed.push(completed);
 
         // Copy any remaining views into a new buffer
         let remaining_views = &views[num_view_to_current..];
@@ -328,6 +354,7 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
             }
             b.as_u128()
         });
+
         self.views.extend(new_views);
         self.current = Some(dst_buffer);
     }
@@ -335,6 +362,10 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
 
 impl<B: ByteViewType> InProgressArray for InProgressByteViewArray<B> {
     fn set_source(&mut self, source: Option<ArrayRef>) {
+        // If used values from source, add only the size that was used
+        self.completed_buffers_size += 
self.size_of_completed_buffers_from_current_source;
+        self.size_of_completed_buffers_from_current_source = 0;
+
         self.source = source.map(|array| {
             let s = array.as_byte_view::<B>();
 
@@ -358,7 +389,7 @@ impl<B: ByteViewType> InProgressArray for 
InProgressByteViewArray<B> {
                 need_gc,
                 ideal_buffer_size,
             }
-        })
+        });
     }
 
     fn copy_rows(&mut self, offset: usize, len: usize) -> Result<(), 
ArrowError> {
@@ -397,7 +428,7 @@ impl<B: ByteViewType> InProgressArray for 
InProgressByteViewArray<B> {
         if source.need_gc {
             self.append_views_and_copy_strings(views, 
source.ideal_buffer_size, buffers);
         } else {
-            self.append_views_and_update_buffer_index(views, buffers);
+            self.append_views_and_update_buffer_index(views, buffers, true);
         }
         self.source = Some(source);
         Ok(())
@@ -452,7 +483,7 @@ impl<B: ByteViewType> InProgressArray for 
InProgressByteViewArray<B> {
         } else {
             self.nulls.append_n_non_nulls(filter.count());
         }
-        self.append_views_and_update_buffer_index(filtered.views(), 
filtered.data_buffers());
+        self.append_views_and_update_buffer_index(filtered.views(), 
filtered.data_buffers(), false);
         Ok(())
     }
 
@@ -464,12 +495,27 @@ impl<B: ByteViewType> InProgressArray for 
InProgressByteViewArray<B> {
         let nulls = self.nulls.finish();
         self.nulls = NullBufferBuilder::new(self.batch_size);
 
+        // Not reusing anything since we took all complete
+        self.size_of_completed_buffers_from_current_source = 0;
+        self.completed_buffers_size = 0;
+
         // Safety: we created valid views and buffers above and the
         // input arrays had value data and nulls
         let new_array =
             unsafe { GenericByteViewArray::<B>::new_unchecked(views.into(), 
buffers, nulls) };
         Ok(Arc::new(new_array))
     }
+
+    fn size(&self) -> usize {
+        self.completed_buffers_size
+            + self.current.as_ref().map_or(0, |c| c.capacity())
+            + self.nulls.allocated_size()
+            + self.views.capacity() * size_of::<u128>()
+            + self
+                .source
+                .as_ref()
+                .map_or(0, |s| s.array.get_array_memory_size())
+    }
 }
 
 const STARTING_BLOCK_SIZE: usize = 4 * 1024; // (note the first size used is 
actually 8KiB)
@@ -607,4 +653,87 @@ mod tests {
             "expected filtered output to reuse the source data buffer"
         );
     }
+
+    /// Build a compacted BinaryViewArray whose values all spill into external
+    /// data buffers. `gc()` makes the buffers dense so the coalescer reuses 
them
+    /// (`need_gc == false`) rather than copying/compacting them.
+    fn non_inline_array(n: usize) -> (BinaryViewArray, usize) {
+        let values = (0..n)
+            .map(|i| format!("This value is longer than 12 bytes: 
{i}").into_bytes())
+            .collect::<Vec<_>>();
+        let array = BinaryViewArray::from_iter(values.iter().map(|v| 
Some(v.as_slice()))).gc();
+        assert!(!array.data_buffers().is_empty());
+        let buffer_capacity = array.data_buffers().iter().map(|b| 
b.capacity()).sum();
+        (array, buffer_capacity)
+    }
+
+    #[test]
+    fn test_size_empty() {
+        let in_progress = InProgressByteViewArray::<BinaryViewType>::new(64);
+        assert_eq!(in_progress.size(), 0);
+    }
+
+    #[test]
+    fn test_size_reused_buffers_not_double_counted() {
+        let (array, buffer_capacity) = non_inline_array(64);
+        let source: ArrayRef = Arc::new(array);
+
+        let mut in_progress = 
InProgressByteViewArray::<BinaryViewType>::new(64);
+        in_progress.set_source(Some(Arc::clone(&source)));
+
+        in_progress.copy_rows(0, 60).unwrap();
+        in_progress.copy_rows(60, 4).unwrap();
+
+        // The reused buffers now live in `completed`, but while the source is
+        // still set they are counted via the source, not 
`completed_buffers_size`,
+        // to avoid double counting them.
+        assert_eq!(in_progress.completed_buffers_size, 0);
+        assert_eq!(
+            in_progress.size_of_completed_buffers_from_current_source,
+            buffer_capacity,
+        );
+
+        // Setting a new source commits the pending reused-buffer bytes, since 
the
+        // old source (and its double count) is dropped.
+        let other: ArrayRef = 
Arc::new(BinaryViewArray::from_iter(std::iter::once(Some(
+            b"short".as_slice(),
+        ))));
+        in_progress.set_source(Some(Arc::clone(&other)));
+        assert_eq!(in_progress.completed_buffers_size, buffer_capacity);
+        assert_eq!(in_progress.size_of_completed_buffers_from_current_source, 
0);
+
+        // finish() releases everything.
+        in_progress.finish().unwrap();
+        assert_eq!(in_progress.completed_buffers_size, 0);
+        assert_eq!(in_progress.size_of_completed_buffers_from_current_source, 
0);
+    }
+
+    #[test]
+    fn 
size_should_be_the_same_if_copying_multiple_time_from_same_source_or_once() {
+        let (array, _buffer_capacity) = non_inline_array(64);
+        let source: ArrayRef = Arc::new(array);
+
+        let in_progress_size_with_split = {
+            let mut in_progress = 
InProgressByteViewArray::<BinaryViewType>::new(64);
+            in_progress.set_source(Some(Arc::clone(&source)));
+
+            in_progress.copy_rows(0, 60).unwrap();
+            in_progress.copy_rows(60, 4).unwrap();
+            in_progress.set_source(None);
+
+            in_progress.size()
+        };
+
+        let in_progress_size_without_split = {
+            let mut in_progress = 
InProgressByteViewArray::<BinaryViewType>::new(64);
+            in_progress.set_source(Some(Arc::clone(&source)));
+
+            in_progress.copy_rows(0, 64).unwrap();
+            in_progress.set_source(None);
+
+            in_progress.size()
+        };
+
+        assert_eq!(in_progress_size_with_split, 
in_progress_size_without_split);
+    }
 }
diff --git a/arrow-select/src/coalesce/generic.rs 
b/arrow-select/src/coalesce/generic.rs
index 4fa64273ec..5e99fb1045 100644
--- a/arrow-select/src/coalesce/generic.rs
+++ b/arrow-select/src/coalesce/generic.rs
@@ -18,7 +18,7 @@
 use super::InProgressArray;
 use crate::concat::concat;
 use crate::filter::FilterPredicate;
-use arrow_array::ArrayRef;
+use arrow_array::{Array, ArrayRef};
 use arrow_schema::ArrowError;
 
 /// Generic implementation for [`InProgressArray`] that works with any type of
@@ -32,8 +32,14 @@ use arrow_schema::ArrowError;
 pub(crate) struct GenericInProgressArray {
     /// The current source
     source: Option<ArrayRef>,
+
+    /// Is [`Self::source`] referenced in [`Self::buffered_arrays`]
+    source_data_referenced_in_buffers: bool,
     /// The buffered array slices
     buffered_arrays: Vec<ArrayRef>,
+
+    /// The number of bytes the arrays in [`Self::buffered_arrays`] takes
+    total_size_of_non_shared_buffers: usize,
 }
 
 impl GenericInProgressArray {
@@ -42,12 +48,22 @@ impl GenericInProgressArray {
         Self {
             source: None,
             buffered_arrays: vec![],
+            total_size_of_non_shared_buffers: 0,
+            source_data_referenced_in_buffers: false,
         }
     }
 }
 impl InProgressArray for GenericInProgressArray {
     fn set_source(&mut self, source: Option<ArrayRef>) {
-        self.source = source
+        if let Some(old_source) = self.source.take() {
+            //  If the source is still referenced in buffered_arrays,
+            // then count it now
+            if self.source_data_referenced_in_buffers {
+                self.total_size_of_non_shared_buffers += 
old_source.get_array_memory_size();
+            }
+        }
+        self.source_data_referenced_in_buffers = false;
+        self.source = source;
     }
 
     fn copy_rows(&mut self, offset: usize, len: usize) -> Result<(), 
ArrowError> {
@@ -56,6 +72,9 @@ impl InProgressArray for GenericInProgressArray {
                 "Internal Error: GenericInProgressArray: source not 
set".to_string(),
             )
         })?;
+        // No need to count the size of the array that was pushed to 
buffered_arrays
+        // since the data is shared with the `source` memory that is already 
counted
+        self.source_data_referenced_in_buffers = true;
         let array = source.slice(offset, len);
         self.buffered_arrays.push(array);
         Ok(())
@@ -67,6 +86,7 @@ impl InProgressArray for GenericInProgressArray {
         filter: &FilterPredicate,
     ) -> Result<(), ArrowError> {
         let array = filter.filter(source.as_ref())?;
+        self.total_size_of_non_shared_buffers += array.get_array_memory_size();
         self.buffered_arrays.push(array);
         Ok(())
     }
@@ -82,6 +102,121 @@ impl InProgressArray for GenericInProgressArray {
                 .collect::<Vec<_>>(),
         )?;
         self.buffered_arrays.clear();
+        self.total_size_of_non_shared_buffers = 0;
+        self.source_data_referenced_in_buffers = false;
         Ok(array)
     }
+
+    fn size(&self) -> usize {
+        self.total_size_of_non_shared_buffers
+            + self.buffered_arrays.capacity() * size_of::<ArrayRef>()
+            + self
+                .source
+                .as_ref()
+                .map_or(0, |a| a.get_array_memory_size())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use arrow_array::Int32Array;
+    use std::sync::Arc;
+
+    fn arr(range: std::ops::Range<i32>) -> ArrayRef {
+        Arc::new(Int32Array::from_iter_values(range))
+    }
+
+    #[test]
+    fn test_size_empty() {
+        let in_progress = GenericInProgressArray::new();
+        assert_eq!(in_progress.size(), 0);
+    }
+
+    #[test]
+    fn test_source_is_counted_in_memory_and_released_when_not_used() {
+        let mut in_progress = GenericInProgressArray::new();
+
+        // Starting with no used memory
+        assert_eq!(in_progress.size(), 0);
+        {
+            let source1 = arr(0..100);
+            in_progress.set_source(Some(Arc::clone(&source1)));
+
+            // We are holding on source so this is the size
+            assert_eq!(in_progress.size(), source1.get_array_memory_size());
+        }
+
+        {
+            // Replacing an unused source drops the old size and counts only 
the new one
+            let source2 = arr(0..40);
+            in_progress.set_source(Some(Arc::clone(&source2)));
+            assert_eq!(in_progress.size(), source2.get_array_memory_size());
+        }
+
+        // Drop the used source
+        in_progress.set_source(None);
+
+        // The source is no longer being held, so it should reduce the size
+        assert_eq!(in_progress.size(), 0);
+    }
+
+    #[test]
+    fn test_double_copy_on_same_source_should_not_double_count() {
+        let mut in_progress = GenericInProgressArray::new();
+
+        // Starting with no used memory
+        assert_eq!(in_progress.size(), 0);
+
+        let source = arr(0..100);
+        in_progress.set_source(Some(Arc::clone(&source)));
+
+        // We are holding on source so this is the size
+        let size_before_copy = in_progress.size();
+        assert_eq!(size_before_copy, source.get_array_memory_size());
+
+        for _ in 0..2 {
+            // Only copy a subset
+            in_progress.copy_rows(0, 98).unwrap();
+
+            // The size should now account for the buffered but not the actual 
array data since we are still holding on it
+            assert!(
+                in_progress.size() > size_before_copy,
+                "size after copy {} should be greater than before copy 
{size_before_copy}",
+                in_progress.size()
+            );
+            {
+                let in_progress_size = in_progress.size() as f64;
+                let source_size = source.get_array_memory_size();
+                let size_if_source_and_sliced_would_be_counted = (source_size 
as f64) * 1.8;
+                assert!(
+                    in_progress_size < 
size_if_source_and_sliced_would_be_counted,
+                    "size after copy {in_progress_size} should not include the 
source and sliced array (should be greater than 
{size_if_source_and_sliced_would_be_counted}), source size is {source_size}"
+                );
+            }
+        }
+
+        let size_before_clear_source = in_progress.size();
+
+        // Drop the used source
+        in_progress.set_source(None);
+
+        // The source is still being held in the buffered
+        assert_eq!(in_progress.size(), size_before_clear_source);
+
+        {
+            let source2 = arr(0..40);
+            in_progress.set_source(Some(Arc::clone(&source2)));
+            assert_eq!(
+                in_progress.size(),
+                size_before_clear_source + source2.get_array_memory_size()
+            );
+            in_progress.set_source(None);
+        }
+
+        in_progress.finish().unwrap();
+
+        // There is still some memory being held by some leftover capacity but 
not arrays
+        assert!(in_progress.size() < source.get_array_memory_size());
+    }
 }
diff --git a/arrow-select/src/coalesce/primitive.rs 
b/arrow-select/src/coalesce/primitive.rs
index ac831fe089..21ddd956cc 100644
--- a/arrow-select/src/coalesce/primitive.rs
+++ b/arrow-select/src/coalesce/primitive.rs
@@ -226,6 +226,14 @@ impl<T: ArrowPrimitiveType + Debug> InProgressArray for 
InProgressPrimitiveArray
             .with_data_type(self.data_type.clone());
         Ok(Arc::new(array))
     }
+
+    fn size(&self) -> usize {
+        self.source
+            .as_ref()
+            .map_or(0, |source| source.get_array_memory_size())
+            + self.current.capacity() * std::mem::size_of::<T::Native>()
+            + self.nulls.allocated_size()
+    }
 }
 
 #[cfg(test)]
@@ -305,4 +313,73 @@ mod tests {
         ]);
         assert_eq!(result, &expected);
     }
+
+    #[test]
+    fn test_size_empty() {
+        // A fresh in-progress array has allocated nothing yet
+        let in_progress = InProgressPrimitiveArray::<Int32Type>::new(64, 
DataType::Int32);
+        assert_eq!(in_progress.size(), 0);
+    }
+
+    #[test]
+    fn test_size_counts_source() {
+        let mut in_progress = InProgressPrimitiveArray::<Int32Type>::new(64, 
DataType::Int32);
+        let source: ArrayRef = Arc::new(Int32Array::from_iter_values(0..100));
+        in_progress.set_source(Some(Arc::clone(&source)));
+        // Nothing copied yet, so size is exactly the source's memory
+        assert_eq!(in_progress.size(), source.get_array_memory_size());
+    }
+
+    #[test]
+    fn test_size_counts_values_buffer_and_resets_on_finish() {
+        const BATCH_SIZE: usize = 64;
+        let mut in_progress =
+            InProgressPrimitiveArray::<Int32Type>::new(BATCH_SIZE, 
DataType::Int32);
+        // Non-null source: the nulls builder stays empty (allocated_size == 
0),
+        // so the only growth is the values buffer.
+        let source: ArrayRef = Arc::new(Int32Array::from_iter_values(0..100));
+        let source_size = source.get_array_memory_size();
+        in_progress.set_source(Some(Arc::clone(&source)));
+
+        in_progress.copy_rows(0, 50).unwrap();
+        assert!(
+            in_progress.size() >= source_size + 50 * size_of::<i32>(),
+            "values buffer under-counted: {} < {} + {} * {}",
+            in_progress.size(),
+            source_size,
+            50,
+            size_of::<i32>(),
+        );
+
+        // finish() takes the buffered values/nulls but keeps the source, so 
the
+        // reported size drops back to exactly the source.
+        in_progress.finish().unwrap();
+        assert_eq!(in_progress.size(), source_size);
+    }
+
+    #[test]
+    fn test_size_counts_null_buffer() {
+        const BATCH_SIZE: usize = 64;
+
+        let in_progress_bytes = |source: ArrayRef| {
+            let mut in_progress =
+                InProgressPrimitiveArray::<Int32Type>::new(BATCH_SIZE, 
DataType::Int32);
+            let source_len = source.len();
+            in_progress.set_source(Some(source));
+            in_progress.copy_rows(0, source_len / 2).unwrap();
+            in_progress.size()
+        };
+
+        // All values valid: the nulls builder never allocates.
+        let all_valid = 
in_progress_bytes(Arc::new(Int32Array::from_iter_values(0..100)));
+        // Some values null: copying materializes a null buffer that must 
count.
+        let with_nulls = in_progress_bytes(Arc::new(Int32Array::from_iter(
+            (0..100).map(|i| (i % 2 == 0).then_some(i)),
+        )));
+
+        assert!(
+            with_nulls > all_valid,
+            "null buffer must be included in size(): with_nulls={with_nulls} 
all_valid={all_valid}"
+        );
+    }
 }

Reply via email to