Repository navigation
[SPARK-60586][CONNECT] Export Arrow tables without IPC reserialization #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
234353b
d1b9f80
7bcf8e1
7c44352
a9a7169
8e368d4
ad7679c
33d1392
a2d251b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
| # | ||
|
|
||
| name: Python Arrow export | ||
|
|
||
| on: | ||
| push: | ||
| branches: [master, "branch-*"] | ||
| pull_request: | ||
| branches: [master, "branch-*"] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| converter: | ||
| name: Python Arrow export | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.11" | ||
| - name: Install Rust | ||
| run: | | ||
| rustup toolchain install stable --profile minimal --component clippy | ||
| rustup default stable | ||
| - name: Install PyArrow | ||
| run: python -m pip install "pyarrow==26.0.0" | ||
|
Comment on lines
+49
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| - name: Test Arrow table export | ||
| # Test separately from pyspark-rs: extension-module feature unification | ||
| # would disable libpython linking for the embedded Python tests. | ||
| run: | | ||
| cargo test -p spark-python-arrow --locked --features pyo3/abi3-py39 | ||
| - name: Check converter Clippy | ||
| run: | | ||
| cargo clippy -p spark-python-arrow \ | ||
| --locked --all-targets --features pyo3/abi3-py39 -- -D warnings | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
| [package] | ||
| name = "spark-python-arrow" | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
| rust-version = "1.85" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that the crate is no longer built outside the workspace, |
||
| license = "Apache-2.0" | ||
| publish = false | ||
| description = "Arrow record batch export to PyArrow for Spark clients" | ||
| repository = "https://github.com/apache/spark-connect-rust" | ||
|
|
||
| [dependencies] | ||
| arrow = { version = "58", default-features = false, features = ["ffi", "ipc"] } | ||
| # The consumer chooses the Python ABI floor and embedding/extension-module linking. | ||
| pyo3 = { version = "0.28" } | ||
|
|
||
| [lints.rust] | ||
| unsafe_op_in_unsafe_fn = "deny" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| <!-- | ||
| Licensed to the Apache Software Foundation (ASF) under one | ||
| or more contributor license agreements. See the NOTICE file | ||
| distributed with this work for additional information | ||
| regarding copyright ownership. The ASF licenses this file | ||
| to you under the Apache License, Version 2.0 (the | ||
| "License"); you may not use this file except in compliance | ||
| with the License. You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, | ||
| software distributed under the License is distributed on an | ||
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| KIND, either express or implied. See the License for the | ||
| specific language governing permissions and limitations | ||
| under the License. | ||
| --> | ||
|
|
||
| # Arrow table export to PyArrow | ||
|
|
||
| `spark-python-arrow` exports decoded Rust Arrow record batches to PyArrow for | ||
| `pyspark-rs`. It has one public entry point, | ||
| `bridge::record_batches_to_pyarrow`, used by `DataFrame.toArrow()`. It does not | ||
| depend on Spark Connect transport, generated protobufs, or an async runtime. | ||
|
|
||
| Python-to-Rust Arrow imports, standalone array/type exports, and worker-side UDF | ||
| conversion are outside this change. Scalar input conversion and row | ||
| materialization remain in `pyspark-rs` until they have another concrete consumer. | ||
|
|
||
| ## Export contract | ||
|
|
||
| The C Data Interface path preserves batch boundaries, schema and field metadata, | ||
| and zero-column row counts. It shares compatible Arrow value and offset buffers | ||
| with PyArrow. The caller supplies valid Rust record batches and owns its Python | ||
| interpreter; the library does not initialize Python or retain session state. | ||
|
|
||
| PyArrow consumes the exported descriptors and retains their release callbacks, | ||
| so the data survives after Rust's temporary wrappers have been dropped. If an | ||
| importer raises before consuming a descriptor, Rust releases it. PyArrow and | ||
| Python code in the same interpreter are trusted, including the public aliases | ||
| used by the export path. | ||
|
|
||
| Field names or timezone strings containing NUL use an IPC compatibility fallback | ||
| over the already collected batches, without another RPC. Length-prefixed schema | ||
| metadata can contain NUL without this fallback. The outbound schema helpers also | ||
| retain sorted-map and dictionary-order flags when present. | ||
|
|
||
| Table export raises `ValueError` if batches have inconsistent schemas, including | ||
| field metadata or dictionary ordering. This is stricter than the previous IPC | ||
| export path, which wrote batches under the first batch's schema. An empty batch | ||
| vector still produces an empty table with an empty schema; preserving schemas | ||
| from schema-only IPC streams requires a separate change to the decoding path. | ||
|
|
||
| Zero-copy describes compatible Arrow **value and offset buffers**, not schema | ||
| descriptors, Python scalar/row objects, pandas materialization, IPC decoding, or | ||
| the IPC fallback. Sliced arrays whose value and null offsets differ can require | ||
| copying the validity bitmap during C Data export. | ||
|
|
||
| ## Integration | ||
|
|
||
| This is an unpublished workspace implementation crate (`publish = false`), not | ||
| a separately released package. `pyspark-rs` uses a path dependency. The current | ||
| package name and `0.1.0` version do not identify a crates.io release; independent | ||
| publication would require Apache package naming and release-flow integration. | ||
| Source-based consumers can pin a repository revision and validate it in their | ||
| own dependency graph. The repository's root `LICENSE` covers this crate. | ||
|
|
||
| The library does not enable PyO3's `extension-module` feature or select a minimum Python ABI; | ||
| `pyspark-rs` selects these features for the Python extension. | ||
|
|
||
| PyO3 must resolve to one compatible version in each consuming dependency graph. | ||
| The manifest uses the same PyO3 0.28 line as the Python extension, with 0.28.3 | ||
| pinned in the workspace lockfile. This change does not upgrade PyO3. | ||
| This crate declares Rust 1.85, matching Arrow 58.3.0's declared minimum. | ||
| The existing workspace default of 1.82 is unchanged; it is not a claim that | ||
| these Python/Arrow packages build on 1.82. The minimum compiler has not been | ||
| independently tested locally. | ||
|
|
||
| The Python Arrow export CI job tests this crate with the workspace lockfile. | ||
| These tests run separately from the extension build and are not repeated in coverage. | ||
|
|
||
| ## Tests | ||
|
|
||
| With PyArrow installed in the Python environment used by PyO3: | ||
|
|
||
| ```sh | ||
| CARGO_NET_OFFLINE=true CARGO_REGISTRIES_CRATES_IO_PROTOCOL=git \ | ||
| cargo test -p spark-python-arrow --locked --features pyo3/abi3-py39 | ||
|
Comment on lines
+88
to
+89
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This command differs from what CI runs ( |
||
| ``` | ||
|
|
||
| Run this package separately from the extension's test/build command: Cargo | ||
| feature unification with an extension-module consumer can disable libpython | ||
| linking required by the embedded test executable. For a virtual environment, | ||
| set `PYO3_PYTHON` to its interpreter and expose its site-packages to the embedded | ||
| interpreter with `PYTHONPATH` when necessary. | ||
|
|
||
| The standalone tests exercise the table export entry point: buffer identity and | ||
| lifetime, schema metadata, slices, chunks, empty shapes, schema mismatch errors, | ||
| and NUL fallback. The Python client's `test_arrow_handoff.py` exercises the public | ||
| `toArrow()` and `toPandas()` paths, including importer failures. Scalar regressions | ||
| live with the client tests. These are not substitutes for Spark's complete | ||
| Connect parity suite. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This installs latest stable and then runs clippy with
-D warnings(L59), so the job will start failing whenever a new stable release adds lints, even if this crate hasn't changed. Could it pin a toolchain, or match the workspace lint job's setup?