Skip to content

fix: preserve TopK dynamic filters across recursive CTE iterations - #26670

Merged
jayzhan211 merged 8 commits into
apache:mainfrom
Kristofferjoha:fix/topk-recursive-26642
Oct 25, 2026
Merged

jayzhan211 merged 8 commits into
apache:mainfrom
Kristofferjoha:fix/topk-recursive-26642

Conversation

@Kristofferjoha

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Closes #26642.

Rationale for this change

TopK dynamic filters inside recursive CTEs stop updating the scan because SortExec::reset_state() creates a new filter on each iteration, while the scan still references the original one. This prevents pruning during recursive queries.

What changes are included in this PR?

  • Reset the existing shared dynamic filter instead of replacing it.
  • Rebuild the TopK filter state using the existing DynamicFilterPhysicalExpr.
  • Add a recursive TopK regression test in cte.slt.

Depends on #26056 for DynamicFilterPhysicalExpr::reset(). It hasn't been merged yet, but it has been approved, so I based this change on its implementation.

What is the testing strategy for this PR?

The new cte.slt case checks that the recursive query returns 1–4 and that the Parquet scan outputs 30 rows instead of 300, pruning 27 row groups across three iterations.

Are there any user-facing changes?

No.

@Kristofferjoha Kristofferjoha changed the title Fix/topk recursive 26642 fix: preserve TopK dynamic filters across recursive CTE iterations Oct 23, 2026
.clone();
// Our dynamic filter and execution metrics are the state we need to reset.
new_sort.filter = Some(new_sort.create_filter());
let expr = new_sort.filter.as_ref().map(|filter| filter.read().expr());

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: after the reset this is the same as the existing rebuild_filter_for_current_partitioning() helper, so it could reuse it:

-        let expr = new_sort.filter.as_ref().map(|filter| filter.read().expr());
-        if let Some(expr) = expr {
-            expr.reset()?;
-            new_sort.filter = Some(new_sort.create_filter_with_expr(expr));
+        if let Some(filter) = &new_sort.filter {
+            filter.read().expr().reset()?;
         }
-
+        new_sort.rebuild_filter_for_current_partitioning();
         new_sort.metrics_set = ExecutionPlanMetricsSet::new();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My bad, missed the existing helper. reset_state() uses it now.

@github-actions github-actions Bot added physical-expr Changes to the physical-expr crates sqllogictest SQL Logic Tests (.slt) physical-plan Changes to the physical-plan crate labels Oct 24, 2026
// Our dynamic filter and execution metrics are the state we need to reset.
new_sort.filter = Some(new_sort.create_filter());
if let Some(filter) = &new_sort.filter {
filter.read().expr().reset()?;

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: #26669 makes DynamicFilterPhysicalExpr::reset() infallible, so this ? won't compile once both PRs are in. The two PRs touch different files, so git won't flag a conflict. Could you drop the ? in whichever of the two merges second (or rebase this one on #26669)?

-            filter.read().expr().reset()?;
+            filter.read().expr().reset();

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.

let me address this as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sorry didnt see it until now, Thanks

@github-actions github-actions Bot removed the physical-expr Changes to the physical-expr crates label Oct 25, 2026
@jayzhan211
jayzhan211 added this pull request to the merge queue Oct 25, 2026
@jayzhan211

Copy link
Copy Markdown
Contributor

Thanks @Kristofferjoha

Merged via the queue into apache:main with commit 6aef019 Oct 25, 2026
45 checks passed
@Xuanwo Xuanwo mentioned this pull request Nov 5, 2026
35 of 57 tasks
@alamb

alamb commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

This PR appears to have introduced a 56 regression:

@alamb

alamb commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

We are going to revert this change for 56, and then we can work on the longer term fix for 57.

bvolpato pushed a commit to bvolpato/datafusion that referenced this pull request Nov 6, 2026
…(revert apache#26670) (apache#27142)

## Which issue does this PR close?

- Closes apache#27130.

## Rationale for this change

As discussed in apache#27130, this reverts apache#26670 so copies made by
`SortExec::reset_state()` no longer share one TopK dynamic filter. When
a cached `LIMIT 1` plan was reset twice and both copies ran, the first
one to finish tightened the shared filter and the second returned no
rows.

A plain revert brings back a different wrong result, though: a plan that
has run once and is then reset still has its old bound in the scan's
predicate (the scan keeps the filter it got at planning time). For
`SELECT a FROM t ORDER BY a LIMIT 1` over a parquet file with pushdown
enabled, the rerun returned no rows with the plain revert. So this keeps
the in-place `reset()` from apache#26056 on the old filter and then gives the
new plan its own filter, as @Kristofferjoha suggested keeping.

| | main | plain revert | this PR |
|---|---|---|---|
| reset twice, run both copies (apache#27130) | 2nd: no rows | ok | ok |
| run, reset, run again (parquet, pushdown) | ok | 2nd: no rows | ok |

## What changes are included in this PR?

- `SortExec::reset_state` resets the old dynamic filter in place, then
creates a new one for the reset plan. The `sort_concurrency` reset from
apache#26242 is kept. (The plain revert did not apply cleanly because apache#26242
changed the same lines.)
- Removes the recursive CTE pruning test apache#26670 added to `cte.slt`,
since the scan now keeps the old filter (reset to `true`) and the
pruning no longer happens, as in 55.
- Adds two unit tests in `sorts/sort.rs`: the reproducer from apache#27130,
and a test that the filter a scan would hold is back to `true` after
`reset_state`. The second one fails with the plain revert.

## Are these changes tested?

Yes. Both new tests fail on main or on the plain revert respectively,
and pass here. All `datafusion-physical-plan` unit tests and
sqllogictests pass.

## Are there any user-facing changes?

The TopK dynamic filter inside a recursive CTE no longer prunes after
the first iteration, as in 55 (the apache#26670 test's scan reads 300 rows
instead of 30). Query results are unchanged. The proper fix (a fresh
filter per copy that the scan also picks up, which would also cover
`AggregateExec`) can follow after 56.

This reopens apache#26642.
alamb pushed a commit that referenced this pull request Nov 6, 2026
…amic filter (backport #27142) (#27174)

## Which issue does this PR close?

- part of #24461
- Backport of #27142 to `branch-56`
- Fixes the 56 regression reported in #27130.

## What changes are included in this PR?

Clean cherry-pick of #27142: copies made by `SortExec::reset_state()`
each get their own TopK dynamic filter again (reverting #26670), and the
old filter is reset so a plan that is run, reset and run again doesn't
keep the previous bound in the scan.

## Are these changes tested?

Same tests as #27142. On `branch-56`: `datafusion-physical-plan` sort
tests 164/164, sqllogictest 544/544, clippy and fmt clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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) v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TopK dynamic filters don't prune inside recursive CTEs

4 participants