Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions arrow-array/src/builder/generic_bytes_view_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,25 +224,39 @@ impl<T: ByteViewType + ?Sized> GenericByteViewBuilder<T> {
/// Appends an array to the builder.
/// This will flush any in-progress block and append the data buffers
/// and add the (adapted) views.
///
/// # Panics
///
/// Panics if the builder would hold more than `u32::MAX` data buffers
Comment on lines +227 to +230

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 this makes sense to do but it is a breaking change right? code the previously worked (produced incorrect results) ran but now this will cause a panic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a breaking change. Code that produces incorrect results should not be considered "code that previously worked" -- if that was the definition of a breaking change, many straightforward bugfixes would not be allowed in minor releases.

Additional considerations:

  • In debug builds, the previous coding resulted in a panic anyway, so there is no change in behavior.
  • In release builds, the previous behavior is actually worse than just incorrect results: buffer_index points at one buffer but the view's length and offset were validated against a different buffer. Reader methods like GenericByteViewArray::value(k) do an unchecked read somewhere off into heap memory, resulting in undefined behavior.
  • Overflow requires an array with u32::MAX data buffers, which is extremely unlikely to occur in practice.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code that produces incorrect results should not be considered "code that previously worked" -- if that was the definition of a breaking change, many straightforward bugfixes would not be allowed in minor releases.

good point.

pub fn append_array(&mut self, array: &GenericByteViewArray<T>) {
self.flush_in_progress();
// keep original views if this array is the first to be added or if there are no data buffers (all inline views)
let keep_views = self.completed.is_empty() || array.data_buffers().is_empty();
let starting_buffer = self.completed.len() as u32;

self.completed.extend(array.data_buffers().iter().cloned());
assert!(
u32::try_from(self.completed.len()).is_ok(),
"Too many blocks"
);
Comment on lines +227 to +241

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before this assertion what happened if the builder overflowed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On an overflow, the behavior before this PR was to panic in debug mode. In release mode, the buffer index would wrap and point at the wrong buffer.


if keep_views {
self.views_buffer.extend_from_slice(array.views());
} else {
self.views_buffer.extend(array.views().iter().map(|v| {
let mut byte_view = ByteView::from(*v);
if byte_view.length > MAX_INLINE_VIEW_LEN {
// Small views (<=12 bytes) are inlined, so only need to update large views
byte_view.buffer_index += starting_buffer;
}

byte_view.as_u128()
// A view of a string of 12 bytes or fewer stores the string
// inline and has no buffer index, so adding 0 leaves it
// unchanged.
let offset = if *v as u32 > MAX_INLINE_VIEW_LEN {
starting_buffer
} else {
0
};
// The buffer index is bits 64 to 95 of a view. Adding
// `starting_buffer` gives the string's buffer index in the
// builder, which fits in 32 bits, so the sum never carries
// into bit 96.
v + ((offset as u128) << 64)
}));
}

Expand Down
25 changes: 18 additions & 7 deletions arrow-select/src/coalesce/byte_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,21 +194,32 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
self.size_of_completed_buffers_from_current_source += buffers_size;
}

let starting_buffer: u32 = self.completed.len().try_into().expect("too many buffers");
let starting_buffer = self.completed.len() as u32;
self.completed.extend_from_slice(buffers);
assert!(
u32::try_from(self.completed.len()).is_ok(),
"too many buffers"
);

if starting_buffer == 0 {
// If there are no buffers, we can just use the views as is
self.views.extend_from_slice(views);
} else {
// If there are buffers, we need to update the buffer index
let updated_views = views.iter().map(|v| {
let mut byte_view = ByteView::from(*v);
if byte_view.length > MAX_INLINE_VIEW_LEN {
// Small views (<=12 bytes) are inlined, so only need to update large views
byte_view.buffer_index += starting_buffer;
}
byte_view.as_u128()
// A view of a string of 12 bytes or fewer stores the string
// inline and has no buffer index, so adding 0 leaves it
// unchanged.
let offset = if *v as u32 > MAX_INLINE_VIEW_LEN {
starting_buffer
} else {
0
};
// The buffer index is bits 64 to 95 of a view. Adding
// `starting_buffer` gives the string's buffer index in the
// output, which fits in 32 bits, so the sum never carries into
// bit 96.
v + ((offset as u128) << 64)
Comment thread
Rich-T-kid marked this conversation as resolved.
});

self.views.extend(updated_views);
Expand Down
32 changes: 23 additions & 9 deletions arrow/benches/coalesce_kernels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1447,10 +1447,17 @@ impl DataStreamBuilder {
)
.with_timezone(Arc::clone(tz)),
),
DataType::Utf8 => Arc::new(create_string_array::<i32>(
self.batch_size,
self.null_density,
)), // TODO seed
// Strings of 0 to 399 bytes; `max_string_len` doesn't apply
Comment thread
Rich-T-kid marked this conversation as resolved.
DataType::Utf8 => Arc::new(
create_string_array_with_len_range_and_prefix_and_seed::<i32>(
self.batch_size,
self.null_density,
0,
399,
"",
seed,
),
),
DataType::Binary => Arc::new(create_binary_array_with_len_range_and_prefix_and_seed::<
i32,
>(
Expand Down Expand Up @@ -1482,11 +1489,18 @@ impl DataStreamBuilder {
if key_type.as_ref() == &DataType::Int32
&& value_type.as_ref() == &DataType::Utf8 =>
{
Arc::new(create_string_dict_array::<Int32Type>(
self.batch_size,
self.null_density,
self.max_string_len,
)) // TODO seed
Arc::new(
create_string_array_with_len_range_and_prefix_and_seed::<i32>(
self.batch_size,
self.null_density,
self.max_string_len,
self.max_string_len,
"",
seed,
)
.iter()
.collect::<DictionaryArray<Int32Type>>(),
)
}
DataType::FixedSizeBinary(size) => Arc::new(create_fixed_size_binary_array(
self.batch_size,
Expand Down
Loading