|
5 | 5 | of truth; these tests are regression guards that will fail immediately if the |
6 | 6 | Python implementation diverges. |
7 | 7 |
|
8 | | -Covers 37+ distinct Arrow tables / arrays across categories: |
| 8 | +Covers 40+ distinct Arrow tables / arrays across categories: |
9 | 9 | - Empty arrays (0 elements) |
10 | 10 | - All-null arrays |
11 | 11 | - Large boolean arrays (multi-byte bitvec) |
|
14 | 14 | - Three-level nested structs |
15 | 15 | - Struct with list child |
16 | 16 | - List<Struct>, nullable List<Struct> |
| 17 | + - List<Struct> with nullable vs non-nullable item field (PLT-1048 edge case) |
17 | 18 | - Non-nullable lists |
18 | 19 | - Mixed-type structs |
19 | 20 | - Mixed struct+list record batches |
@@ -899,6 +900,79 @@ def test_multi_batch_list(self): |
899 | 900 | # --------------------------------------------------------------------------- |
900 | 901 |
|
901 | 902 |
|
| 903 | +# --------------------------------------------------------------------------- |
| 904 | +# LargeList<Struct> with nullable vs non-nullable inner (item) field |
| 905 | +# |
| 906 | +# This exercises the structural-only BTreeMap entry created for List<Struct>. |
| 907 | +# When inner_field.nullable=True the entry's BitVec is allocated but never |
| 908 | +# populated (bit_count=0), so 8 zero bytes are prepended to the structural |
| 909 | +# digest at finalisation. Both Rust and Python handle this identically. |
| 910 | +# Golden values verified via `cargo test list_struct_nullable_inner_field_parity` |
| 911 | +# against the Rust starfix crate (PLT-1048). |
| 912 | +# --------------------------------------------------------------------------- |
| 913 | + |
| 914 | + |
| 915 | +class TestListStructNullableInnerField: |
| 916 | + """LargeList<Struct> — nullable vs non-nullable item field parity.""" |
| 917 | + |
| 918 | + _ids = [1, 2, 3, 4] |
| 919 | + _labels = ["a", "b", "c", "d"] |
| 920 | + |
| 921 | + def _make_list_array(self, inner_nullable: bool) -> "pa.LargeListArray": |
| 922 | + ids = pa.array(self._ids, type=pa.int32()) |
| 923 | + labels = pa.array(self._labels, type=pa.large_utf8()) |
| 924 | + struct_fields = [ |
| 925 | + pa.field("id", pa.int32(), nullable=False), |
| 926 | + pa.field("label", pa.large_utf8(), nullable=False), |
| 927 | + ] |
| 928 | + struct_arr = pa.StructArray.from_arrays( |
| 929 | + [ids, labels], |
| 930 | + fields=struct_fields, |
| 931 | + ) |
| 932 | + inner_field = pa.field("item", pa.struct(struct_fields), nullable=inner_nullable) |
| 933 | + offsets = pa.array([0, 2, 4], type=pa.int64()) |
| 934 | + return pa.LargeListArray.from_arrays( |
| 935 | + offsets, |
| 936 | + struct_arr, |
| 937 | + type=pa.large_list(inner_field), |
| 938 | + ) |
| 939 | + |
| 940 | + def test_nullable_inner_field(self): |
| 941 | + """LargeList<Struct> with nullable=True item field. |
| 942 | +
|
| 943 | + ids=[1,2,3,4], labels=['a','b','c','d'], offsets=[0,2,4] |
| 944 | + inner_field.nullable=True → structural-only entry has empty BitVec. |
| 945 | + Golden value confirmed against Rust (PLT-1048). |
| 946 | + """ |
| 947 | + result = ArrowDigester.hash_array(self._make_list_array(inner_nullable=True)).hex() |
| 948 | + assert result == "0000012c2b1c1d5b4c3dc46ed5335834dbf0d7386c38e607a398a2897fdaf1df387e1c" |
| 949 | + |
| 950 | + def test_non_nullable_inner_field(self): |
| 951 | + """LargeList<Struct> with nullable=False item field. |
| 952 | +
|
| 953 | + ids=[1,2,3,4], labels=['a','b','c','d'], offsets=[0,2,4] |
| 954 | + inner_field.nullable=False → no BitVec in structural-only entry. |
| 955 | + Golden value confirmed against Rust (PLT-1048). |
| 956 | + """ |
| 957 | + result = ArrowDigester.hash_array(self._make_list_array(inner_nullable=False)).hex() |
| 958 | + assert result == "000001713fa0e500c9aebea61039b30371fd84c0dff8cd3b96b4266978658bf73e4d8c" |
| 959 | + |
| 960 | + def test_nullable_differs_from_non_nullable(self): |
| 961 | + """Nullable vs non-nullable inner field must produce different hashes.""" |
| 962 | + hash_nullable = ArrowDigester.hash_array(self._make_list_array(inner_nullable=True)).hex() |
| 963 | + hash_not_nullable = ArrowDigester.hash_array( |
| 964 | + self._make_list_array(inner_nullable=False) |
| 965 | + ).hex() |
| 966 | + assert hash_nullable != hash_not_nullable, ( |
| 967 | + "nullable and non-nullable item fields must produce distinct hashes" |
| 968 | + ) |
| 969 | + |
| 970 | + |
| 971 | +# --------------------------------------------------------------------------- |
| 972 | +# Float special values |
| 973 | +# --------------------------------------------------------------------------- |
| 974 | + |
| 975 | + |
902 | 976 | class TestFloatSpecialValues: |
903 | 977 | def test_float64_special_values(self): |
904 | 978 | """Float64 array with NaN, Inf, -Inf, 0.0, -0.0.""" |
|
0 commit comments