Describe the bug
RowConverter::convert_rows panics when the target schema contains a FixedSizeList whose element type is (or transitively contains) a Dictionary. The row-format encoder happily accepts these types (supports_fields returns true), but the reverse trip through convert_rows fails with a schema-mismatch:
InvalidArgumentError("FixedSizeListArray expected data type Dictionary(Int32, Utf8) got Utf8 for \"item\"")
The other list-like decoders — List, LargeList, ListView, LargeListView, and Map — round-trip cleanly on the same input. Only decode_fixed_size_list is affected.
To Reproduce
Encode a one-row FixedSizeList<Dictionary<Int32, Utf8>, 2> and convert it back:
use std::sync::Arc;
use arrow_array::{
ArrayRef, DictionaryArray, FixedSizeListArray, StringArray,
types::Int32Type,
};
use arrow_row::{RowConverter, SortField};
use arrow_schema::{DataType, Field};
fn main() {
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);
// supports_fields returns `true` — this schema is considered supported
assert!(RowConverter::supports_fields(&[SortField::new(fsl_dt.clone())]));
// Build one row = ["a", "b"] as FixedSizeList<Dict<Int32, Utf8>, 2>
let values = Arc::new(StringArray::from(vec!["a", "b"]));
let keys = arrow_array::Int32Array::from(vec![0, 1]);
let dict = DictionaryArray::<Int32Type>::try_new(keys, values).unwrap();
let fsl = FixedSizeListArray::new(Arc::clone(&element_field), 2, Arc::new(dict), None);
let converter = RowConverter::new(vec![SortField::new(fsl_dt.clone())]).unwrap();
let rows = converter.convert_columns(&[Arc::new(fsl) as ArrayRef]).unwrap();
// 💥 boom
let _ = converter.convert_rows(&rows).unwrap();
}
Panics on the last line with:
InvalidArgumentError("FixedSizeListArray expected data type Dictionary(Int32, Utf8) got Utf8 for \"item\"")
Same panic for any type containing an FSL<Dict> anywhere in its subtree — FSL<Struct<Dict>>, FSL<List<Dict>>, List<FSL<Dict>>, Struct<FSL<Dict>>, etc.
Expected behavior
Either
- (a)
convert_rows round-trips the dictionary correctly, producing a FixedSizeListArray whose element is Dictionary<Int32, Utf8> (matching the declared schema), or
- (b)
convert_rows returns an error rather than panicking, and RowConverter::supports_fields returns false for these shapes so callers can fall back.
Ideally (a) — the same behaviour the other list-like decoders already exhibit.
Root cause
arrow-row/src/list.rs::decode_fixed_size_list at line 424-430 on main builds the returned array with the declared element_field (which still carries Dictionary<...>) but the child data comes from converter.convert_raw, which returns the flattened (native) type:
let mut children = unsafe { converter.convert_raw(&mut child_rows, validate_utf8) }?;
assert_eq!(children.len(), 1);
FixedSizeListArray::try_new_with_length(
Arc::clone(element_field), // declared: Dict<Int32, Utf8>
*size,
children.pop().unwrap(), // actual: Utf8
nulls,
num_rows,
)
try_new_with_length validates that element_field.data_type() == child.data_type() and returns InvalidArgumentError. Callers that .expect() the result (as any consumer of RowConverter::convert_rows naturally does) get a panic.
For contrast, the generic list::decode<L: GenericListArrayOrMap> path (which handles List / LargeList / ListView / LargeListView / Map) already applies a corrected_type step to reconcile declared-vs-actual — see list.rs:259-320 on main. The Codec::Struct decode path in lib.rs does the same via corrected_fields.
decode_fixed_size_list is the only list-like decoder that skipped this pattern.
Proposed fix
Adopt the same corrected_type shape used elsewhere in the file:
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 instead of the
// declared element_field's — mirrors decode<L> at list.rs:259-320.
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,
)
Downstream callers that care about the declared type (e.g. DataFusion's RowsGroupColumn::rows_to_array) already have re-encoding helpers (encode_array_if_necessary) that map the flattened output back onto the declared dictionary type. What they need from arrow-row is a non-panicking, self-consistent decoded array — this fix delivers that.
Impact / who this affects
DataFusion hit this in #23523 (nested-type support in GroupValuesColumn) where a GROUP BY fsl_col with fsl_col: FixedSizeList<Dictionary<Int32, Utf8>> would panic on emit. Kosiew's review caught it during a repro; the DataFusion PR is landing a defensive blacklist in the meantime, but the right long-term fix is here in arrow-row.
Similar shapes exist in real workloads: sensor-vector columns (FixedSizeList<Dict<...>, N>), one-hot / embedding batches, and any Iceberg/DeltaLake table with fixed-arity vector columns backed by dictionary-encoded strings.
Version info
- Reproduces on
arrow-row = 59.1.0 and on current apache/arrow-rs:main (verified against arrow-row/src/list.rs on main).
Additional context
Happy to submit a PR with the fix + a round-trip regression test if the direction above looks right.
Describe the bug
RowConverter::convert_rowspanics when the target schema contains aFixedSizeListwhose element type is (or transitively contains) aDictionary. The row-format encoder happily accepts these types (supports_fieldsreturnstrue), but the reverse trip throughconvert_rowsfails with a schema-mismatch:The other list-like decoders —
List,LargeList,ListView,LargeListView, andMap— round-trip cleanly on the same input. Onlydecode_fixed_size_listis affected.To Reproduce
Encode a one-row
FixedSizeList<Dictionary<Int32, Utf8>, 2>and convert it back:Panics on the last line with:
Same panic for any type containing an
FSL<Dict>anywhere in its subtree —FSL<Struct<Dict>>,FSL<List<Dict>>,List<FSL<Dict>>,Struct<FSL<Dict>>, etc.Expected behavior
Either
convert_rowsround-trips the dictionary correctly, producing aFixedSizeListArraywhose element isDictionary<Int32, Utf8>(matching the declared schema), orconvert_rowsreturns an error rather than panicking, andRowConverter::supports_fieldsreturnsfalsefor these shapes so callers can fall back.Ideally (a) — the same behaviour the other list-like decoders already exhibit.
Root cause
arrow-row/src/list.rs::decode_fixed_size_listat line 424-430 on main builds the returned array with the declaredelement_field(which still carriesDictionary<...>) but the child data comes fromconverter.convert_raw, which returns the flattened (native) type:try_new_with_lengthvalidates thatelement_field.data_type() == child.data_type()and returnsInvalidArgumentError. Callers that.expect()the result (as any consumer ofRowConverter::convert_rowsnaturally does) get a panic.For contrast, the generic
list::decode<L: GenericListArrayOrMap>path (which handlesList/LargeList/ListView/LargeListView/Map) already applies acorrected_typestep to reconcile declared-vs-actual — see list.rs:259-320 on main. TheCodec::Structdecode path inlib.rsdoes the same viacorrected_fields.decode_fixed_size_listis the only list-like decoder that skipped this pattern.Proposed fix
Adopt the same
corrected_typeshape used elsewhere in the file:Downstream callers that care about the declared type (e.g. DataFusion's
RowsGroupColumn::rows_to_array) already have re-encoding helpers (encode_array_if_necessary) that map the flattened output back onto the declared dictionary type. What they need from arrow-row is a non-panicking, self-consistent decoded array — this fix delivers that.Impact / who this affects
DataFusion hit this in #23523 (nested-type support in
GroupValuesColumn) where aGROUP BY fsl_colwithfsl_col: FixedSizeList<Dictionary<Int32, Utf8>>would panic on emit. Kosiew's review caught it during a repro; the DataFusion PR is landing a defensive blacklist in the meantime, but the right long-term fix is here in arrow-row.Similar shapes exist in real workloads: sensor-vector columns (
FixedSizeList<Dict<...>, N>), one-hot / embedding batches, and any Iceberg/DeltaLake table with fixed-arity vector columns backed by dictionary-encoded strings.Version info
arrow-row = 59.1.0and on currentapache/arrow-rs:main(verified againstarrow-row/src/list.rson main).Additional context
Happy to submit a PR with the fix + a round-trip regression test if the direction above looks right.