Skip to content

feat: append array views to arrays - #930

Open
rustyconover wants to merge 11 commits into
apache:mainfrom
Query-farm:feat/array-view-appender
Open

feat: append array views to arrays#930
rustyconover wants to merge 11 commits into
apache:mainfrom
Query-farm:feat/array-view-appender

Conversation

@rustyconover

Copy link
Copy Markdown
Contributor

Summary

  • add ArrowArrayAppendArrayView() as a core array-building API
  • append primitive, nested, union, list-view, and run-end encoded values from an ArrowArrayView
  • preserve source slicing semantics and report unsupported sliced run-end encoded inputs
  • add direct native coverage for primitive, nested/sliced, union, and run-end encoded arrays

Context

This extracts the generic array-view appender from #928 as requested in review. It is independently useful and will be the prerequisite for a separate dictionary-delta decoding PR; #928 no longer contains the appender or decoder changes.

Validation

  • native CMake build passed
  • native CTest suite: 334/334 passed (4 optional codec tests skipped)

@codecov-commenter

codecov-commenter commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.15%. Comparing base (6657945) to head (85ea0f7).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #930      +/-   ##
==========================================
- Coverage   79.49%   78.15%   -1.34%     
==========================================
  Files         106      106              
  Lines       16564    16859     +295     
  Branches     1986     1988       +2     
==========================================
+ Hits        13167    13176       +9     
- Misses       2164     2449     +285     
- Partials     1233     1234       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@paleolimbot paleolimbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you!

Some things to take care of here but I think this is great functionality.

I can follow up with R/Python bindings, which are mostly useful because testing this stuff in C++ is a pain. I think this may also make a few of the R and Python constructors quite a bit simpler.

Comment thread src/nanoarrow/common/array.c Outdated
Comment thread src/nanoarrow/common/array.c Outdated
Comment thread src/nanoarrow/common/array.c Outdated
Comment thread src/nanoarrow/common/array.c Outdated
Comment thread src/nanoarrow/common/array_test.cc Outdated
Comment thread src/nanoarrow/common/array_test.cc Outdated
Add Arrow C++ conversion matrices plus sliced validity, overflow, dictionary, and sliced run-end tests. Preserve detailed errors produced by nested append helpers.
Avoid reading past short int8 or int32 input arrays when an append begins in a partially filled bitmap byte.
@rustyconover

Copy link
Copy Markdown
Contributor Author

CI follow-up: b4f4adc fixes the shared failure behind clang-tidy and the four Valgrind jobs. ArrowBitmapAppendInt8Unsafe could read more values than supplied when a short append began in a partially filled byte; the same issue existed in the int32 variant. Both helpers now bound the partial-byte copy, with regression tests for short unaligned appends. Native, Arrow C++, AddressSanitizer, and local clang-tidy checks pass.

Ensure clang-tidy can prove that all values passed through the bitmap packing path are initialized.
@rustyconover

Copy link
Copy Markdown
Contributor Author

clang-tidy follow-up: the Valgrind failures are resolved by b4f4adc, but clang-tidy 18 could not prove that ArrowBitsUnpackInt8 initialized every element later read from the scratch buffer. Commit 0603bc1 explicitly zero-initializes that buffer; this preserves behavior while guaranteeing analyzer-visible initialization.

@paleolimbot paleolimbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A few more things from this pass but I think it's close! On the next pass I'll send the AIs over it as well to look for coverage or correctness I'm not sure about.

Comment thread src/nanoarrow/common/array.c Outdated
Comment thread src/nanoarrow/common/array.c Outdated
Comment thread src/nanoarrow/common/array.c
Comment thread src/nanoarrow/common/array.c
Comment thread src/nanoarrow/common/array.c Outdated
paleolimbot pushed a commit that referenced this pull request Sep 6, 2026
Adds the int16 counterpart to ArrowResolveChunk32 and
ArrowResolveChunk64.

This follows up on the run-end encoded array review in #930, where int16
run ends currently require a linear scan. The helper uses the same
binary-search semantics as the existing resolvers and includes
boundary-parity tests.

Validation:
- 270/270 C/C++ tests passed
- clang-format and git diff checks passed
@rustyconover

Copy link
Copy Markdown
Contributor Author

@paleolimbot I think this is ready for your AI based review.

@paleolimbot

Copy link
Copy Markdown
Member

Apologies for being slow here...I'll review when I'm back at work Tuesday!

@paleolimbot paleolimbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A few issues left:

  • Null storage appended to run-end encoded storage creates an invalid array. A run of nulls has to get added or you could also just reject this combination.
  • The conversion pathway is different in the nested case (e.g., list of decimal or struct containing decimals converts using the element-wise appender). I think the easiest way to do this is to update the signature of ArrowArrayAppendStorageFromArrayView() to accept an additional int64 offset and int64 length and recurse back using that (this is what the R and Python conversion functions do), but if there is another relatively non-verbose way to deal with that I'm happy to take a look.
  • I believe the compatibility check doesn't take into account struct names, and it probably should (appending xmin, xmax vs xmax, xmin is likely to be a problem should it be used for that).

When this merges I'll file a follow up for non-correctness issues that should get handled at some point, like the ability to handle non-storage conversions (e.g., appending timestamps with non-equal units), or speeding up conversions between integer widths / string on string appends.

@rustyconover

Copy link
Copy Markdown
Contributor Author

Addressed the latest review in 2ae9cd6.

  • Null storage is now rejected when the destination uses run-end encoded storage, before any destination mutation.
  • Nested struct/list/fixed-size-list children now recurse through an explicit offset/length range helper, so nested fixed-width storage such as decimals follows the same validated bulk path as top-level storage.
  • Added regressions for null-to-REE rejection and decimal storage nested in both a struct and a list.
  • For struct field names: ArrowArrayView does not carry field names, and this API intentionally operates on storage rather than logical schema metadata. I documented that struct children are matched positionally and that callers must validate field names (as well as decimal precision/scale). The public API signature remains unchanged; offset/length are internal to the recursive helper.

Validation:

  • Native CMake/CTest: 280/280 passed.
  • Arrow C++-gated CMake/CTest: 320/320 passed.

@paleolimbot paleolimbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A few remaining issues...thank you for sticking with this! This is hard...you are basically implementing cast to/from all arrow types. Also very useful (we can use to implement explicit batch sizes on write, casting, and delta dictionaries!).

Codex turned up a few more things:

  • self-append causes use-after-free. I think this one can just be a comment in the docs...it is not normal/supported behaviour to view an array that is in append mode.
  • Two REE-related issues: REE destination-length overflow is undefined behavior and the Null→REE fix does not cover REE nested beneath a null parent. We've gone pretty far down the supporting REEs here so it's probably worth fixing, but you could also not support REEs here.

static ArrowErrorCode ArrowArrayAppendStorageFromArrayViewRange(
struct ArrowArray* dst, const struct ArrowArrayView* src, int64_t offset,
int64_t length, struct ArrowError* error) {
struct ArrowArrayView src_slice = *src;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This line is a little janky (it would be cleaner to pass the offset and length through the internal calls), but also OK as long as it is qualified. I can see how it's possibly less error prone to apply the slice once.

Suggested change
struct ArrowArrayView src_slice = *src;
// Note: src_slice must not be freed: this is used as a convenience to reduce the
// number of times the offset and length must be compose on top of src.
struct ArrowArrayView src_slice = *src;

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.

Added the qualifying comment in 85ea0f7, including that the shallow slice must not be freed and why it is used.

Comment thread src/nanoarrow/common/array.c Outdated
Comment on lines +478 to +485
case NANOARROW_TYPE_INT16:
default: {
int64_t run = 0;
while (run + 1 < run_ends->length &&
ArrowArrayViewGetIntUnsafe(run_ends, run) <= logical_offset) {
run++;
}
return run;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you merge upstream/main and update this now that ArrowResolveChunk16() is merged?

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.

Merged current upstream/main in 9690f71 and replaced the int16 linear scan with ArrowResolveChunk16() in 85ea0f7. The REE append test now exercises int16 run ends.

Comment on lines +373 to +382
static int ArrowArrayCanAppendFixedWidthStorage(struct ArrowArray* dst,
const struct ArrowArrayView* src) {
struct ArrowArrayPrivateData* private_data =
(struct ArrowArrayPrivateData*)dst->private_data;
return private_data->storage_type == src->storage_type && dst->n_buffers == 2 &&
src->n_children == 0 &&
src->layout.buffer_type[1] == NANOARROW_BUFFER_TYPE_DATA &&
src->layout.element_size_bits[1] > 0 &&
src->layout.element_size_bits[1] % 8 == 0;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the other case worth handling out of the gate here (or one level up) is a struct appended to a struct, which I believe is trivial (feel free to punt if it is not).

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.

Added a bulk struct-to-struct path in 85ea0f7. It appends each child range and the parent validity in bulk; the regression test also covers a null struct slot with an REE child.

@rustyconover

rustyconover commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the remaining review points in 85ea0f7 (after merging current upstream/main in 9690f71):

  • documented that the source view must not reference destination-owned storage, since an append can reallocate it
  • qualified the shallow src_slice copy
  • switched int16 REE run lookup to ArrowResolveChunk16()
  • added bulk struct-to-struct appends, including valid child storage beneath null struct slots
  • reject other null appends before they can recursively create invalid REE child storage
  • guard REE destination length addition before any signed overflow or mutation

Added regression coverage for int16 REE lookup, struct/REE storage beneath a null parent, nested Null-to-REE rejection, and destination-length overflow.

Local validation: all 14 focused append tests pass; the full C/C++ suite passes 284/284 tests (3 expected thread-safety skips).

@paleolimbot paleolimbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Codex could only find one test coverage gap here (inline below). I'll open some follow on issues for R and Python integration (which should help get this code covered in a wider variety of scenarios). Thanks!

Comment on lines +5321 to +5326
TEST(ArrayTest, ArrayAppendStorageFromArrayViewRunEndEncoded) {
struct ArrowError error;
struct ArrowSchema schema;
ArrowSchemaInit(&schema);
ASSERT_EQ(ArrowSchemaSetTypeRunEndEncoded(&schema, NANOARROW_TYPE_INT16), NANOARROW_OK);
ASSERT_EQ(ArrowSchemaSetType(schema.children[1], NANOARROW_TYPE_STRING), NANOARROW_OK);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here the int32 REE and int64 REE branches aren't covered. I think a for() over NANOARROW_TYPE_INT16, NANOARROW_TYPE_INT32 and NANOARROW_TYPE_INT64 should cover those.

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.

3 participants