fix: Concatenate null and dictionary arrays - #450
CurtHagenlocher wants to merge 2 commits into
Conversation
ArrayDataConcatenator had no case for NullType, so null arrays (and null-typed children such as struct fields) threw NotImplementedException. Dictionary arrays went through the fixed-width path, which concatenated the indices but dropped the dictionary. Null arrays now concatenate to a null array of the combined length. Dictionary arrays keep the dictionary when every non-empty input shares it; otherwise the dictionaries are concatenated and each input's indices are shifted past the entries before it, with an OverflowException if the combined dictionary can't be addressed by the index type. Closes apache#446. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The ordered-dictionary correctness issue and exceptional-path memory leak must be addressed before approval.
Review effort: Lite
Findings: 1
What changed in this PR
Fixes concatenation of null and dictionary arrays, including nested null fields and dictionary index remapping.
Changes:
- Adds null-array concatenation.
- Preserves shared dictionaries and combines distinct dictionaries with overflow checks.
- Adds regression coverage for nulls, dictionaries, slicing, and overflow.
| File | Summary | Findings |
|---|---|---|
src/Apache.Arrow/Arrays/ArrayDataConcatenator.cs |
Implements null and dictionary concatenation. | Critical (2 votes): Combined dictionaries can incorrectly retain the first input’s Ordered flag. Moderate (1 vote): Validity buffers may leak when later validation or concatenation throws. |
test/Apache.Arrow.Tests/ArrowArrayConcatenatorTests.cs |
Tests null, dictionary, slicing, and overflow behavior. | Nit (1 vote): Add coverage for shared dictionary memory across distinct ArrayData instances and verify retained lifetime. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| var otherType = (DictionaryType)arrayData.DataType; | ||
| if (otherType.IndexType.TypeId != indexType.TypeId) | ||
| { | ||
| throw new ArgumentException( | ||
| $"Cannot concatenate dictionary arrays with different index types: {indexType.Name} vs {otherType.IndexType.Name}"); |
There was a problem hiding this comment.
Good catch, fixed in e2d1b28. When different dictionaries are appended, the result is now never ordered: even if every input is ordered, the appended entries aren't necessarily in order, and nothing verifies that. When every input shares one dictionary, nothing is appended, so the result is ordered only if every input says so. That commit also moves the dictionary concatenation ahead of the buffer allocations and releases the buffers if building the indices throws, which covers the leak from the overview. It also adds a test for dictionaries held in separate ArrayData objects over the same memory that checks the result still works after the inputs are disposed.
Appending dictionaries end to end doesn't prove that the combined dictionary is ordered, so the result of that path is never ordered. When every input shares one dictionary, the result is ordered only if every input says so. Also concatenate the dictionaries before allocating the validity and index buffers, and release them if building the indices throws, so nothing leaks on the error paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

What's Changed
ArrayDataConcatenator(and soArrowArrayConcatenator) couldn't handle two array types:NotImplementedException, at the top level or as a child (e.g. a null-typed struct field, which Parquet readers hit for pyarrowpa.null()columns). They now concatenate to a null array of the combined length.Visit(FixedWidthType)becauseDictionaryTypederives fromFixedWidthType. That concatenated the indices but dropped the dictionary, soArrowArrayConcatenatorthrewDictionary must not be nullandArrayDataConcatenatorsilently returned dictionary-typed data with no dictionary.For dictionaries:
ArrayData, or distinctArrayDataover the same memory, e.g. afterRetain/SliceShared), the indices are concatenated and that dictionary is kept.OverflowExceptioninstead of wrapping around.ArgumentException.Dictionaries that are equal in content but stored separately take the concatenation path. The result is correct but not deduplicated. Unifying dictionaries could be added later if needed.
New tests cover null arrays, a struct with a null field, shared and different dictionaries (including nulls, slices and empty inputs), index overflow at the
int8boundary, theOrderedflag, andArrayDataConcatenatorkeeping the dictionary.Closes #446.
🤖 Generated with Claude Code