[SPARK-58551][PYTHON] Python Data Sources Limit Pushdown API - #57752
Open
ganeshashree wants to merge 2 commits into
Open
[SPARK-58551][PYTHON] Python Data Sources Limit Pushdown API#57752ganeshashree wants to merge 2 commits into
ganeshashree wants to merge 2 commits into
Conversation
This PR adds LIMIT pushdown support to Python Data Sources, following the existing filter pushdown API (SPARK-51271). - A new optional `DataSourceReader.pushLimit(limit) -> bool` method. It is called once during planning, before `partitions()` and `read()`, and returns whether the reader will use the limit to read less data. - `PythonScanBuilder` now mixes in `SupportsPushDownLimit`. Because DSv2 calls `pushLimit` after `pushFilters`, and the planning worker exits between the two calls, the filters are replayed on a fresh reader so that it reaches the same state before the limit is pushed. The replayed filter decision is validated against the first pass and the query fails fast if they differ, since Spark has already committed to the first decision. - `isPartiallyPushed` returns true: arbitrary user Python is not trusted to return at most `limit` rows, so Spark always applies the LIMIT again. A pushed limit is therefore only a hint that lets the source read less data. - A pushed limit is reported as `PushedLimit` in the scan metadata. - New config `spark.sql.python.limitPushdown.enabled` (internal, default false), mirroring `spark.sql.python.filterPushdown.enabled`. - As with `pushFilters`, a reader that implements `pushLimit` while the config is disabled raises `DATA_SOURCE_PUSHDOWN_DISABLED` rather than having the method silently ignored. The message template is now parameterized by method name so it is accurate for both pushdowns. Python Data Sources cannot use a query's LIMIT to reduce the work they do. A reader that talks to a REST API or a database always plans its full set of partitions and reads at batch granularity (10,000 rows by default), so a `LIMIT 5` can still cost many requests or a full extract. Limit pushdown lets the source add a `LIMIT` clause or page size parameter, and plan fewer partitions, which is the same capability JVM DSv2 sources already have via `SupportsPushDownLimit`. Yes. `DataSourceReader.pushLimit` is new API, enabled by the new config. Data sources that do not implement it are unaffected. Note that a limit is only pushed down when every filter was pushed down, because Spark cannot apply a limit before a filter it still has to evaluate itself. Implementing `pushFilters` alongside `pushLimit` now also requires `pushFilters` to be deterministic; this is documented and enforced. New tests in `PythonDataSourceSuite` covering the pushed and not-pushed cases and the `PushedLimit` scan metadata, and new tests in `test_python_datasource.py` covering: a pushed limit reaching `partitions()` and `read()`; a reader declining the limit; a reader that accepts but ignores the limit (the query still returns exactly `n` rows); limits combined with filters; a post-scan filter blocking limit pushdown; non-deterministic `pushFilters` failing the query; `LIMIT 0`; the config disabled; and a reader that does not implement `pushLimit`. Yes, using Claude Code.
uros-b
reviewed
Aug 4, 2026
uros-b
reviewed
Aug 4, 2026
…ding policy Address review comments on apache#57752: - `branch-4.3` has already been cut, so this master-only change targets 5.0.0: update `spark.sql.python.limitPushdown.enabled`'s `.version()` and `pushLimit`'s `.. versionadded::`. - Declare `ConfigBindingPolicy.NOT_APPLICABLE` on the new config. Limit pushdown runs in the optimizer, so it cannot change what a view/UDF body resolves to. This also fixes SparkConfigBindingPolicySuite, which enforces that every new config declares a policy. Co-authored-by: Isaac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
This PR adds LIMIT pushdown to Python Data Sources, completing the pushdown family alongside filter pushdown (SPARK-51271).
A new optional DataSourceReader.pushLimit method:
It is called once during planning, before partitions() and read(), so a reader can use the limit to plan cheaper work:
Supporting changes:
Why are the changes needed?
Python Data Sources cannot use a query's LIMIT to reduce the work they do. A DataSourceReader plans its full set of partitions with no knowledge of the limit, and reads at Arrow batch granularity (10,000 rows by default), so LIMIT 5 over a REST or database-backed source can still cost many paged requests or a full extract. The reader has no way to add a LIMIT clause, set a page size parameter, or open fewer connections.
JVM DSv2 sources have had this via SupportsPushDownLimit since 3.3.0. This is the Python counterpart, alongside SPARK-51713 for column pruning.
Does this PR introduce any user-facing change?
Yes, new API. DataSourceReader.pushLimit is only called when the new spark.sql.python.limitPushdown.enabled config is true (default false), and readers that do not implement it are unaffected. This lands in an unreleased branch, so there is no change relative to any released version.
Two semantics deserve reviewer attention:
A limit is only pushed when every filter was pushed. Spark cannot apply a limit before a filter it must still evaluate itself, so any residual post-scan filter blocks limit pushdown. This is pre-existing shared DSv2 behavior, asserted for JDBC in JDBCV2Suite ("LIMIT is pushed down only if all the filters are pushed down"). Practically, a reader must also accept the IsNotNull filters Spark generates in order to combine filter and limit pushdown.
Implementing pushFilters alongside pushLimit requires pushFilters to be deterministic. The limit push replays the filters on a fresh reader, and by then Spark has already dropped the first pass's pushed filters from the plan. A reader reporting a different supported set the second time would leave Spark reading via the second reader while trusting the first decision — silently returning wrong rows. This is now validated, failing with a clear error rather than producing bad results. Both points are documented in the API docs.
How was this patch tested?
New tests in PythonDataSourceSuite: the pushed and not-pushed cases, that Spark retains its own limit operator, and PushedLimit scan metadata.
New tests in test_python_datasource.py:
Full suites pass: test_python_datasource (101), test_python_streaming_datasource (16), PythonDataSourceSuite (25).
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)