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.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. 🚀 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.
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. |
paleolimbot
left a comment
There was a problem hiding this comment.
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.
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
|
@paleolimbot I think this is ready for your AI based review. |
|
Apologies for being slow here...I'll review when I'm back at work Tuesday! |
paleolimbot
left a comment
There was a problem hiding this comment.
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 additionalint64 offsetandint64 lengthand 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,xmaxvsxmax,xminis 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.
|
Addressed the latest review in 2ae9cd6.
Validation:
|
paleolimbot
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
Added the qualifying comment in 85ea0f7, including that the shallow slice must not be freed and why it is used.
| case NANOARROW_TYPE_INT16: | ||
| default: { | ||
| int64_t run = 0; | ||
| while (run + 1 < run_ends->length && | ||
| ArrowArrayViewGetIntUnsafe(run_ends, run) <= logical_offset) { | ||
| run++; | ||
| } | ||
| return run; |
There was a problem hiding this comment.
Can you merge upstream/main and update this now that ArrowResolveChunk16() is merged?
| 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; | ||
| } |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
|
Addressed the remaining review points in 85ea0f7 (after merging current upstream/main in 9690f71):
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
left a comment
There was a problem hiding this comment.
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!
| 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); |
There was a problem hiding this comment.
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.
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