Skip to content

Commit 473b9e9

Browse files
rustyconoverclaude
andcommitted
test: cover union argument boundary cases (null payload, non-contiguous codes, dense)
Add unit tests for _scalar_to_py/TaggedUnion exercising the previously untested branches: non-contiguous type codes (tag resolved via type_codes.index, not member position), dense unions (what DuckDB emits), and the null-valued union slot branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 56ef84b commit 473b9e9

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

‎tests/test_union_argument.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,61 @@ def test_union_arg_other_member() -> None:
4848
assert got.value == {"C": [1.0, 10.0], "kernel": ["rbf", "linear"]}
4949

5050

51+
def test_union_arg_null_payload() -> None:
52+
"""A null union slot decodes its value to None (the ``inner is None`` branch).
53+
54+
Note: pyarrow reports ``type_code == 0`` for any null union slot regardless of
55+
the codes buffer, so the active member's tag is not recoverable at the scalar
56+
level for a null payload — only the value is. (Tag preservation for a
57+
non-null payload of a non-zero-code member is covered above and through the
58+
batch-level round-trip in the echo integration suite.)
59+
"""
60+
scalar = _union_scalar(1, {"svc": None})
61+
got = Arguments(named={"config": scalar}).get("config")
62+
assert isinstance(got, TaggedUnion)
63+
assert got.value is None
64+
65+
66+
def test_union_arg_non_contiguous_type_codes() -> None:
67+
"""Tag resolution maps via type_codes, not member position.
68+
69+
Type codes need not be 0,1,2,...; ``_scalar_to_py`` must locate the active
70+
member by ``type_codes.index(code)`` rather than treating the code as a
71+
positional index.
72+
"""
73+
arr = pa.UnionArray.from_sparse(
74+
pa.array([9], type=pa.int8()),
75+
[
76+
pa.array([{"n_estimators": [1], "max_depth": [2]}], type=_RF),
77+
pa.array([{"C": [1.0], "kernel": ["rbf"]}], type=_SVC),
78+
],
79+
field_names=["random_forest_classifier", "svc"],
80+
type_codes=[5, 9],
81+
)
82+
got = Arguments(named={"config": arr[0]}).get("config")
83+
assert isinstance(got, TaggedUnion)
84+
assert got.tag == "svc"
85+
assert got.value == {"C": [1.0], "kernel": ["rbf"]}
86+
87+
88+
def test_union_arg_dense_union() -> None:
89+
"""Dense unions (what DuckDB emits) decode the same as sparse."""
90+
arr = pa.UnionArray.from_dense(
91+
pa.array([1], type=pa.int8()),
92+
pa.array([0], type=pa.int32()),
93+
[
94+
pa.array([], type=_RF),
95+
pa.array([{"C": [2.0], "kernel": ["linear"]}], type=_SVC),
96+
],
97+
field_names=["random_forest_classifier", "svc"],
98+
type_codes=[0, 1],
99+
)
100+
got = Arguments(named={"config": arr[0]}).get("config")
101+
assert isinstance(got, TaggedUnion)
102+
assert got.tag == "svc"
103+
assert got.value == {"C": [2.0], "kernel": ["linear"]}
104+
105+
51106
def test_non_union_args_unchanged() -> None:
52107
"""Non-union arguments still decode via plain as_py()."""
53108
args = Arguments(named={"n": pa.scalar(5), "s": pa.scalar("hi")}, positional=(pa.scalar(1.5),))

0 commit comments

Comments
 (0)