This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-24405-c1b39bd503cefcc170748216495191b3cdf567ae in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit b551ba03753d719ff83a14b7f8f36bf667cdee67 Author: Liang-Chi Hsieh <[email protected]> AuthorDate: Sun Aug 16 07:04:16 2026 +0000 fix: skip WindowTopN rewrite when fetch is 0 to avoid PartitionedTopK panic (#24405) ## Which issue does this PR close? - Closes #24404. ## Rationale for this change With `datafusion.optimizer.enable_window_topn = true`, a query filtering a partitioned `ROW_NUMBER()`/`RANK()` with `rn < 1` (or the flipped `1 > rn`) panics with `PartitionedTopK requires k > 0`. `ROW_NUMBER`/`RANK` are always `>= 1`, so such a predicate matches no rows and the correct result is empty — it must not panic. The `WindowTopN` rule's `extract_window_limit` maps `rn < K` to a fetch of `K - 1`; for `K = 1` that fetch is `0`, which is passed to `PartitionedTopKExec::try_new`, whose `assert!(k > 0)` panics. With the optimization disabled the same query correctly returns no rows. ## What changes are included in this PR? - In `WindowTopN::try_transform`, bail out of the rewrite when the computed limit is `0`, letting the regular `FilterExec` produce the (empty) result. - Add regression cases to `window_topn.slt` for `rn < 1`, `1 > rn`, and `rn <= 0` (all must return empty without panicking). ## Are these changes tested? Yes — the new sqllogictest cases fail (panic) without the fix and pass with it. Existing `window_topn` tests continue to pass. ## Are there any user-facing changes? No API changes. `rn < 1` / `1 > rn` now returns an empty result instead of panicking when `enable_window_topn` is on, matching the behavior when it is off. --- datafusion/physical-optimizer/src/window_topn.rs | 9 +++++++++ datafusion/sqllogictest/test_files/window_topn.slt | 23 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/datafusion/physical-optimizer/src/window_topn.rs b/datafusion/physical-optimizer/src/window_topn.rs index 20bd8b0d38..aa46a5a9a7 100644 --- a/datafusion/physical-optimizer/src/window_topn.rs +++ b/datafusion/physical-optimizer/src/window_topn.rs @@ -141,6 +141,15 @@ impl WindowTopN { // Step 2: Extract limit from predicate (rn <= K, rn < K, etc.) let (col_idx, limit_n) = extract_window_limit(filter.predicate())?; + // A predicate such as `rn < 1` (or the flipped `1 > rn`) yields a fetch of + // 0. `ROW_NUMBER`/`RANK` are always >= 1, so no row can satisfy it and the + // correct result is empty. `PartitionedTopKExec` requires `k > 0` and would + // panic on `k = 0`, so bail out here and let the regular `FilterExec` produce + // the (empty) result instead of rewriting. + if limit_n == 0 { + return None; + } + // Step 3: Walk through optional ProjectionExec and RepartitionExec to find BoundedWindowAggExec let child = filter.input(); let (window_exec, intermediates) = find_window_below(child)?; diff --git a/datafusion/sqllogictest/test_files/window_topn.slt b/datafusion/sqllogictest/test_files/window_topn.slt index 44cb31153b..a9a52a654f 100644 --- a/datafusion/sqllogictest/test_files/window_topn.slt +++ b/datafusion/sqllogictest/test_files/window_topn.slt @@ -84,6 +84,29 @@ SELECT id, pk, val FROM ( 8 3 100 9 3 50 +# Test 3b: rn < 1 has fetch = 0. ROW_NUMBER is always >= 1 so no row qualifies and +# the result must be empty. The rewrite must not build a PartitionedTopKExec with k = 0 +# (which panics); it falls back to the regular filter. Regression for the k = 0 panic. +query III rowsort +SELECT id, pk, val FROM ( + SELECT *, ROW_NUMBER() OVER (PARTITION BY pk ORDER BY val) as rn FROM window_topn_t +) WHERE rn < 1; +---- + +# Test 3c: flipped form `1 > rn` (fetch = 0) — same empty result, no panic. +query III rowsort +SELECT id, pk, val FROM ( + SELECT *, ROW_NUMBER() OVER (PARTITION BY pk ORDER BY val) as rn FROM window_topn_t +) WHERE 1 > rn; +---- + +# Test 3d: rn <= 0 (fetch = 0) — empty, no panic. +query III rowsort +SELECT id, pk, val FROM ( + SELECT *, ROW_NUMBER() OVER (PARTITION BY pk ORDER BY val) as rn FROM window_topn_t +) WHERE rn <= 0; +---- + # Test 4: Without PARTITION BY — should NOT optimize query II rowsort SELECT id, val FROM ( --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
