feat: append array views to arrays - #930
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #930 +/- ##
==========================================
- Coverage 79.30% 78.06% -1.24%
==========================================
Files 106 106
Lines 16223 16484 +261
Branches 1882 1884 +2
==========================================
+ Hits 12865 12869 +4
- Misses 2177 2434 +257
Partials 1181 1181 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
paleolimbot
left a comment
There was a problem hiding this comment.
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.
| ArrowErrorCode ArrowArrayAppendArrayView(struct ArrowArray* dst, | ||
| const struct ArrowArrayView* src, | ||
| struct ArrowError* error) { | ||
| if (src->storage_type == NANOARROW_TYPE_RUN_END_ENCODED) { |
There was a problem hiding this comment.
At the top of this function we should sanity check that the append makes sense, because I am not sure that all combinations of spurious comparisons will generate a reasonable error here. ArrowArrayView only gives you storage type, so we probably should name the function accordingly (ArrowArrayAppendStorageFromArrayView()) to make it a bit more obvious to the caller that they're on their own with respect to whether they should do this or not.
Also a note that this needs to check src->dictionary, which I don't believe is reflected in the storage type (for better or worse).
There was a problem hiding this comment.
Addressed in bab29ad. The API is now ArrowArrayAppendStorageFromArrayView, with recursive preflight checks for compatible storage, child layouts, fixed-size widths, and dictionary presence. Dictionary values are deliberately caller-managed: both source and destination must be dictionary-encoded, and the API docs state that values are not copied. Follow-up 693718d narrows fixed-size layout equality checks to fixed-size-to-fixed-size appends and covers fixed-to-variable binary/list conversion.
| if (src->offset != 0) { | ||
| ArrowErrorSet(error, "Can't append a sliced run-end encoded array view"); | ||
| return ENOTSUP; | ||
| } |
There was a problem hiding this comment.
No need to get too into the weeds supporting this one, but we do have a utility that I think can resolve the first run:
arrow-nanoarrow/src/nanoarrow/common/inline_buffer.h
Lines 53 to 54 in f55f85b
There was a problem hiding this comment.
Addressed in bab29ad. Sliced run-end encoded views now resolve the first run with ArrowResolveChunk32 or ArrowResolveChunk64 (with the int16 fallback), clip the boundary runs to the slice, and rewrite run ends relative to the destination.
| static ArrowErrorCode ArrowArrayAppendArrayViewElement(struct ArrowArray* dst, | ||
| const struct ArrowArrayView* src, | ||
| int64_t i, | ||
| struct ArrowError* error) { |
There was a problem hiding this comment.
While we are here, I think it is worth the effort to special case primitive fixed-size appends of the same storage type (e.g., int32 getting appended to an int32). These should be much faster because they are just appending buffers to each other (the nulls bitmap is a bit more complex...may have to go through a uint8 buffer for the bitmap append if either the source or the target has any nulls).
There was a problem hiding this comment.
Addressed in 4974cd7. Matching byte-aligned primitive storage takes a bulk buffer-copy path. Validity is copied in bounded uint8 chunks so sliced bitmaps and all four source/destination bitmap-presence combinations are handled.
| NANOARROW_RETURN_NOT_OK(ArrowArrayAppendInt( | ||
| dst->children[0], | ||
| run_end_offset + ArrowArrayViewGetIntUnsafe(src->children[0], i))); |
There was a problem hiding this comment.
These (and below) should use NANOARROW_RETURN_NOT_OK_WITH_ERROR()...we are in general very careful about ensuring that the contents of error are populated if an error code is returned.
There was a problem hiding this comment.
| static ArrowErrorCode AppendArrayViewForTest(const struct ArrowArrayView* src, | ||
| struct ArrowArray* dst, | ||
| struct ArrowError* error) { |
There was a problem hiding this comment.
In addition to these lovely nanoarrow only tests, an Arrow C++ gated test I think would help cover some of the things that can happen here in a way that can be parameterized to cover more cases. Perhaps one test per type family (unigned int, signed int, strings, binaries, lists), testing against the Arrow C++ builder for the matrix of source and destination types? Arrow C++ makes asserting the equality a bit easier too.
There was a problem hiding this comment.
Addressed in e56f47b. Added Arrow C++-gated source/destination matrices for signed integers, unsigned integers, string/large_string, binary/large_binary, and list/large_list, with Arrow C++ equality checks on the imported result.
| TEST(ArrayTest, ArrayAppendArrayViewPrimitiveTypes) { | ||
| struct ArrowArray array; | ||
|
|
||
| ASSERT_EQ(ArrowArrayInitFromType(&array, NANOARROW_TYPE_INT64), NANOARROW_OK); |
There was a problem hiding this comment.
Some things that I am not sure are covered here are:
- Appending an integer that is out of range to a narrower integer type (should pass on the error from the integer appender)
- Appending too many elements to a narrower list type (you can use a list of Null type to avoid allocating huge buffers), which should pass on an error from the list element finisher
- Dictionary encoded source or target
There was a problem hiding this comment.
Addressed in e56f47b. Added tests for integer narrowing overflow, int32 list-offset overflow using a Null child, successful dictionary storage copying with caller-managed dictionary values, and rejection when only one side is dictionary-encoded.
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.
|
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.
|
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. |
Summary
ArrowArrayAppendArrayView()as a core array-building APIArrowArrayViewContext
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