Skip to content

[SPARK-58024][PYTHON] Convert Arrow struct and map columns to Python rows in bulk#57105

Closed
viirya wants to merge 2 commits into
apache:masterfrom
viirya:arrow-to-pylist-maps-structs
Closed

[SPARK-58024][PYTHON] Convert Arrow struct and map columns to Python rows in bulk#57105
viirya wants to merge 2 commits into
apache:masterfrom
viirya:arrow-to-pylist-maps-structs

Conversation

@viirya

@viirya viirya commented Jul 8, 2026

Copy link
Copy Markdown
Member

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).

@viirya viirya force-pushed the arrow-to-pylist-maps-structs branch 3 times, most recently from 8b51d7a to 1cf3d8a Compare July 8, 2026 06:34
@viirya viirya force-pushed the arrow-to-pylist-maps-structs branch from 1cf3d8a to f3340f4 Compare July 8, 2026 20:09
@viirya viirya force-pushed the arrow-to-pylist-maps-structs branch 5 times, most recently from 904fa86 to 96a606f Compare July 10, 2026 21:22
…rows in bulk

Extend ArrowTableToRowsConversion._to_pylist with bulk paths for struct
and map columns:

* Struct columns convert each child field in bulk (recursively reusing
  the list/leaf fast 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.

ASV microbenchmark (bench_arrow.ArrowStructMapColumnToRowsBenchmark,
1M rows, 10% nulls): struct<int64,string> 914ms -> 175ms (5.2x);
map<string,int64> with 2 entries per row 2.07s -> 303ms (6.8x). Peak
memory unchanged.

Co-authored-by: Isaac
@viirya viirya force-pushed the arrow-to-pylist-maps-structs branch from 96a606f to 40f92f7 Compare July 10, 2026 23:26
@viirya

viirya commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@huaxingao Can you hold 4.2 new RC cut until this gets merged? This is to extend #57099 to struct and map columns. Thanks.

@viirya viirya requested review from Yicong-Huang and gaogaotiantian and removed request for Yicong-Huang July 10, 2026 23:27
@viirya

viirya commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@dongjoon-hyun @Yicong-Huang @gaogaotiantian This extends the optimization to struct and map columns. Could you also take a look? Thanks!

Comment thread python/pyspark/sql/conversion.py Outdated
for i in range(n)
]

if pa.types.is_list(column.type) or pa.types.is_large_list(column.type):

@gaogaotiantian gaogaotiantian Jul 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could you change this if and the one below to elif? otherwise looks good to me.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fc45dd7 — both changed to elif.

@viirya viirya closed this in 9c6e57b Jul 11, 2026
viirya added a commit that referenced this pull request Jul 11, 2026
…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>
viirya added a commit that referenced this pull request Jul 11, 2026
…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>
@viirya

viirya commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

Merge Summary:

Posted by merge_spark_pr.py

@viirya

viirya commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

Thanks @gaogaotiantian

@viirya viirya deleted the arrow-to-pylist-maps-structs branch July 11, 2026 05:40
@viirya

viirya commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@huaxingao Both PRs are now merged into master: #57099 (SPARK-58019) and this #57105 (SPARK-58024). Nothing else is blocking on our side, so please feel free to proceed with the new 4.2 RC cut. Thank you for your patience and for holding the RC for these!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants