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 c3c6ba3db4 Replace BufferBuilder with Vec in UnionBuilder's
FieldDataValues (#11040)
c3c6ba3db4 is described below
commit c3c6ba3db409909d3de9f5ad8025472adb6e325e
Author: Shivam <[email protected]>
AuthorDate: Mon Sep 14 21:53:28 2026 -0700
Replace BufferBuilder with Vec in UnionBuilder's FieldDataValues (#11040)
# Which issue does this PR close?
Part of #10245.
# Rationale for this change
`UnionBuilder`'s `FieldDataValues` trait is implemented for
`BufferBuilder<T>` and used purely as a growable, type-erased per-field
value buffer: `append`/`push` one value at a time, then
`finish()`/`finish_cloned()` into a `Buffer`. This is exactly the
scratch-buffer shape #10245 is tracking, and it was still on the
"remaining callsites" list (the two other listed sites, arity.rs and
zip.rs, already have PRs open: #10518 and #10913).
# What changes are included in this PR?
`impl<T: ArrowNativeType> FieldDataValues for BufferBuilder<T>` becomes
`impl<T: ArrowNativeType> FieldDataValues for Vec<T>`:
- `append_null` (`self.advance(1)`) becomes `self.push(T::default())`.
`BufferBuilder::advance` zero-pads; `ArrowNativeType: Default` is a
sealed, primitive-only supertrait bound, and zero is its default for
every implementor, so this is the same padding value.
- `finish` (`self.finish()`, which resets the builder via `mem::take`)
becomes `Buffer::from_vec(std::mem::take(self))` — same reset-and-return
shape. `finish` is only reached through `UnionBuilder::build(self)`,
which consumes the whole builder, so the reset value is never observed.
- `finish_cloned` keeps the existing
`Buffer::from_slice_ref(self.as_slice())` call unchanged — `Vec<T>` and
`BufferBuilder<T>` both expose `as_slice()`.
- `FieldData::new` and `FieldData::append_value`'s `downcast_mut` switch
from `BufferBuilder::<T::Native>` to `Vec::<T::Native>`, and
`.append(v)` becomes `.push(v)`.
`type_id_builder`/`value_offset_builder`
(`Int8BufferBuilder`/`Int32BufferBuilder`) are untouched — they're a
different pair of fields, not on #10245's callsite list, and out of
scope for this targeted change.
# Are these changes tested?
Yes, by the file's existing tests — no behavior changes, so no new test
was added (matching #10851, the other merged PR in this epic that also
didn't add one):
- `cargo test -p arrow-array --lib union`: 31 passed, 0 failed (includes
both `union_builder` tests plus every `union_array` test — dense/sparse,
with/without nulls, offsets — since `UnionArray` is built through
`UnionBuilder` or checked against its output in several of these).
- `cargo test -p arrow-array --doc union_builder`: 2 passed, 0 failed
(the dense/sparse doctests on `UnionBuilder` itself).
- `cargo clippy -p arrow-array --lib -- -D warnings`: no diagnostics on
`union_builder.rs`. (Unrelated pre-existing clippy findings in
`arrow-data/src/data.rs` reproduce identically on an unmodified `main`
under my clippy version and are not part of this PR.)
- `cargo fmt -p arrow-array -- --check`: clean.
I also ran a throwaway, uncommitted `cargo run --release` microbenchmark
(200,000 `UnionBuilder::append` + one `finish`, 5 runs, median reported)
to see whether this site shows the same kind of win as the zip PR. It
doesn't, and I want to report that honestly rather than imply a bigger
effect than exists: main (`BufferBuilder`) median 22.12ms (110.6 ns/row)
vs. this branch (`Vec`) median 22.04ms (110.2 ns/row) — roughly 0.3%,
inside run-to-run noise. `UnionBuilder::append_option` removes and
reinserts a `BTreeMap` entry and allocates a `String` key on every call,
which dominates the per-row cost far more than the value-buffer push
does, so this change doesn't move the needle on its own the way the zip
kernel's tighter loop did. I'm including it anyway because it still
removes one more `BufferBuilder` usage per #10245's stated goal, with no
behavior or performance regression.
# Are there any user-facing changes?
No. `FieldDataValues`/`FieldData` are private to this module;
`UnionBuilder`'s public API is unchanged.
# Automated assistance
This PR — the code change, the verification, and this description — was
drafted by an AI coding agent (Claude), reviewed and run by me before
opening. Per CONTRIBUTING.md's AI Generated Submissions guidance: I read
the whole diff and the surrounding `FieldDataValues`/`UnionBuilder` code
to confirm the `finish`-only-reachable-through-`build(self)` reasoning
above, ran every check listed under "Are these changes tested?" myself,
and the microbenchmark numbers are from an actual local run (not
fabricated) — reported as inconclusive/noise-level rather than rounded
up to look like a win.
---
arrow-array/src/builder/union_builder.rs | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/arrow-array/src/builder/union_builder.rs
b/arrow-array/src/builder/union_builder.rs
index 9e64d97d9f..63a401387b 100644
--- a/arrow-array/src/builder/union_builder.rs
+++ b/arrow-array/src/builder/union_builder.rs
@@ -15,8 +15,8 @@
// specific language governing permissions and limitations
// under the License.
+use crate::builder::ArrayBuilder;
use crate::builder::buffer_builder::{Int8BufferBuilder, Int32BufferBuilder};
-use crate::builder::{ArrayBuilder, BufferBuilder};
use crate::{ArrayRef, ArrowPrimitiveType, UnionArray, make_array};
use arrow_buffer::NullBufferBuilder;
use arrow_buffer::{ArrowNativeType, Buffer, ScalarBuffer};
@@ -41,7 +41,7 @@ struct FieldData {
null_buffer_builder: NullBufferBuilder,
}
-/// A type-erased [`BufferBuilder`] used by [`FieldData`]
+/// A type-erased growable value buffer used by [`FieldData`]
trait FieldDataValues: std::fmt::Debug + Send + Sync {
fn as_mut_any(&mut self) -> &mut dyn Any;
@@ -52,17 +52,17 @@ trait FieldDataValues: std::fmt::Debug + Send + Sync {
fn finish_cloned(&self) -> Buffer;
}
-impl<T: ArrowNativeType> FieldDataValues for BufferBuilder<T> {
+impl<T: ArrowNativeType> FieldDataValues for Vec<T> {
fn as_mut_any(&mut self) -> &mut dyn Any {
self
}
fn append_null(&mut self) {
- self.advance(1)
+ self.push(T::default())
}
fn finish(&mut self) -> Buffer {
- self.finish()
+ Buffer::from_vec(std::mem::take(self))
}
fn finish_cloned(&self) -> Buffer {
@@ -77,7 +77,7 @@ impl FieldData {
type_id,
data_type,
slots: 0,
- values_buffer: Box::new(BufferBuilder::<T::Native>::new(capacity)),
+ values_buffer: Box::new(Vec::<T::Native>::with_capacity(capacity)),
null_buffer_builder: NullBufferBuilder::new(capacity),
}
}
@@ -86,9 +86,9 @@ impl FieldData {
fn append_value<T: ArrowPrimitiveType>(&mut self, v: T::Native) {
self.values_buffer
.as_mut_any()
- .downcast_mut::<BufferBuilder<T::Native>>()
+ .downcast_mut::<Vec<T::Native>>()
.expect("Tried to append unexpected type")
- .append(v);
+ .push(v);
self.null_buffer_builder.append(true);
self.slots += 1;