Skip to content

feat(array): let callers choose a nested builder's chunk threshold [builders-child-stack] - #8965

Closed
robert3005 wants to merge 5 commits into
claude/builders-canonical-children-9ze0t6from
claude/builders-min-chunk-len-9ze0t6
Closed

feat(array): let callers choose a nested builder's chunk threshold [builders-child-stack]#8965
robert3005 wants to merge 5 commits into
claude/builders-canonical-children-9ze0t6from
claude/builders-min-chunk-len-9ze0t6

Conversation

@robert3005

@robert3005 robert3005 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

Stacked on #8964 — review that one first.

ChildBuilder copies appended arrays shorter than 64 values instead of giving each one a chunk. That default is right for the callers it was written for: nested builders routinely append the elements of a single list, and a chunk per list would produce a ChunkedArray with more chunks than values.

It is wrong for a caller whose appended arrays are already chunks whose identity is the point. The motivating one is ChunkedArray's canonicalization, which today hand-rolls swizzle_list_chunks and friends precisely to reuse each chunk's children without copying.

The argument for the knob is compatibility rather than measured performance. Canonicalizing a ChunkedArray currently preserves every chunk boundary in the children, at any chunk size; letting the default threshold apply would silently coalesce chunks under 64 rows into a copy. Passing 0 reproduces today's behaviour exactly, which is what a refactor should do. Whether sub-64-row chunks are common enough at canonicalization time for that to matter is unmeasured — I instrumented the threshold across eleven crates' test suites and every production-shaped append (7232 and 8192 rows, file scan batches) chunked under either setting; the only sub-64 chunks observed came from tests.

What changes are included in this PR?

ArrayBuilder::set_min_chunk_len, defaulting to a no-op so builders without array children ignore it. StructBuilder, ListBuilder, ListViewBuilder, FixedSizeListBuilder and ExtensionBuilder forward it to their ChildBuilders, which hold the threshold as a field instead of reading the const.

Two semantics worth stating, both documented on the trait method:

  • The threshold is read when an array is appended, so it only affects subsequent appends — no "call this before anything else" ordering trap.
  • It applies transitively. A ChildBuilder's scalar builder is itself a nested builder with children of its own, so it gets the setting too.

Three tests: a threshold of zero keeps every chunk boundary at the ChildBuilder level and through the public builder_with_capacity, and the transitive case is checked with a list-typed child short enough to be materialized whose elements would otherwise have earned a chunk.

What APIs are changed? Are there any user-facing changes?

One new ArrayBuilder trait method with a default implementation, so external implementors are unaffected. No behaviour change for anyone who does not call it: DEFAULT_MIN_CHUNK_LEN is the previous constant.

Checks

  • cargo test -p vortex-array — 3167 lib tests
  • cargo +nightly fmt --all, cargo clippy -p vortex-array --all-targets --all-features

Generated by Claude Code

Stacked PR Chain: builders-child-stack

PR Title Merges Into
#9046 perf(array): stop flattening appended list views in ListViewBuilder [builders-child-stack] N/A
#8964 feat(array): builders no longer canonicalize their children [builders-child-stack] #9046
#8965 feat(array): let callers choose a nested builder's chunk threshold [builders-child-stack] #8964
#8966 perf(array): accumulate nested builder validity without a null buffer [builders-child-stack] #8965
#8967 refactor(array): canonicalize chunked structs and FSLs through the builder [builders-child-stack] #8966

Generated by Claude Code

@claude claude Bot changed the title feat(array): let callers choose a nested builder's chunk threshold feat(array): let callers choose a nested builder's chunk threshold [builders-child-stack] Jul 25, 2026
@robert3005
robert3005 force-pushed the claude/builders-canonical-children-9ze0t6 branch from 2e987a3 to b4cb8fb Compare July 25, 2026 21:07
@robert3005
robert3005 force-pushed the claude/builders-min-chunk-len-9ze0t6 branch from cc36904 to 6f3ef13 Compare July 25, 2026 21:07
@codspeed-hq

codspeed-hq Bot commented Jul 25, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 1841 untouched benchmarks
⏩ 55 skipped benchmarks1


Comparing claude/builders-min-chunk-len-9ze0t6 (20dd597) with claude/builders-canonical-children-9ze0t6 (c78ed3c)

Open in CodSpeed

Footnotes

  1. 55 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

claude added 5 commits July 29, 2026 08:30
`append_listview_array` rebuilt every incoming `ListViewArray` into an exact
layout before appending it. Rebasing offsets by the number of elements
already in the builder is correct whatever layout they have, so the rebuild
bought nothing except an unconditional promise that the finished array is
zero-copyable to a `ListArray` - and it cost the caller any sharing the
source expressed.

A constant list array is the case that matters: canonicalizing one already
points every view at a single copy of the value, and flattening it
materialized one copy per row. Appending a 10,000-row constant list of three
elements produced 30,000 elements; it now produces 3.

Keep trimming unreferenced elements, but otherwise append the views as they
arrived and track whether the result is still zero-copyable to a `ListArray`
instead of asserting it. The flag is per-array and consumers already branch
on it, so callers that need an exact layout can rebuild.

Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: Robert Kruszewski <robert@spiraldb.com>
Nested builders used to push every appended child array through
`append_to_builder`, which decoded it into the child's canonical builder.
That work is wasted: `Canonical` only promises a canonical *top level*, so
struct fields, list elements and extension storage are free to stay
compressed.

Introduce `ChildBuilder`, which accumulates a child as a `Vec<ArrayRef>` of
chunks plus a scalar builder for the values that cannot come from an array,
and stitches them into a `ChunkedArray` on `finish` when more than one chunk
accumulated. `StructBuilder`, `ListBuilder`, `ListViewBuilder`,
`FixedSizeListBuilder` and `ExtensionBuilder` now hold their children this
way.

Arrays shorter than `MIN_CHUNK_LEN` are still materialized into the scalar
builder: nested builders routinely append the elements of a single list, and
a chunk per list would cost far more than the copy.

Signed-off-by: Claude <noreply@anthropic.com>
The first pass only checked that children survive undecoded. Add coverage
for the arithmetic and validity handling that the chunk path changes:

- `ChildBuilder`: empty arrays never become chunks, dtype mismatches are
  rejected on both sides of the length threshold, and scalars, zeros and
  nulls appended around a chunk keep their order.
- `ChildBuilder::set_validity_unchecked`: the override is sliced per chunk,
  covers values still pending in the scalar builder, is dropped for a
  non-nullable child, and panics on a chunk that already contains nulls.
- List builders: offsets are rebased onto the running element count once the
  elements live in chunks, for `ListViewArray` and `ListArray` inputs and for
  `ListBuilder::append_array_as_list`.
- Nested builders keep their own validity buffer alongside a chunked child,
  extension storage receives a validity override through its chunks, and a
  built array with chunked children still canonicalizes recursively.

Each test was checked against a mutated implementation: dropping the
per-chunk validity slice, dropping the pending flush, zeroing the listview
offset base, and taking the `ListBuilder` offset from the appended array
instead of the running total all fail the intended tests.

Signed-off-by: Claude <noreply@anthropic.com>
`typos` splits `mis-sliced` on the hyphen and flags `mis`.

Signed-off-by: Claude <noreply@anthropic.com>
`ChildBuilder` copies appended arrays shorter than 64 values instead of
giving them a chunk, which is right for the nested builders that append the
elements of a single list, and wrong for a caller whose appended arrays are
themselves chunks worth preserving.

Add `ArrayBuilder::set_min_chunk_len`, a no-op for builders without array
children, so such a caller can lower the threshold — to zero to keep every
chunk boundary. The threshold is read when an array is appended and applies
transitively to the children of a builder's children.

Signed-off-by: Claude <noreply@anthropic.com>
@robert3005
robert3005 force-pushed the claude/builders-canonical-children-9ze0t6 branch from b4cb8fb to c78ed3c Compare July 29, 2026 07:35
@robert3005
robert3005 force-pushed the claude/builders-min-chunk-len-9ze0t6 branch from 6f3ef13 to 20dd597 Compare July 29, 2026 07:35
@robert3005
robert3005 force-pushed the claude/builders-canonical-children-9ze0t6 branch from c78ed3c to e45996f Compare July 29, 2026 13:48
@robert3005

Copy link
Copy Markdown
Contributor Author

Superseded: MIN_CHUNK_LEN is deleted in #8964, which now keeps every appended array as a chunk however short it is. With no threshold left there is nothing for set_min_chunk_len to configure, and #8967 no longer needs to call it.

The looping callers that motivated the threshold — the sparse fill loops and ListBuilder::extend_from_listview — are converted to bulk appends in #9064 instead, so nothing depends on this any more.

@robert3005 robert3005 closed this Jul 29, 2026
@robert3005
robert3005 deleted the claude/builders-min-chunk-len-9ze0t6 branch July 29, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants