This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new a5158c8bf9 arrow-row: Fix decode_fixed_size_list to apply the
corrected_type step for dictionary children (#10414)
a5158c8bf9 is described below
commit a5158c8bf926bd74a01093a05187d25b2dfc1bc3
Author: Qi Zhu <[email protected]>
AuthorDate: Fri Jul 24 09:13:15 2026 +0800
arrow-row: Fix decode_fixed_size_list to apply the corrected_type step for
dictionary children (#10414)
# Which issue does this PR close?
Closes #10413.
# Rationale for this change
`RowConverter::convert_rows` used to fail with a schema-mismatch
`InvalidArgumentError` on any target type that contained a
`FixedSizeList<Dictionary<K, V>>`:
```text
InvalidArgumentError("FixedSizeListArray expected data type
Dictionary(Int32, Utf8) got Utf8 for \"item\"")
```
`decode_fixed_size_list` was building the returned array with the
declared `element_field` (which still carried `Dictionary<...>`) while
the children came from `converter.convert_raw`, which returns the
flattened (non-dictionary) type. `try_new_with_length` validates the
child type against `element_field` and returned `Err`.
The other list-like decoders — `List`, `LargeList`, `ListView`,
`LargeListView`, `Map` — already reconcile declared-vs-actual via a
`corrected_type` step (see `list::decode<L>` at [list.rs:~260 on
main](https://github.com/apache/arrow-rs/blob/main/arrow-row/src/list.rs#L259-L320)),
and `Codec::Struct` in `lib.rs` does the same via `corrected_fields`.
`decode_fixed_size_list` was the only list-like decoder that skipped
this pattern.
# What changes are included in this PR?
- `arrow-row/src/list.rs::decode_fixed_size_list`: adopt the
`corrected_type` step, mirroring the shape of every other list-like
decoder in the file.
-
`arrow-row/src/lib.rs::tests::test_fixed_size_list_of_dictionaries_round_trips`:
regression test that reproduces the original failure on one row `["a",
"b"]` of `FixedSizeList<Dictionary<Int32, Utf8>, 2>`.
Diff on the decoder is ~10 lines:
```rust
let mut children = unsafe { converter.convert_raw(&mut child_rows,
validate_utf8) }?;
assert_eq!(children.len(), 1);
// Since `RowConverter` flattens certain data types (i.e. `Dictionary`),
// we need to use the child's actual data type rather than the declared
// `element_field`'s. Mirrors the `corrected_type` logic in `decode<L>`
// above and `Codec::Struct`'s `corrected_fields` in `lib.rs`.
let corrected_element_field = Arc::new(
element_field
.as_ref()
.clone()
.with_data_type(children[0].data_type().clone()),
);
FixedSizeListArray::try_new_with_length(
corrected_element_field,
*size,
children.pop().unwrap(),
nulls,
num_rows,
)
```
# Are these changes tested?
Yes:
- `test_fixed_size_list_of_dictionaries_round_trips` — regression test
in `arrow-row/src/lib.rs`. Pre-fix it panics at the first
`convert_rows(...).unwrap()` (matching the report in #10413); post-fix
it returns a `FixedSizeList<Utf8>` (same flattened shape produced today
for `List<Dictionary<...>>`) whose values are `"a"`, `"b"`.
- All existing `arrow-row` tests continue to pass (`cargo test -p
arrow-row`).
- `cargo clippy -p arrow-row --all-targets -- -D warnings` clean.
# Are there any user-facing changes?
Behaviour change (bug fix): `RowConverter::convert_rows` now succeeds
where it previously returned an `InvalidArgumentError` for
`FixedSizeList<Dictionary<...>>` (and any type containing one, e.g.
`FSL<Struct<Dict>>`, `List<FSL<Dict>>`). The returned array flattens the
dictionary child to its native representation — same behaviour today's
`List<Dict>` / `LargeList<Dict>` / `Map<Dict>` decoders exhibit. Callers
that need the declared dictionary type back can re-encode after
`convert_rows` (that's the documented contract).
No public API surface changes.
# Downstream context
DataFusion hit this in https://github.com/apache/datafusion/pull/23523
(nested-type support in `GroupValuesColumn`) where a `GROUP BY fsl_col`
on `FixedSizeList<Dictionary<Int32, Utf8>>` would panic on emit. The
DataFusion PR is landing a defensive blacklist in the meantime; this fix
will let that blacklist be lifted in a follow-up.
---------
Co-authored-by: Jeffrey Vo <[email protected]>
---
arrow-row/src/lib.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++
arrow-row/src/list.rs | 11 ++++++++++-
2 files changed, 65 insertions(+), 1 deletion(-)
diff --git a/arrow-row/src/lib.rs b/arrow-row/src/lib.rs
index f1cb42d3d8..b647dab288 100644
--- a/arrow-row/src/lib.rs
+++ b/arrow-row/src/lib.rs
@@ -6169,6 +6169,61 @@ mod tests {
assert_eq!(&list, &back[0]);
}
+ /// Ensure dictionaries nested within FixedSizeLists are not flattened
+ #[test]
+ fn test_fixed_size_list_of_dictionaries_round_trips() {
+ // Build one row = ["a", "b"] as
+ // `FixedSizeList<Dictionary<Int32, Utf8>, 2>`.
+ let dict_dt = DataType::Dictionary(Box::new(DataType::Int32),
Box::new(DataType::Utf8));
+ let element_field = Arc::new(Field::new("item", dict_dt.clone(),
true));
+ let fsl_dt = DataType::FixedSizeList(Arc::clone(&element_field), 2);
+
+ let values = Arc::new(StringArray::from(vec!["a", "b"]));
+ let keys = Int32Array::from(vec![0, 1]);
+ let dict = DictionaryArray::<Int32Type>::try_new(keys,
values).unwrap();
+ let fsl: ArrayRef = Arc::new(FixedSizeListArray::new(
+ Arc::clone(&element_field),
+ 2,
+ Arc::new(dict),
+ None,
+ ));
+
+ assert!(RowConverter::supports_fields(&[SortField::new(
+ fsl_dt.clone()
+ )]));
+
+ let converter =
RowConverter::new(vec![SortField::new(fsl_dt.clone())]).unwrap();
+ let rows = converter.convert_columns(&[Arc::clone(&fsl)]).unwrap();
+
+ // Before the fix this panicked at the `.unwrap()` because
+ // `convert_rows` returned `Err(InvalidArgumentError(...))`.
+ let back = converter.convert_rows(&rows).unwrap();
+ assert_eq!(back.len(), 1);
+
+ // The returned array is a `FixedSizeList` with a decoded
+ // (flattened) child — same self-consistent shape the other
+ // list-like decoders produce for dictionary children.
+ let out = back[0]
+ .as_any()
+ .downcast_ref::<FixedSizeListArray>()
+ .expect("decoded array must be a FixedSizeListArray");
+ assert_eq!(out.len(), 1);
+ assert_eq!(out.value_length(), 2);
+ // Child data type is the flattened `Utf8`, not the declared
+ // `Dictionary`. Callers that want the dictionary back need to
+ // re-encode (see the module docs).
+ assert_eq!(out.values().data_type(), &DataType::Utf8);
+
+ // Sanity: values survived the round trip.
+ let values = out
+ .values()
+ .as_any()
+ .downcast_ref::<StringArray>()
+ .expect("child must be a StringArray after flattening");
+ assert_eq!(values.value(0), "a");
+ assert_eq!(values.value(1), "b");
+ }
+
// Test List<Null> with various combinations of nulls and empty lists
#[test]
fn test_list_null_variations() {
diff --git a/arrow-row/src/list.rs b/arrow-row/src/list.rs
index c35e614a9f..751ecc12ad 100644
--- a/arrow-row/src/list.rs
+++ b/arrow-row/src/list.rs
@@ -421,8 +421,17 @@ pub unsafe fn decode_fixed_size_list(
let mut children = unsafe { converter.convert_raw(&mut child_rows,
validate_utf8) }?;
assert_eq!(children.len(), 1);
+ // Since RowConverter flattens certain data types (i.e. Dictionary),
+ // we need to use updated data type instead of original field
+ let corrected_element_field = Arc::new(
+ element_field
+ .as_ref()
+ .clone()
+ .with_data_type(children[0].data_type().clone()),
+ );
+
FixedSizeListArray::try_new_with_length(
- Arc::clone(element_field),
+ corrected_element_field,
*size,
children.pop().unwrap(),
nulls,