Describe the bug, including details regarding any error messages, version, and platform
Serializing a table whose column is a registered ExtensionType with union storage through Arrow IPC (file or stream format) produces bytes in which the union's buffers are missing. Reading them back yields a table that fails validate(full=True) with Buffer #1 too small, and accessing the values (to_pylist) crashes the process with a segmentation fault. The table validates before writing, and the same union columns without the extension wrapper roundtrip correctly.
import io
import pyarrow as pa
import pyarrow.ipc
class MyExt(pa.ExtensionType):
def __init__(self, storage_type):
super().__init__(storage_type, "my.ext")
def __arrow_ext_serialize__(self):
return b""
@classmethod
def __arrow_ext_deserialize__(cls, storage_type, serialized):
return cls(storage_type)
storage = pa.UnionArray.from_dense(
pa.array([0, 1], type=pa.int8()),
pa.array([0, 0], type=pa.int32()),
[pa.array([1.5], type=pa.float64()), pa.array(["abc"], type=pa.large_string())],
)
pa.register_extension_type(MyExt(storage.type))
ext = pa.ExtensionArray.from_storage(MyExt(storage.type), storage)
t = pa.table({"x": ext})
t.validate(full=True) # passes; t.column("x").to_pylist() == [1.5, "abc"]
sink = io.BytesIO()
with pa.ipc.new_file(sink, t.schema) as w:
w.write_table(t)
t2 = pa.ipc.open_file(io.BytesIO(sink.getvalue())).read_all()
t2.validate(full=True)
# ArrowInvalid: Column 0: In chunk 0: Invalid: Buffer #1 too small in array of
# type extension<my.ext<MyExt>> and length 2: expected at least 2 byte(s), got 0
t2.column("x").to_pylist()
# Segmentation fault (exit code 139)
All eight combinations of {dense, sparse} union × {file, stream} IPC format × {extension-wrapped, bare}:
| storage |
column type |
format |
result |
| dense union |
extension |
file |
ArrowInvalid: Buffer #1 too small … |
| dense union |
extension |
stream |
ArrowInvalid: Buffer #1 too small … |
| sparse union |
extension |
file |
ArrowInvalid: Buffer #1 too small … |
| sparse union |
extension |
stream |
ArrowInvalid: Buffer #1 too small … |
| dense union |
bare |
file |
OK |
| dense union |
bare |
stream |
OK |
| sparse union |
bare |
file |
OK |
| sparse union |
bare |
stream |
OK |
The defect is on the write side: reading the same file in a fresh process in which my.ext is NOT registered falls back to the storage type (a plain dense_union column with ARROW:extension:name in the field metadata) and fails validate(full=True) with the same error, so the union's buffers are absent from the serialized bytes themselves rather than being lost during extension-type reconstruction.
Reproduced with pyarrow 23.0.1 and 25.0.0 (pip wheels), Python 3.10.20, macOS 26.5 arm64. Also observed through pyarrow.feather version 2 files, which use the IPC file format. The commit history of cpp/src/arrow/ipc/ contains no related fix after 25.0.0.
This looks like the same mechanism as #15068 (IPC roundtrip of an extension type with pa.null() storage reconstructs invalid buffers): both null and union are types whose buffer layout differs from the default — null has no buffers and unions have no validity buffer — and in both cases the extension wrapper breaks the IPC buffer accounting for them. Possibly one for the #49963 umbrella.
Context: encountered while property-based-testing the feather I/O of the Awkward Array library (scikit-hep/awkward).
Component(s)
C++, Python
🤖 Generated with Claude Code
Describe the bug, including details regarding any error messages, version, and platform
Serializing a table whose column is a registered
ExtensionTypewith union storage through Arrow IPC (file or stream format) produces bytes in which the union's buffers are missing. Reading them back yields a table that failsvalidate(full=True)withBuffer #1 too small, and accessing the values (to_pylist) crashes the process with a segmentation fault. The table validates before writing, and the same union columns without the extension wrapper roundtrip correctly.All eight combinations of {dense, sparse} union × {file, stream} IPC format × {extension-wrapped, bare}:
ArrowInvalid: Buffer #1 too small …ArrowInvalid: Buffer #1 too small …ArrowInvalid: Buffer #1 too small …ArrowInvalid: Buffer #1 too small …The defect is on the write side: reading the same file in a fresh process in which
my.extis NOT registered falls back to the storage type (a plaindense_unioncolumn withARROW:extension:namein the field metadata) and failsvalidate(full=True)with the same error, so the union's buffers are absent from the serialized bytes themselves rather than being lost during extension-type reconstruction.Reproduced with pyarrow 23.0.1 and 25.0.0 (pip wheels), Python 3.10.20, macOS 26.5 arm64. Also observed through
pyarrow.featherversion 2 files, which use the IPC file format. The commit history ofcpp/src/arrow/ipc/contains no related fix after 25.0.0.This looks like the same mechanism as #15068 (IPC roundtrip of an extension type with
pa.null()storage reconstructs invalid buffers): both null and union are types whose buffer layout differs from the default — null has no buffers and unions have no validity buffer — and in both cases the extension wrapper breaks the IPC buffer accounting for them. Possibly one for the #49963 umbrella.Context: encountered while property-based-testing the feather I/O of the Awkward Array library (scikit-hep/awkward).
Component(s)
C++, Python
🤖 Generated with Claude Code