diff --git a/pyiceberg/io/pyarrow.py b/pyiceberg/io/pyarrow.py index 7fd0c85b77..1ddb925416 100644 --- a/pyiceberg/io/pyarrow.py +++ b/pyiceberg/io/pyarrow.py @@ -2111,7 +2111,8 @@ def list(self, list_type: ListType, list_array: pa.Array | None, value_array: pa if isinstance(value_array, pa.StructArray): # This can be removed once this has been fixed: # https://github.com/apache/arrow/issues/38809 - list_array = pa.LargeListArray.from_arrays(list_array.offsets, value_array) + # Pass the validity mask, otherwise null lists become empty lists + list_array = pa.LargeListArray.from_arrays(list_array.offsets, value_array, mask=list_array.is_null()) value_array = self._cast_if_needed(list_type.element_field, value_array) arrow_field = list_initializer(self._construct_field(list_type.element_field, value_array.type)) return list_array.cast(arrow_field) diff --git a/tests/integration/test_reads.py b/tests/integration/test_reads.py index a151d62b82..3e07c87b09 100644 --- a/tests/integration/test_reads.py +++ b/tests/integration/test_reads.py @@ -1001,10 +1001,7 @@ def test_null_list_and_map(catalog: Catalog) -> None: arrow_table = table_test_empty_list_and_map.scan().to_arrow() assert arrow_table["col_list"].to_pylist() == [None, []] assert arrow_table["col_map"].to_pylist() == [None, []] - # This should be: - # assert arrow_table["col_list_with_struct"].to_pylist() == [None, [{'test': 1}]] - # Once https://github.com/apache/arrow/issues/38809 has been fixed - assert arrow_table["col_list_with_struct"].to_pylist() == [[], [{"test": 1}]] + assert arrow_table["col_list_with_struct"].to_pylist() == [None, [{"test": 1}]] @pytest.mark.integration diff --git a/tests/io/test_pyarrow.py b/tests/io/test_pyarrow.py index e97b7813da..c3d798cbf7 100644 --- a/tests/io/test_pyarrow.py +++ b/tests/io/test_pyarrow.py @@ -3579,6 +3579,33 @@ def test__to_requested_schema_null_map_of_structs() -> None: assert result.column(1).to_pylist() == [[], None, [("b", 2), ("c", None)]] +def test__to_requested_schema_null_list_of_structs() -> None: + requested_schema = Schema( + NestedField( + 1, + "col_list_with_struct", + ListType( + 11, + StructType(NestedField(111, "source", StringType(), required=False)), + element_required=False, + ), + required=False, + ), + ) + arrow_schema = pa.schema([pa.field("col_list_with_struct", pa.list_(pa.struct([pa.field("source", pa.string())])))]) + batch = pa.RecordBatch.from_arrays( + [pa.array([[{"source": "x"}], [], None, [{"source": "y"}, None]], type=arrow_schema.field(0).type)], + schema=arrow_schema, + ) + + result = _to_requested_schema(requested_schema, requested_schema, batch) + assert result.column(0).to_pylist() == [[{"source": "x"}], [], None, [{"source": "y"}, None]] + + # A sliced batch has to keep the nulls as well + result = _to_requested_schema(requested_schema, requested_schema, batch.slice(1, 3)) + assert result.column(0).to_pylist() == [[], None, [{"source": "y"}, None]] + + @pytest.mark.parametrize( "arrow_type,iceberg_type,expected_arrow_type", [