[SPARK-58019][PYTHON] Convert Arrow list columns to Python rows in bulk#57099
[SPARK-58019][PYTHON] Convert Arrow list columns to Python rows in bulk#57099viirya wants to merge 9 commits into
Conversation
PyArrow's Array.to_pylist() materializes one Scalar per element; for list-typed columns each row additionally allocates a C++ scalar, a Python Scalar wrapper and a Python Array wrapper before converting elements one by one. This makes Arrow-optimized Python UDF inputs and Spark Connect collect() several times slower on array columns than necessary (apache/arrow#50326; a fix is proposed upstream in apache/arrow#50327 but will only be available in a future PyArrow release). Add ArrowTableToRowsConversion._to_pylist, which converts the flattened child values of a list column in a single pass and slices the resulting Python list per row using the offsets and the validity bitmap, and use it in the Arrow-to-rows conversion paths (Spark Connect collect, Arrow batch UDF inputs, Arrow UDTF inputs). Leaf values are still converted by Arrow's own to_pylist, so results are exactly identical - None stays None and values inside numeric lists stay Python ints, unlike a pandas round trip which coerces them to floats/NaN. NumPy is only used for the offsets and validity bitmap, never for the values. ASV microbenchmark (python/benchmarks/bench_arrow.py, 1M rows): list<string> 769ms -> 507ms (1.5x); list<list<int32>> with nulls 1.86s -> 537ms (3.5x). Peak memory unchanged. The helper can be removed once the minimum supported PyArrow version includes the upstream fix. Co-authored-by: Isaac
|
cc @Yicong-Huang FYI |
dbtsai
left a comment
There was a problem hiding this comment.
Reviewed the full diff. Nice, well-scoped optimization. I traced the correctness-critical _to_pylist path against to_pylist() semantics and it holds up on the edges that matter:
- Sliced arrays:
column.valuesreturns the full unsliced child andcolumn.offsetsare absolute into it, so readingstart = offsets[0], slicingvalues.slice(start, offsets[-1]-start), and rebasing every per-row slice by-startis correct. Using.valuesrather than.flatten()is the right call here --.flatten()would drop null-slot ranges and break the rebasing. - Null slots: Arrow does not guarantee a null list slot has an empty offset range, and keying None-ness off the validity bitmap (
valid[i]) rather than off an empty range handles that correctly. - Exact types: leaf values still go through Arrow's own
to_pylist; NumPy only touches offsets and the validity bitmap, solist<int32>[1, None, 3]stays ints -- the pandas round-trip float/NaN coercion is genuinely avoided. - Fallbacks: map / fixed_size_list / list_view don't match
is_list/is_large_listand fall through toto_pylist(). list_view is the important one (non-monotonic offsets would break the slicing), and it correctly does not take the fast path.
The test matrix -- comparing _to_pylist vs to_pylist() with exact element-type assertions across list/large_list/nested/struct/map/fixed_size, and across sliced and chunked views -- is exactly what's needed for a function that reimplements Arrow semantics.
Approve from my side. Two minor comments inline, both non-blocking.
| column | ||
| ) > 0: | ||
| n = len(column) | ||
| offsets = column.offsets.to_numpy(zero_copy_only=True).tolist() |
There was a problem hiding this comment.
Minor: zero_copy_only=True here is load-bearing and works only because list offset buffers never carry a validity bitmap. That invariant is correct, but non-obvious -- a one-line comment would stop a future reader from "fixing" it to zero_copy_only=False (and would flag the assumption if some new Arrow list variant ever violated it).
There was a problem hiding this comment.
Good point — added the comment in eebbc4f. zero_copy_only=True also doubles as an assertion: if a future Arrow list variant ever produced offsets with a validity bitmap, this would fail loudly instead of silently copying.
| if column.null_count == 0: | ||
| return [flat[offsets[i] - start : offsets[i + 1] - start] for i in range(n)] | ||
| valid = column.is_valid().to_numpy(zero_copy_only=False).tolist() | ||
| return [ |
There was a problem hiding this comment.
Minor (no change needed): the per-row slicing here is still O(rows) Python (offsets.tolist() + a comprehension slicing flat), so the win is fewer object allocations, not vectorization -- consistent with the benchmark numbers. Fine as the documented interim tradeoff; noting it so the "bulk" framing isn't read as fully vectorized.
There was a problem hiding this comment.
Right — row assembly is still an O(rows) Python loop; the win is that each row costs one list slice instead of a C++ scalar + Python Scalar wrapper + Python Array wrapper + generator. "Bulk" refers to the child-values conversion being a single call, not to vectorized row assembly. The fully vectorized fix is the upstream one (apache/arrow#50327); this stays intentionally simple as the interim.
Co-authored-by: Isaac
The files were formatted with black 26.3.1 (dev/requirements.txt), but the CI linter checks with ruff format, which disagrees on hunks unrelated to this change. Reformat with ruff 0.14.8 to match CI. Co-authored-by: Isaac
| @@ -844,6 +844,83 @@ def test_geometry_convert_numpy(self): | |||
| self.assertEqual(len(result), 0) | |||
|
|
|||
|
|
|||
There was a problem hiding this comment.
We need skipIf like the other arrow-based test cases.
| @unittest.skipIf(not have_pyarrow, pyarrow_requirement_message) |
dongjoon-hyun
left a comment
There was a problem hiding this comment.
+1, LGTM (with one comment about skipIf).
Co-authored-by: Isaac
…<struct> benchmarks Address review: cache the NumPy availability check in a module-level helper instead of re-running try/import on every (recursive) invocation, and extend the ASV benchmark with an array<struct> case plus peakmem variants for the nested cases (1M rows: nested list 862M -> 862M, array<struct> 885M -> 921M, ~4% peak increase from the transient flattened pointer list; leaf objects are shared). Co-authored-by: Isaac
…out per-element Scalars Spark 4.2 ships with this code frozen, so gate the pure-Python bulk conversion on the installed PyArrow version: releases containing the apache/arrow#50326 fix (planned for 25.0.1) convert natively without per-element Scalars and keep improving (apache/arrow#50448), so _to_pylist short-circuits to column.to_pylist() there and only uses the bulk paths on older PyArrow. The version constant can be bumped in a patch release if the fix ships elsewhere. Tests force the gate off so the bulk paths stay covered on any PyArrow, plus a gate pass-through test. Co-authored-by: Isaac
|
Added one more commit (2e405eb) after discussion: since Spark 4.2 ships with this code frozen, |
|
I will go to merge this. @gaogaotiantian Do you want to do a review before that? |
|
Please give me some time to review this. Will do this asap. |
|
Thanks @viirya for the fix! I will cut RC6 as soon as this is in. |
| return _pyarrow_native_to_pylist_is_fast | ||
|
|
||
|
|
||
| _numpy_available: Optional[bool] = None |
There was a problem hiding this comment.
change to has_numpy and np to align with other files? for reference: #57163
There was a problem hiding this comment.
Renamed the cached flag to has_numpy in 8e24c2f to align with #57163. I didn't cache an np module global though: unlike stateful_processor_api_client.py, this file never references the numpy module directly — numpy is only needed because pyarrow.Array.to_numpy() requires it — so np would have no user here.
gaogaotiantian
left a comment
There was a problem hiding this comment.
Overall I think this approach is good and the code is maintainable (also discardable in the future). The ROI of the patch is nice.
I have a few suggestions.
- Move all the
numpyandpyarrowversion check into a staticmethod ofLocalDataToArrowConversion. Name it something like_should_manual_bulk()(or whatever you think is proper). Do a@functools.cacheon the function so the actual import path is only triggered once. Let's leave no garbage in the module level block so it's easier to cleanly remove it in the future. - Comment wise, for both method, let's emphasize that this is an internal function that should not be used externally, and it should (not can) be removed when the minumum version contains the patch.
| result.extend(ArrowTableToRowsConversion._to_pylist(chunk)) | ||
| return result | ||
|
|
||
| column_type = column.type |
There was a problem hiding this comment.
I don't think this is really necessary.
There was a problem hiding this comment.
It is actually needed: ArrowTableToRowsConversion.convert passes table.columns, and pa.Table.columns are ChunkedArrays (the Spark Connect collect path). Without this branch a chunked list column falls into the list path and fails with AttributeError: 'pyarrow.lib.ChunkedArray' object has no attribute 'offsets'. The worker.py call sites pass RecordBatch columns (plain Arrays), so only the convert path exercises it; the chunked variants in ArrowColumnToPylistTests cover it.
There was a problem hiding this comment.
I meant assigning column.type to column_type. I think it's also some optimization that tries to save attribute access cost.
There was a problem hiding this comment.
Ah, I misread your comment as being about the ChunkedArray block above. Removed in a96c82b — column.type is used inline now.
…undant annotation, align has_numpy naming Co-authored-by: Isaac
|
Thanks @huaxingao! Addressing review comments now — will keep this moving so RC6 isn't blocked. |
…cached _should_manual_bulk staticmethod Co-authored-by: Isaac
|
@gaogaotiantian Both done in 5ce205e:
|
Co-authored-by: Isaac
### What changes were proposed in this pull request? Add `ArrowTableToRowsConversion._to_pylist`, which converts Arrow list-typed columns to Python values in bulk: the flattened child values are converted with a single recursive `to_pylist` call, and the resulting Python list is sliced per row using the offsets and the validity bitmap. It is used in the Arrow-to-rows conversion paths (Spark Connect `collect()`, Arrow batch UDF inputs, Arrow UDTF inputs). Non-list columns, map columns and environments without NumPy fall back to plain `column.to_pylist()`. Leaf values are still converted by Arrow's own `to_pylist`, so results are exactly identical to `column.to_pylist()`: `None` stays `None` and values inside numeric lists stay Python ints. NumPy is used only for the offsets (non-null integers) and the validity bitmap (booleans), never for the values, so the type coercion problems of a pandas round trip (`list<int32>` of `[1, None, 3]` becoming `[1.0, nan, 3.0]`) cannot occur. This is an interim measure for a PyArrow-side inefficiency: `Array.to_pylist()` materializes one Scalar per element, and for list types each row additionally allocates a C++ scalar, a Python Scalar wrapper and a Python Array wrapper before converting elements one by one (root-caused in apache/arrow#50326, fix proposed in apache/arrow#50327). The helper documents this and can be removed once the minimum supported PyArrow version includes the upstream fix. ### Why are the changes needed? Converting Arrow list columns to Python rows is the hot path of Arrow-optimized Python UDF inputs and Spark Connect `collect()`. With plain `to_pylist()` it is several times slower than necessary, which caused a performance regression on array columns when Arrow serialization became the default for regular Python UDFs. ASV microbenchmark (`python/benchmarks/bench_arrow.py::ArrowListColumnToRowsBenchmark`, added in this PR; 1M rows, macOS arm64, PyArrow 24.0.0): | case | `to_pylist()` | this PR | speedup | |---|---|---|---| | `list<string>` | 769 ms | 507 ms | 1.5x | | `list<list<int32>>` with nulls | 1.86 s | 537 ms | 3.5x | Peak memory is unchanged. ### Does this PR introduce _any_ user-facing change? No. Only performance; conversion results are byte-identical (covered by exact-type tests). ### How was this patch tested? New `ArrowColumnToPylistTests` in `python/pyspark/sql/tests/test_conversion.py`, comparing `_to_pylist` against `column.to_pylist()` with exact element-type assertions across list/large_list/nested/struct/map/fixed-size-list columns, sliced and chunked variants, plus a dedicated test that `list<int32>` of `[1, None, 3]` stays ints, and an end-to-end `ArrowTableToRowsConversion.convert` test with array columns. Full `test_conversion.py` passes. The new ASV benchmark class parametrizes `baseline` vs `bulk`, so the comparison above is reproducible with `./python/asv run --python=same --quick -b 'bench_arrow.ArrowListColumnToRowsBenchmark'`. ### Was this patch authored or co-authored using generative AI tooling? Yes. This pull request and its description were written by Isaac (Claude Code). Closes #57099 from viirya/arrow-to-pylist-shim. Authored-by: Liang-Chi Hsieh <viirya@gmail.com> Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com> (cherry picked from commit 484342a) Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com>
### What changes were proposed in this pull request? Add `ArrowTableToRowsConversion._to_pylist`, which converts Arrow list-typed columns to Python values in bulk: the flattened child values are converted with a single recursive `to_pylist` call, and the resulting Python list is sliced per row using the offsets and the validity bitmap. It is used in the Arrow-to-rows conversion paths (Spark Connect `collect()`, Arrow batch UDF inputs, Arrow UDTF inputs). Non-list columns, map columns and environments without NumPy fall back to plain `column.to_pylist()`. Leaf values are still converted by Arrow's own `to_pylist`, so results are exactly identical to `column.to_pylist()`: `None` stays `None` and values inside numeric lists stay Python ints. NumPy is used only for the offsets (non-null integers) and the validity bitmap (booleans), never for the values, so the type coercion problems of a pandas round trip (`list<int32>` of `[1, None, 3]` becoming `[1.0, nan, 3.0]`) cannot occur. This is an interim measure for a PyArrow-side inefficiency: `Array.to_pylist()` materializes one Scalar per element, and for list types each row additionally allocates a C++ scalar, a Python Scalar wrapper and a Python Array wrapper before converting elements one by one (root-caused in apache/arrow#50326, fix proposed in apache/arrow#50327). The helper documents this and can be removed once the minimum supported PyArrow version includes the upstream fix. ### Why are the changes needed? Converting Arrow list columns to Python rows is the hot path of Arrow-optimized Python UDF inputs and Spark Connect `collect()`. With plain `to_pylist()` it is several times slower than necessary, which caused a performance regression on array columns when Arrow serialization became the default for regular Python UDFs. ASV microbenchmark (`python/benchmarks/bench_arrow.py::ArrowListColumnToRowsBenchmark`, added in this PR; 1M rows, macOS arm64, PyArrow 24.0.0): | case | `to_pylist()` | this PR | speedup | |---|---|---|---| | `list<string>` | 769 ms | 507 ms | 1.5x | | `list<list<int32>>` with nulls | 1.86 s | 537 ms | 3.5x | Peak memory is unchanged. ### Does this PR introduce _any_ user-facing change? No. Only performance; conversion results are byte-identical (covered by exact-type tests). ### How was this patch tested? New `ArrowColumnToPylistTests` in `python/pyspark/sql/tests/test_conversion.py`, comparing `_to_pylist` against `column.to_pylist()` with exact element-type assertions across list/large_list/nested/struct/map/fixed-size-list columns, sliced and chunked variants, plus a dedicated test that `list<int32>` of `[1, None, 3]` stays ints, and an end-to-end `ArrowTableToRowsConversion.convert` test with array columns. Full `test_conversion.py` passes. The new ASV benchmark class parametrizes `baseline` vs `bulk`, so the comparison above is reproducible with `./python/asv run --python=same --quick -b 'bench_arrow.ArrowListColumnToRowsBenchmark'`. ### Was this patch authored or co-authored using generative AI tooling? Yes. This pull request and its description were written by Isaac (Claude Code). Closes #57099 from viirya/arrow-to-pylist-shim. Authored-by: Liang-Chi Hsieh <viirya@gmail.com> Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com> (cherry picked from commit 484342a) Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com>
…rows in bulk ### What changes were proposed in this pull request? Follow-up of SPARK-58019 (#57099); **only the last commit is new — this PR is stacked on #57099 and will be rebased once it merges.** (Originally stacked on SPARK-58023 (#57104), which has been closed in favor of the upstream apache/arrow#50326 fix; the numbers below are measured without it.) Extend `ArrowTableToRowsConversion._to_pylist` with bulk paths for struct and map columns: - **Struct** columns convert each child field in bulk (recursively reusing the bulk list paths), then zip the field values into one dict per row, masked by the validity bitmap. Duplicate field names fall back to `to_pylist`, so they raise the same `ValueError` that `StructScalar.as_py` raises. - **Map** columns share the list offsets layout: the flattened keys and items children are each converted in bulk, and every row becomes a list of `(key, value)` tuples, matching `MapScalar.as_py` exactly. ### Why are the changes needed? Struct and map columns still convert one Scalar per row; per-row Scalar/wrapper allocation dominates (apache/arrow#50326), and maps are the worst case since every row also wraps its keys/items in per-row Arrays. ASV microbenchmark (`bench_arrow.ArrowStructMapColumnToRowsBenchmark`, 1M rows, 10% nulls, PyArrow 24.0.0): | case | `to_pylist()` | this PR | speedup | |---|---|---|---| | `struct<int64,string>` | 970 ms | 474 ms | 2.0x | | `map<string,int64>` (2 entries/row) | 2.16 s | 799 ms | 2.7x | Peak memory unchanged. ### Does this PR introduce _any_ user-facing change? No. Only performance; conversion results are identical (covered by exact-type tests). ### How was this patch tested? Extended `ArrowColumnToPylistTests` with struct (incl. nested struct/list children, empty struct, all-null), map (incl. list values), struct-of-map, list-of-struct, sliced and chunked views — all compared against `column.to_pylist()` with exact element-type assertions — plus dedicated tests that duplicate struct field names still raise `ValueError` and that empty-struct rows are distinct dict objects. New ASV benchmark class `ArrowStructMapColumnToRowsBenchmark`. ### Was this patch authored or co-authored using generative AI tooling? Yes. This pull request and its description were written by Isaac (Claude Code). Closes #57105 from viirya/arrow-to-pylist-maps-structs. Authored-by: Liang-Chi Hsieh <viirya@gmail.com> Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com>
…rows in bulk ### What changes were proposed in this pull request? Follow-up of SPARK-58019 (#57099); **only the last commit is new — this PR is stacked on #57099 and will be rebased once it merges.** (Originally stacked on SPARK-58023 (#57104), which has been closed in favor of the upstream apache/arrow#50326 fix; the numbers below are measured without it.) Extend `ArrowTableToRowsConversion._to_pylist` with bulk paths for struct and map columns: - **Struct** columns convert each child field in bulk (recursively reusing the bulk list paths), then zip the field values into one dict per row, masked by the validity bitmap. Duplicate field names fall back to `to_pylist`, so they raise the same `ValueError` that `StructScalar.as_py` raises. - **Map** columns share the list offsets layout: the flattened keys and items children are each converted in bulk, and every row becomes a list of `(key, value)` tuples, matching `MapScalar.as_py` exactly. ### Why are the changes needed? Struct and map columns still convert one Scalar per row; per-row Scalar/wrapper allocation dominates (apache/arrow#50326), and maps are the worst case since every row also wraps its keys/items in per-row Arrays. ASV microbenchmark (`bench_arrow.ArrowStructMapColumnToRowsBenchmark`, 1M rows, 10% nulls, PyArrow 24.0.0): | case | `to_pylist()` | this PR | speedup | |---|---|---|---| | `struct<int64,string>` | 970 ms | 474 ms | 2.0x | | `map<string,int64>` (2 entries/row) | 2.16 s | 799 ms | 2.7x | Peak memory unchanged. ### Does this PR introduce _any_ user-facing change? No. Only performance; conversion results are identical (covered by exact-type tests). ### How was this patch tested? Extended `ArrowColumnToPylistTests` with struct (incl. nested struct/list children, empty struct, all-null), map (incl. list values), struct-of-map, list-of-struct, sliced and chunked views — all compared against `column.to_pylist()` with exact element-type assertions — plus dedicated tests that duplicate struct field names still raise `ValueError` and that empty-struct rows are distinct dict objects. New ASV benchmark class `ArrowStructMapColumnToRowsBenchmark`. ### Was this patch authored or co-authored using generative AI tooling? Yes. This pull request and its description were written by Isaac (Claude Code). Closes #57105 from viirya/arrow-to-pylist-maps-structs. Authored-by: Liang-Chi Hsieh <viirya@gmail.com> Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com> (cherry picked from commit 9c6e57b) Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com>
…rows in bulk ### What changes were proposed in this pull request? Follow-up of SPARK-58019 (#57099); **only the last commit is new — this PR is stacked on #57099 and will be rebased once it merges.** (Originally stacked on SPARK-58023 (#57104), which has been closed in favor of the upstream apache/arrow#50326 fix; the numbers below are measured without it.) Extend `ArrowTableToRowsConversion._to_pylist` with bulk paths for struct and map columns: - **Struct** columns convert each child field in bulk (recursively reusing the bulk list paths), then zip the field values into one dict per row, masked by the validity bitmap. Duplicate field names fall back to `to_pylist`, so they raise the same `ValueError` that `StructScalar.as_py` raises. - **Map** columns share the list offsets layout: the flattened keys and items children are each converted in bulk, and every row becomes a list of `(key, value)` tuples, matching `MapScalar.as_py` exactly. ### Why are the changes needed? Struct and map columns still convert one Scalar per row; per-row Scalar/wrapper allocation dominates (apache/arrow#50326), and maps are the worst case since every row also wraps its keys/items in per-row Arrays. ASV microbenchmark (`bench_arrow.ArrowStructMapColumnToRowsBenchmark`, 1M rows, 10% nulls, PyArrow 24.0.0): | case | `to_pylist()` | this PR | speedup | |---|---|---|---| | `struct<int64,string>` | 970 ms | 474 ms | 2.0x | | `map<string,int64>` (2 entries/row) | 2.16 s | 799 ms | 2.7x | Peak memory unchanged. ### Does this PR introduce _any_ user-facing change? No. Only performance; conversion results are identical (covered by exact-type tests). ### How was this patch tested? Extended `ArrowColumnToPylistTests` with struct (incl. nested struct/list children, empty struct, all-null), map (incl. list values), struct-of-map, list-of-struct, sliced and chunked views — all compared against `column.to_pylist()` with exact element-type assertions — plus dedicated tests that duplicate struct field names still raise `ValueError` and that empty-struct rows are distinct dict objects. New ASV benchmark class `ArrowStructMapColumnToRowsBenchmark`. ### Was this patch authored or co-authored using generative AI tooling? Yes. This pull request and its description were written by Isaac (Claude Code). Closes #57105 from viirya/arrow-to-pylist-maps-structs. Authored-by: Liang-Chi Hsieh <viirya@gmail.com> Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com> (cherry picked from commit 9c6e57b) Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com>
What changes were proposed in this pull request?
Add
ArrowTableToRowsConversion._to_pylist, which converts Arrow list-typed columns to Python values in bulk: the flattened child values are converted with a single recursiveto_pylistcall, and the resulting Python list is sliced per row using the offsets and the validity bitmap. It is used in the Arrow-to-rows conversion paths (Spark Connectcollect(), Arrow batch UDF inputs, Arrow UDTF inputs). Non-list columns, map columns and environments without NumPy fall back to plaincolumn.to_pylist().Leaf values are still converted by Arrow's own
to_pylist, so results are exactly identical tocolumn.to_pylist():NonestaysNoneand values inside numeric lists stay Python ints. NumPy is used only for the offsets (non-null integers) and the validity bitmap (booleans), never for the values, so the type coercion problems of a pandas round trip (list<int32>of[1, None, 3]becoming[1.0, nan, 3.0]) cannot occur.This is an interim measure for a PyArrow-side inefficiency:
Array.to_pylist()materializes one Scalar per element, and for list types each row additionally allocates a C++ scalar, a Python Scalar wrapper and a Python Array wrapper before converting elements one by one (root-caused in apache/arrow#50326, fix proposed in apache/arrow#50327). The helper documents this and can be removed once the minimum supported PyArrow version includes the upstream fix.Why are the changes needed?
Converting Arrow list columns to Python rows is the hot path of Arrow-optimized Python UDF inputs and Spark Connect
collect(). With plainto_pylist()it is several times slower than necessary, which caused a performance regression on array columns when Arrow serialization became the default for regular Python UDFs.ASV microbenchmark (
python/benchmarks/bench_arrow.py::ArrowListColumnToRowsBenchmark, added in this PR; 1M rows, macOS arm64, PyArrow 24.0.0):to_pylist()list<string>list<list<int32>>with nullsPeak memory is unchanged.
Does this PR introduce any user-facing change?
No. Only performance; conversion results are byte-identical (covered by exact-type tests).
How was this patch tested?
New
ArrowColumnToPylistTestsinpython/pyspark/sql/tests/test_conversion.py, comparing_to_pylistagainstcolumn.to_pylist()with exact element-type assertions across list/large_list/nested/struct/map/fixed-size-list columns, sliced and chunked variants, plus a dedicated test thatlist<int32>of[1, None, 3]stays ints, and an end-to-endArrowTableToRowsConversion.converttest with array columns. Fulltest_conversion.pypasses. The new ASV benchmark class parametrizesbaselinevsbulk, so the comparison above is reproducible with./python/asv run --python=same --quick -b 'bench_arrow.ArrowListColumnToRowsBenchmark'.Was this patch authored or co-authored using generative AI tooling?
Yes. This pull request and its description were written by Isaac (Claude Code).