Skip to content

fix: return no rows from TopK with LIMIT 0 instead of panicking - #26432

Open
ump45nose wants to merge 1 commit into
apache:mainfrom
ump45nose:fix/topk-limit-zero
Open

ump45nose wants to merge 1 commit into
apache:mainfrom
ump45nose:fix/topk-limit-zero

Conversation

@ump45nose

Copy link
Copy Markdown

Which issue does this PR close?

Rationale for this change

ORDER BY ... LIMIT 0 panics with assertion failed: k > 0 when the plan reaches SortExec: TopK(fetch=0). The default optimizer hides this because eliminate_limit replaces LIMIT 0 with an empty relation, but it is reachable with datafusion.optimizer.max_passes = 0, with custom optimizer rules, or by building SortExec::with_fetch(Some(0)) directly. A LIMIT 0 query should return no rows, not panic.

What changes are included in this PR?

  • TopKHeap::new no longer asserts k > 0.
  • TopK::try_new marks a k = 0 TopK as finished up front, and TopK::insert_batch returns early when k = 0 (no row can enter the heap).
  • SortExec checks topk.finished before polling the next input batch, so a fetch = 0 TopK does not read its input. The loop behaves the same as before for k > 0.

The fix is in TopK itself, not a separate empty-stream branch in SortExec. This way emit() still runs and marks the shared TopK dynamic filter as emitted/complete, as it does for any other TopK.

What is the testing strategy for this PR?

  • New unit test topk::tests::test_topk_with_zero_k: a k = 0 TopK is finished at construction, ignores an inserted batch, emits no rows, and marks its dynamic filter complete. Without this change it panics in TopKHeap::new.
  • New topk.slt case that sets datafusion.optimizer.max_passes = 0, checks that the plan is SortExec: TopK(fetch=0), and checks that select * from topk order by x limit 0 returns no rows.
  • Locally: cargo test -p datafusion-physical-plan --lib -- topk sorts::, the topk.slt, limit*.slt and order.slt sqllogictests, cargo fmt --all -- --check and cargo clippy -p datafusion-physical-plan --all-targets --all-features -- -D warnings all pass. The new topk.slt case panics with assertion failed: k > 0 without the fix.

Are there any user-facing changes?

No API changes. LIMIT 0 with a TopK sort now returns an empty result instead of panicking.


This PR was prepared with AI assistance. I reviewed the change and ran the tests listed above.

…he#26431)

A SortExec with fetch = Some(0) over unsorted input built a TopK with k = 0,
which panicked on assert!(k > 0) in TopKHeap::new. This is reachable when
eliminate_limit does not run, e.g. with datafusion.optimizer.max_passes = 0.

Treat a k = 0 TopK as finished from the start: it ignores input, emits no
rows and still marks its dynamic filter complete. SortExec now checks
finished before polling input, so it does not read any input in that case.
@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) physical-plan Changes to the physical-plan crate labels Oct 16, 2026

@rgbuilds rgbuilds left a comment

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.

Thanks for fixing this.

I traced the zero-k lifecycle through SortExec and TopK and verified the focused tests locally. The old implementation reproduces the panic, while the current implementation returns an empty result, completes the dynamic filter, and avoids polling the input.

The implementation looks sound to me. I left one non-blocking test suggestion.

// Check `finished` before polling so that a TopK that
// is already complete (e.g. `fetch = 0`) does not read
// any input.
while !topk.finished {

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.

Non-blocking test suggestion: could we add a focused SortExec::with_fetch(Some(0)) execution test using PanicExec or another first-poll-failing stream?

The unit test covers direct TopK construction, insertion, and emission, while the SLT covers the plan and empty result, but neither would detect a future change that polls the input once before checking finished. I verified locally that such a PanicExec test passes with this implementation.

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

Labels

physical-plan Changes to the physical-plan crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ORDER BY ... LIMIT 0 panics in TopK (assertion failed: k > 0) when eliminate_limit does not run

2 participants