mkzung opened a new pull request, #50475:
URL: https://github.com/apache/arrow/pull/50475
### Rationale for this change
An unsigned dictionary index type is silently replaced by the signed one of
the same width:
```python
>>> pa.array(["a", "b"], type=pa.dictionary(pa.uint32(), pa.string())).type
dictionary<values=string, indices=int32, ordered=0>
```
It also makes `pa.chunked_array(values, dict_type)` fail with
`ArrowTypeError: Array chunks must all be same type`, because the chunk it
builds internally comes back with a different type than the one requested.
The cause is in `DictionaryBuilderCase::CreateFor()` in `builder.cc`. The
requested index type is reduced to `index_type->byte_width()` and handed to
`AdaptiveIntBuilder` as a starting width, so everything except the width is
dropped, and the dictionary type is then rebuilt from whatever the indices
builder reports. That builder only ever reports signed types.
@jorisvandenbossche suggested using `AdaptiveUIntBuilder` for unsigned index
types. I tried that first and it does not work: it makes
`MakeDictionaryBuilder` return a different builder class, and
`util/converter.h`, `json/from_string.cc` and the R binding all cast the result
to `DictionaryBuilder<T>`. `TestDictionaryUnifier.ChunkedArrayNestedDict` dies
on a `checked_pointer_cast` DCHECK as soon as you try it.
So this keeps one builder class and preserves the requested signedness where
the index type is reported. Indices are non-negative and the signed and
unsigned types of a given width have the same layout, so reporting one as the
other is value-preserving and free. The width stays adaptive, as it is for
signed index types.
### What changes are included in this PR?
`DictionaryBuilderBase` and its `NullType` specialization record whether an
unsigned index type was requested and map the indices builder's signed type to
the unsigned one of the same width in `type()`, `FinishInternal()` and
`FinishDelta()`. `CreateFor()` passes the signedness through instead of
discarding it.
`util/converter.h`, `python_to_arrow.cc` and `r_to_arrow.cpp` are not
touched, so R is fixed rather than broken.
### Are these changes tested?
Ten tests in `array_dict_test.cc` and four parametrized ones in
`test_array.py`, covering all four unsigned index types, a signed regression
guard, nulls, `FinishDelta`, the supplied-dictionary constructor,
fixed-size-binary values, `ordered` together with an unsigned index, and the
exact-index builder. Eight of the ten C++ tests fail without the change.
The width still adapts: a requested `uint8` grows to `uint16` and then
`uint32` as the dictionary grows, and stays unsigned at each step.
`arrow-array-test` passes (1056), as do `arrow-compute-scalar-cast-test` and
`arrow-c-bridge-test`, and the pyarrow suites are unchanged from main.
One thing I noticed while in here: the supplied-dictionary constructor
starts the indices builder at its default width rather than the requested one,
so the requested width is dropped on that path. That is not new, a requested
`int32` reports `int8` there on main too. This preserves the signedness there
and leaves the width alone.
### Are there any user-facing changes?
`pa.array` and `pa.chunked_array` now return the unsigned index type that
was requested.
One consequence is worth flagging. `dictionary(uint64(), ...)` now genuinely
produces uint64 indices, and `arrow_to_pandas.cc` deliberately refuses to
convert those ("Converting UInt64 dictionary indices to pandas is not
supported", since pandas categorical codes are signed). So `to_pandas()` on
such an array raises now, where before it worked by accident on int32 indices.
uint8, uint16 and uint32 are unaffected. I think that is the bug surfacing
rather than a new one, but adding uint64 support to the pandas conversion may
be worth a follow-up, and there is a test pinning the current behaviour.
I checked the rest of the surface for every unsigned width: `take`,
`filter`, `cast`, `dictionary_decode`, IPC, a Parquet write and read back, the
compute kernels and an Acero `group_by` all work and keep the index type.
Pandas conversion of uint64 is the only thing that changes.
### AI usage
I used an AI coding assistant while working on this, to help track down the
root cause and to draft tests. I built Arrow and pyarrow from source, ran the
C++ and Python suites and the lint hooks myself, and I understand and take
responsibility for every line.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]