sunchao commented on code in PR #7258:
URL: https://github.com/apache/datafusion-comet/pull/7258#discussion_r4406374041
##########
native/core/src/execution/jni_api.rs:
##########
@@ -2105,6 +2106,150 @@ pub unsafe extern "system" fn
Java_org_apache_comet_Native_decodeShuffleBlock(
})
}
+/// Decode a local frame into owned arrays. The direct buffer can be reused
after this call.
+fn append_shuffle_batch(
+ coalescer: &mut DecodedBatchCoalescer,
+ block: &[u8],
+ num_columns: usize,
+) -> CometResult<()> {
+ let batch = read_ipc_compressed(block)?;
+ if batch.num_columns() != num_columns {
+ return Err(CometError::Internal(
+ "Shuffle frame column count mismatch".into(),
+ ));
+ }
+ coalescer.push(batch)?;
+ Ok(())
+}
+
+/// The reader owns this handle and serializes native calls with cleanup.
+/// # Safety
+/// A nonzero handle must refer to a live coalescer exclusively borrowed for
this call.
+unsafe fn shuffle_batch_coalescer<'a>(handle: jlong) -> CometResult<&'a mut
DecodedBatchCoalescer> {
+ unsafe { (handle as *mut DecodedBatchCoalescer).as_mut() }
+ .ok_or_else(|| CometError::Internal("Closed shuffle batch
coalescer".into()))
+}
+
+fn shuffle_batch_ready_columns(coalescer: &DecodedBatchCoalescer) -> jint {
+ coalescer
+ .peek_ready()
+ .map_or(-1, |batch| batch.num_columns() as jint)
+}
+
+#[no_mangle]
+pub extern "system" fn
Java_org_apache_comet_Native_createShuffleBatchCoalescer(
+ e: EnvUnowned,
+ _class: JClass,
+ max_rows: jint,
+) -> jlong {
+ try_unwrap_or_throw(&e, |_| {
+ if max_rows <= 0 {
Review Comment:
Centralized the check in `try_new`: JNI maps negative values to zero before
converting to usize, so nonpositive targets use the same validation and message.
Updated in `48e1024bfc`.
##########
native/core/src/execution/jni_api.rs:
##########
@@ -2105,6 +2106,150 @@ pub unsafe extern "system" fn
Java_org_apache_comet_Native_decodeShuffleBlock(
})
}
+/// Decode a local frame into owned arrays. The direct buffer can be reused
after this call.
+fn append_shuffle_batch(
+ coalescer: &mut DecodedBatchCoalescer,
+ block: &[u8],
+ num_columns: usize,
+) -> CometResult<()> {
+ let batch = read_ipc_compressed(block)?;
+ if batch.num_columns() != num_columns {
+ return Err(CometError::Internal(
+ "Shuffle frame column count mismatch".into(),
+ ));
+ }
+ coalescer.push(batch)?;
+ Ok(())
+}
+
+/// The reader owns this handle and serializes native calls with cleanup.
+/// # Safety
+/// A nonzero handle must refer to a live coalescer exclusively borrowed for
this call.
+unsafe fn shuffle_batch_coalescer<'a>(handle: jlong) -> CometResult<&'a mut
DecodedBatchCoalescer> {
+ unsafe { (handle as *mut DecodedBatchCoalescer).as_mut() }
+ .ok_or_else(|| CometError::Internal("Closed shuffle batch
coalescer".into()))
+}
+
+fn shuffle_batch_ready_columns(coalescer: &DecodedBatchCoalescer) -> jint {
+ coalescer
+ .peek_ready()
+ .map_or(-1, |batch| batch.num_columns() as jint)
+}
+
+#[no_mangle]
+pub extern "system" fn
Java_org_apache_comet_Native_createShuffleBatchCoalescer(
+ e: EnvUnowned,
+ _class: JClass,
+ max_rows: jint,
+) -> jlong {
+ try_unwrap_or_throw(&e, |_| {
+ if max_rows <= 0 {
+ return Err(CometError::Internal(
+ "Decoded shuffle grouping requires a positive row
target".into(),
+ ));
+ }
+ let coalescer = DecodedBatchCoalescer::try_new(max_rows as usize)?;
+ Ok(Box::into_raw(Box::new(coalescer)) as jlong)
+ })
+}
+
+#[no_mangle]
+/// Decode and retain one local frame, returning ready columns or -1 for more
input.
+/// # Safety
+/// The handle must be live and exclusively owned by this reader. On error,
close the reader.
+pub unsafe extern "system" fn Java_org_apache_comet_Native_appendShuffleBatch(
+ e: EnvUnowned,
+ _class: JClass,
+ handle: jlong,
+ byte_buffer: JByteBuffer,
+ length: jint,
+ num_columns: jint,
+ tracing_enabled: jboolean,
+) -> jint {
+ try_unwrap_or_throw(&e, |env| {
+ with_trace("appendShuffleBatch", tracing_enabled != JNI_FALSE, || {
+ let capacity = env.get_direct_buffer_capacity(&byte_buffer)?;
+ if length < 4 || length as usize > capacity || num_columns < 0 {
Review Comment:
Both JNI decode paths now use the same small direct-buffer helper, including
the capacity check. I also removed the extra checks from `takeShuffleBatch`;
its safety contract now states that both address arrays have the column count
returned by peek, matching how `NativeUtil.getNextBatch` allocates them.
Updated in `48e1024bfc`.
##########
spark/src/main/scala/org/apache/spark/sql/comet/execution/shuffle/CometBlockStoreShuffleReader.scala:
##########
@@ -121,6 +124,45 @@ class CometBlockStoreShuffleReader[K, C](
})
.map(b => (0, b))
+ withReadMetrics(recordIter)
+ }
+
+ /** One decoder groups frames across fetched blocks for a single broadcast
producer task. */
+ private[shuffle] def readWithCoalescing(maxRows: Int): Iterator[Product2[K,
C]] = {
+ val input = readAsRawStream()
+ val nativeUtil =
+ try new NativeUtil()
Review Comment:
Simplified this to direct construction followed by listener registration.
The task-completion listener and decoder runtime-error cleanup remain; the
constructor-only catch blocks are gone.
Updated in `48e1024bfc`.
##########
native/shuffle/src/decoded_batch_coalescer.rs:
##########
@@ -0,0 +1,579 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Group small decoded shuffle batches before exporting them to the JVM.
+//!
+//! Groups are bounded by rows, retained Arrow bytes, and input batch count. An
+//! oversized input passes through unchanged instead of being split or copied.
+//!
+//! Keep inputs until a group is ready, then concatenate once at its actual
size.
+
+use std::collections::VecDeque;
+use std::ptr::NonNull;
+
+use arrow::array::RecordBatch;
+use arrow::buffer::Buffer;
+use arrow::compute::concat_batches;
+use arrow::datatypes::DataType;
+use datafusion::common::{DataFusionError, Result};
+
+const MAX_ROWS: usize = 65_536;
+const MAX_GROUP_BATCHES: usize = 1024;
+const MAX_GROUP_BYTES: usize = 8 * 1024 * 1024;
+
+/// Charge the shared IPC body once, but count each column's output copy
separately.
+/// Other shared allocations may be conservatively overcounted.
+#[derive(Default)]
+struct BatchBytes {
+ first_owner: Option<NonNull<u8>>,
+ retained: usize,
+ copied: usize,
+}
+
+impl BatchBytes {
+ fn add_buffer(&mut self, buffer: &Buffer) {
+ let owner = buffer.data_ptr();
+ if self.first_owner == Some(owner) {
+ return;
+ }
+ let capacity = buffer.capacity().max(buffer.len());
+ if capacity != 0 {
+ if self.first_owner.is_none() {
+ self.first_owner = Some(owner);
+ }
+ self.retained = self.retained.saturating_add(capacity);
+ }
+ }
+}
+
+/// Ordered grouping of batches returned by the native shuffle IPC decoder.
+///
+/// `push` and `finish` enqueue output. Inspect it with `peek_ready`, and drain
+/// it with `take_ready` before pushing another input. Call `finish` at EOF.
+/// A singleton, an oversized input, or an input at least half the row target
is
+/// never copied. The byte budget bounds retained group inputs, not the whole
+/// reader: concatenation also owns its output and may have one decoded
lookahead
+/// input. Callers must drop the helper on any error.
+#[derive(Debug)]
+pub struct DecodedBatchCoalescer {
+ max_rows: usize,
+ max_bytes: usize,
+ batches: Vec<RecordBatch>,
+ rows: usize,
+ bytes: usize,
+ ready: VecDeque<RecordBatch>,
+}
+
+impl DecodedBatchCoalescer {
+ /// Creates an empty, reader-owned grouper with the production byte budget.
+ /// Caps positive row targets at 65536.
+ pub fn try_new(max_rows: usize) -> Result<Self> {
+ Self::try_new_with_limits(max_rows, MAX_GROUP_BYTES)
+ }
+
+ /// Creates an empty grouper with explicit limits, allowing small test
budgets.
+ /// Invalid limits return a configuration error. Keeping the byte budget at
+ /// most 8 MiB also keeps every concatenated STRING/BINARY offset within
i32.
+ fn try_new_with_limits(max_rows: usize, max_bytes: usize) -> Result<Self> {
+ if max_rows == 0 {
+ return Err(DataFusionError::Configuration(
+ "Decoded shuffle grouping requires a positive row
target".into(),
+ ));
+ }
+ if !(1..=MAX_GROUP_BYTES).contains(&max_bytes) {
+ return Err(DataFusionError::Configuration(
+ "Decoded shuffle grouping requires 1..8388608 bytes".into(),
+ ));
+ }
+ Ok(Self {
+ max_rows: max_rows.min(MAX_ROWS),
+ max_bytes,
+ batches: Vec::new(),
+ rows: 0,
+ bytes: 0,
+ ready: VecDeque::new(),
+ })
+ }
+
+ /// Transfers the oldest ready output to the caller without copying
buffers.
+ /// Returns None when empty and leaves any pending group unchanged.
+ pub fn take_ready(&mut self) -> Option<RecordBatch> {
+ self.ready.pop_front()
+ }
+
+ /// Borrows the oldest ready output until the next mutation, without
draining it.
+ pub fn peek_ready(&self) -> Option<&RecordBatch> {
+ self.ready.front()
+ }
+
+ /// Takes ownership of one decoded input. A flush followed by a
passthrough can make two
+ /// outputs ready; drain both before reading another input. Drop this
helper on error.
+ pub fn push(&mut self, batch: RecordBatch) -> Result<()> {
+ if !self.ready.is_empty() {
+ return Err(DataFusionError::Execution(
+ "Drain shuffle grouping output before reading another
input".into(),
+ ));
+ }
+ let rows = batch.num_rows();
+ let bytes = self.eligible_bytes(&batch);
+
+ if !self.batches.is_empty()
+ && (bytes.is_none_or(|bytes| bytes > self.max_bytes - self.bytes)
+ || self.batches[0].schema() != batch.schema()
+ || rows > self.max_rows - self.rows)
+ {
+ self.flush()?;
+ }
+ if let Some(bytes) = bytes {
+ self.rows += rows;
+ self.bytes += bytes;
+ self.batches.push(batch);
+ if self.rows == self.max_rows
+ || self.bytes == self.max_bytes
+ || self.batches.len() == MAX_GROUP_BATCHES
+ {
+ self.flush()?;
+ }
+ } else {
+ self.ready.push_back(batch);
+ }
+ Ok(())
+ }
+
+ /// Flushes the final group. Repeated calls do not copy or emit it again.
+ pub fn finish(&mut self) -> Result<()> {
+ self.flush()
+ }
+
+ /// Bounds both retained backing allocations and output copies for flat
columns.
+ /// None means pass through without copying.
+ fn eligible_bytes(&self, batch: &RecordBatch) -> Option<usize> {
+ if batch.num_rows() == 0
+ || batch.num_rows() >= self.max_rows.div_ceil(2)
+ || batch.num_columns() == 0
+ {
+ return None;
+ }
+ let rows = batch.num_rows();
+ // ArrayData reports logical bytes; bitmap builders round up to
64-byte blocks.
+ let bitmap_bytes = rows.div_ceil(8);
Review Comment:
I kept the conservative bitmap allowance. You are right that charging
padding per input can overcount, but removing it does not preserve the
output-copy bound: `get_slice_memory_size()` reports logical bitmap bytes,
while concatenation rounds Boolean values and null buffers to 64-byte blocks.
With aliased Boolean columns, retained source buffers are shared but each
output column needs its own bitmaps, so logical bytes plus metadata need not
cover those allocations. I added a short comment explaining why the allowance
stays; `BatchBytes` still keeps that retained-versus-copied accounting together.
Updated in `48e1024bfc`.
##########
native/shuffle/src/decoded_batch_coalescer.rs:
##########
@@ -0,0 +1,579 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Group small decoded shuffle batches before exporting them to the JVM.
+//!
+//! Groups are bounded by rows, retained Arrow bytes, and input batch count. An
+//! oversized input passes through unchanged instead of being split or copied.
+//!
+//! Keep inputs until a group is ready, then concatenate once at its actual
size.
+
+use std::collections::VecDeque;
+use std::ptr::NonNull;
+
+use arrow::array::RecordBatch;
+use arrow::buffer::Buffer;
+use arrow::compute::concat_batches;
+use arrow::datatypes::DataType;
+use datafusion::common::{DataFusionError, Result};
+
+const MAX_ROWS: usize = 65_536;
+const MAX_GROUP_BATCHES: usize = 1024;
+const MAX_GROUP_BYTES: usize = 8 * 1024 * 1024;
+
+/// Charge the shared IPC body once, but count each column's output copy
separately.
+/// Other shared allocations may be conservatively overcounted.
+#[derive(Default)]
+struct BatchBytes {
+ first_owner: Option<NonNull<u8>>,
+ retained: usize,
+ copied: usize,
+}
+
+impl BatchBytes {
+ fn add_buffer(&mut self, buffer: &Buffer) {
+ let owner = buffer.data_ptr();
+ if self.first_owner == Some(owner) {
+ return;
+ }
+ let capacity = buffer.capacity().max(buffer.len());
+ if capacity != 0 {
+ if self.first_owner.is_none() {
+ self.first_owner = Some(owner);
+ }
+ self.retained = self.retained.saturating_add(capacity);
+ }
+ }
+}
+
+/// Ordered grouping of batches returned by the native shuffle IPC decoder.
+///
+/// `push` and `finish` enqueue output. Inspect it with `peek_ready`, and drain
+/// it with `take_ready` before pushing another input. Call `finish` at EOF.
+/// A singleton, an oversized input, or an input at least half the row target
is
+/// never copied. The byte budget bounds retained group inputs, not the whole
+/// reader: concatenation also owns its output and may have one decoded
lookahead
+/// input. Callers must drop the helper on any error.
+#[derive(Debug)]
+pub struct DecodedBatchCoalescer {
+ max_rows: usize,
+ max_bytes: usize,
+ batches: Vec<RecordBatch>,
+ rows: usize,
+ bytes: usize,
+ ready: VecDeque<RecordBatch>,
+}
+
+impl DecodedBatchCoalescer {
+ /// Creates an empty, reader-owned grouper with the production byte budget.
+ /// Caps positive row targets at 65536.
+ pub fn try_new(max_rows: usize) -> Result<Self> {
+ Self::try_new_with_limits(max_rows, MAX_GROUP_BYTES)
+ }
+
+ /// Creates an empty grouper with explicit limits, allowing small test
budgets.
+ /// Invalid limits return a configuration error. Keeping the byte budget at
+ /// most 8 MiB also keeps every concatenated STRING/BINARY offset within
i32.
+ fn try_new_with_limits(max_rows: usize, max_bytes: usize) -> Result<Self> {
+ if max_rows == 0 {
+ return Err(DataFusionError::Configuration(
+ "Decoded shuffle grouping requires a positive row
target".into(),
+ ));
+ }
+ if !(1..=MAX_GROUP_BYTES).contains(&max_bytes) {
+ return Err(DataFusionError::Configuration(
+ "Decoded shuffle grouping requires 1..8388608 bytes".into(),
+ ));
+ }
+ Ok(Self {
+ max_rows: max_rows.min(MAX_ROWS),
+ max_bytes,
+ batches: Vec::new(),
+ rows: 0,
+ bytes: 0,
+ ready: VecDeque::new(),
+ })
+ }
+
+ /// Transfers the oldest ready output to the caller without copying
buffers.
+ /// Returns None when empty and leaves any pending group unchanged.
+ pub fn take_ready(&mut self) -> Option<RecordBatch> {
+ self.ready.pop_front()
+ }
+
+ /// Borrows the oldest ready output until the next mutation, without
draining it.
+ pub fn peek_ready(&self) -> Option<&RecordBatch> {
+ self.ready.front()
+ }
+
+ /// Takes ownership of one decoded input. A flush followed by a
passthrough can make two
+ /// outputs ready; drain both before reading another input. Drop this
helper on error.
+ pub fn push(&mut self, batch: RecordBatch) -> Result<()> {
+ if !self.ready.is_empty() {
+ return Err(DataFusionError::Execution(
+ "Drain shuffle grouping output before reading another
input".into(),
+ ));
+ }
+ let rows = batch.num_rows();
+ let bytes = self.eligible_bytes(&batch);
+
+ if !self.batches.is_empty()
+ && (bytes.is_none_or(|bytes| bytes > self.max_bytes - self.bytes)
+ || self.batches[0].schema() != batch.schema()
+ || rows > self.max_rows - self.rows)
+ {
+ self.flush()?;
+ }
+ if let Some(bytes) = bytes {
+ self.rows += rows;
+ self.bytes += bytes;
+ self.batches.push(batch);
+ if self.rows == self.max_rows
+ || self.bytes == self.max_bytes
+ || self.batches.len() == MAX_GROUP_BATCHES
+ {
+ self.flush()?;
+ }
+ } else {
+ self.ready.push_back(batch);
+ }
+ Ok(())
+ }
+
+ /// Flushes the final group. Repeated calls do not copy or emit it again.
+ pub fn finish(&mut self) -> Result<()> {
+ self.flush()
+ }
+
+ /// Bounds both retained backing allocations and output copies for flat
columns.
+ /// None means pass through without copying.
+ fn eligible_bytes(&self, batch: &RecordBatch) -> Option<usize> {
+ if batch.num_rows() == 0
+ || batch.num_rows() >= self.max_rows.div_ceil(2)
+ || batch.num_columns() == 0
+ {
+ return None;
+ }
+ let rows = batch.num_rows();
+ // ArrayData reports logical bytes; bitmap builders round up to
64-byte blocks.
+ let bitmap_bytes = rows.div_ceil(8);
+ let padded_bitmap_bytes = bitmap_bytes.next_multiple_of(64);
+ let mut bytes = BatchBytes::default();
+ for column in batch.columns() {
+ if !matches!(
+ column.data_type(),
+ DataType::Int32
+ | DataType::Int64
+ | DataType::Boolean
+ | DataType::Utf8
+ | DataType::Binary
+ ) {
+ return None;
+ }
+ let data = column.to_data();
+ let metadata = std::mem::size_of_val(column.as_ref());
+ // to_data may omit an all-valid bitmap; account for the original
nulls below.
+ let copied =
+ data.get_slice_memory_size().ok()? - data.nulls().map_or(0,
|_| bitmap_bytes);
+ bytes.copied =
bytes.copied.saturating_add(copied).saturating_add(metadata);
+ bytes.retained = bytes.retained.saturating_add(metadata);
+ for buffer in data.buffers() {
+ bytes.add_buffer(buffer);
+ }
+ if let Some(nulls) = column.nulls() {
+ bytes.add_buffer(nulls.buffer());
+ bytes.copied =
bytes.copied.saturating_add(padded_bitmap_bytes);
+ }
+ if data.data_type() == &DataType::Boolean {
+ bytes.copied = bytes
+ .copied
+ .saturating_add(padded_bitmap_bytes - bitmap_bytes);
+ }
+ if bytes.retained.max(bytes.copied) > self.max_bytes {
+ return None;
+ }
+ }
+ Some(bytes.retained.max(bytes.copied))
+ }
+
+ fn flush(&mut self) -> Result<()> {
+ if self.batches.len() < 2 {
+ self.ready.extend(self.batches.drain(..));
+ } else {
+ self.ready
+ .push_back(concat_batches(&self.batches[0].schema(),
&self.batches)?);
+ self.batches.clear();
+ }
+ self.rows = 0;
+ self.bytes = 0;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::sync::Arc;
+
+ use arrow::array::{
+ ArrayRef, AsArray, BinaryArray, BooleanArray, Int32Array, Int64Array,
StringArray,
+ };
+ use arrow::datatypes::{Field, Int32Type, Schema};
+ use arrow::ipc::writer::StreamWriter;
+
+ fn one_column(array: ArrayRef) -> RecordBatch {
+ let schema = Arc::new(Schema::new(vec![Field::new(
+ "x",
+ array.data_type().clone(),
+ true,
+ )]));
+ RecordBatch::try_new(schema, vec![array]).unwrap()
+ }
+
+ fn ints(values: impl IntoIterator<Item = i32>) -> RecordBatch {
+ one_column(Arc::new(Int32Array::from_iter_values(values)))
+ }
+
+ fn encode_frame(batch: &RecordBatch) -> Vec<u8> {
+ let mut frame = b"NONE".to_vec();
+ let mut writer = StreamWriter::try_new(&mut frame,
batch.schema().as_ref()).unwrap();
+ writer.write(batch).unwrap();
+ writer.finish().unwrap();
+ frame
+ }
+
+ fn collect(max_rows: usize, batches: Vec<RecordBatch>) -> Vec<RecordBatch>
{
+ collect_with_limits(max_rows, MAX_GROUP_BYTES, batches)
+ }
+
+ fn collect_with_limits(
+ max_rows: usize,
+ max_bytes: usize,
+ batches: Vec<RecordBatch>,
+ ) -> Vec<RecordBatch> {
+ let mut coalescer =
+ DecodedBatchCoalescer::try_new_with_limits(max_rows,
max_bytes).unwrap();
+ let mut output = Vec::new();
+ for batch in batches {
+ coalescer.push(batch).unwrap();
+ while let Some(batch) = coalescer.take_ready() {
+ output.push(batch);
+ }
+ }
+ coalescer.finish().unwrap();
+ while let Some(batch) = coalescer.take_ready() {
+ output.push(batch);
+ }
+ output
+ }
+
+ #[test]
+ fn finishing_repeatedly_emits_the_pending_group_once() {
+ let mut coalescer = DecodedBatchCoalescer::try_new(8).unwrap();
+ coalescer.push(ints(0..1)).unwrap();
+ coalescer.push(ints(1..2)).unwrap();
+ coalescer.finish().unwrap();
+ coalescer.finish().unwrap();
+ assert_eq!(coalescer.take_ready(), Some(ints(0..2)));
+ coalescer.finish().unwrap();
+ assert!(coalescer.take_ready().is_none());
+ }
+
+ #[test]
+ fn singleton_large_and_near_full_inputs_are_not_copied() {
+ for batches in [
+ vec![ints(0..1)],
+ vec![ints(0..1), ints(1..7)],
+ vec![ints(0..6), ints(6..7)],
+ vec![ints(0..1), ints(1..10)],
+ ] {
+ let originals: Vec<_> = batches
+ .iter()
+ .map(|batch| batch.column(0).clone())
+ .collect();
+ let output = collect(8, batches);
+ assert_eq!(output.len(), originals.len());
+ for (batch, original) in output.iter().zip(originals) {
+ assert!(Arc::ptr_eq(batch.column(0), &original));
+ }
+ }
+ }
+
+ #[test]
+ fn row_and_input_count_limits_preserve_order() {
+ let batches = (0..4).map(|i| ints(i * 2..i * 2 + 2)).collect();
+ assert_eq!(collect(8, batches), vec![ints(0..8)]);
+ assert_eq!(
+ collect(8, vec![ints(0..3), ints(3..6), ints(6..9)]),
+ vec![ints(0..6), ints(6..9)]
+ );
+ let batches = (0..1025).map(|i| ints(i..i + 1)).collect();
+ assert_eq!(
+ collect(MAX_ROWS, batches),
+ vec![ints(0..1024), ints(1024..1025)]
+ );
+ }
+
+ #[test]
+ fn byte_limit_flushes_at_and_before_crossing_the_boundary() {
+ let inputs: Vec<_> = (0..3).map(|i| ints(i..i + 1)).collect();
+ let two_batches = inputs[0].get_array_memory_size() +
inputs[1].get_array_memory_size();
+ assert_eq!(
+ collect_with_limits(32, two_batches, inputs.clone()),
+ vec![ints(0..2), ints(2..3)]
+ );
+ let output = collect_with_limits(32, two_batches - 1, inputs.clone());
+ assert_eq!(output, inputs);
+ for (output, input) in output.iter().zip(&inputs) {
+ assert!(Arc::ptr_eq(output.column(0), input.column(0)));
+ }
+
+ let mut coalescer = DecodedBatchCoalescer::try_new_with_limits(32,
two_batches).unwrap();
+ coalescer.push(inputs[0].clone()).unwrap();
+ assert!(coalescer.peek_ready().is_none());
+ coalescer.push(inputs[1].clone()).unwrap();
+ assert_eq!(coalescer.take_ready(), Some(ints(0..2)));
+ coalescer.push(inputs[2].clone()).unwrap();
+ assert!(coalescer.take_ready().is_none());
+ coalescer.finish().unwrap();
+ assert_eq!(coalescer.take_ready(), Some(ints(2..3)));
+ }
+
+ #[test]
+ fn wide_json_values_pass_through_without_copying() {
Review Comment:
Removed the JSON-specific duplicate, the repeated push/peek/take block, and
the weak-reference lifetime test. The sliced-backing-buffer case now has a
second small input after passthrough and checks that grouping resumes. The
remaining native and JVM cases retain the observable flush/passthrough and
ordering coverage.
Updated in `48e1024bfc`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]