Skip to content

arrow-row: decode_fixed_size_list panics on FixedSizeList<Dictionary> (missing corrected_type) #10413

Description

@zhuqi-lucas

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions