[branch-55] fix wrong TopK results from re-reading already-delivered row groups (#24352) - #24368
Conversation
…ts from re-reading already-delivered row groups (apache#24352) (apache#24354) ## Which issue does this PR close? - Closes apache#24352. ## Rationale for this change With `datafusion.execution.parquet.pushdown_filters = true` and TopK dynamic filter pushdown (both on by default), a query of the shape `SELECT b FROM t WHERE <predicate on a> ORDER BY b LIMIT k` can silently return **wrong results** — one source row emitted several times and the true tail of the top-k missing — with no error or warning. Root cause (thanks to @hhhizzz's very detailed report + fixture in apache#24352): a row group whose post-predicate selection is empty is silently finished by arrow-rs **without handing back a reader**. `PushDecoderStreamState` pops its `rg_plan` **only** when a reader is returned, so after a silently-finished RG the plan trails the decoder by one. When the runtime row-group pruner then rebuilds the decoder (`into_builder().with_row_groups(...)`) from the stale `rg_plan`, it re-includes an already-delivered row group, whose rows are emitted a second time and displace the genuine top-k in the heap. ## What changes are included in this PR? - `push_decoder.rs`: before each boundary prune/rebuild, `rg_plan` is synced to the row group the decoder will actually emit next via `peek_next_row_group()` (`sync_rg_plan_to_decoder_frontier` / `advance_rg_plan_to`), dropping entries for silently-finished row groups so a rebuild can never re-include a delivered group. A rebuild frontier naming an RG not in the plan is now an internal error instead of a silent plan drain. ## Are these changes tested? - Adds @hhhizzz's fixture as an slt regression test in `dynamic_row_group_pruning.slt` (filter column `search_phrase` differs from the sort column `event_time`, one row group has an empty post-predicate selection invisible to statistics). It now returns the correct `p0 p4096 p4097 … p4104` (was the buggy `p0 p4096 p4096 …`). - clippy clean; `datasource-parquet` unit tests and the sqllogictest suite pass locally. ## Are there any user-facing changes? Fixes silently-wrong query results; no API change. ## Note This is the standalone bug fix extracted from apache#23696 (per review discussion in apache#24352): the same `rg_plan` ↔ decoder-frontier sync, on its own so it merges fast and is easy to backport. apache#23696 will rebase on top so it carries only the fully-matched `RowFilter` skip performance optimization. cc @alamb @adriangb @hhhizzz (cherry picked from commit 574fe67)
There was a problem hiding this comment.
Pull request overview
Backports the fix for a silent wrong-results bug in the Parquet push-decoder path on branch-55, ensuring runtime row-group prune/rebuild cannot re-read already-delivered row groups when pushdown_filters and TopK dynamic filter pushdown are enabled.
Changes:
- Synchronize
rg_planwith the decoder’s actual frontier viapeek_next_row_group()before any runtime prune/rebuild, and surface divergence as an internal error. - Add an end-to-end sqllogictest regression reproducer for #24352.
- Add a Rust end-to-end regression test that asserts both correct TopK output and that dynamic RG pruning actually occurred.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| datafusion/datasource-parquet/src/push_decoder.rs | Sync rg_plan to decoder frontier prior to runtime prune/rebuild to prevent re-reading previously delivered row groups; add unit tests for plan advancement behavior. |
| datafusion/sqllogictest/test_files/dynamic_row_group_pruning.slt | Adds SLT regression that writes a crafted Parquet fixture and verifies correct TopK output under pushdown_filters + dynamic filter pushdown. |
| datafusion/core/tests/parquet/dynamic_row_group_pruning.rs | Adds Rust regression test with the same fixture characteristics, asserting correct output and that runtime dynamic pruning was exercised. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Let's wait for CI to finish before merging |
|
Should we also backport the fix for this one: #24359 ? |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## branch-55 #24368 +/- ##
=============================================
- Coverage 81.14% 81.14% -0.01%
=============================================
Files 1110 1110
Lines 386132 386179 +47
Branches 386132 386179 +47
=============================================
+ Hits 313325 313362 +37
- Misses 54340 54344 +4
- Partials 18467 18473 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Merging this PR now @timsaucer @alamb , CI has passed. |
…ection is live (#24355) (#24374) Backport of #24359 to `branch-55` for the 55.0.0 release, per @timsaucer's request in #22393. Stacks cleanly on the already-merged #24368 (#24354 backport). ## Which issue does this PR close? - Backports the fix for #24355 — a second, independent silent wrong-results bug in the same parquet dynamic row-group pruning path as #24352. ## Rationale With `pushdown_filters=true` + a TopK dynamic filter, the runtime row-group pruner rebuilds the push decoder via `into_builder().with_row_groups(...)`, which drops row groups **without slicing** the carried flat page-index `RowSelection` to match — a dropped RG's selectors are then applied to the next surviving RG, silently returning wrong rows (no error). The fix declines to build the runtime `RowGroupPruner` when a row selection is present (correctness over the pruning optimization); the proper fix that keeps both is tracked upstream in apache/arrow-rs#10624 / #24358. ## Notes - Clean cherry-pick of #24359 onto `branch-55` (which now has #24354 via #24368). No conflicts. - #24359 is **approved** on `main` and pending merge; opening this now so it can ride RC3. - Verified locally on this branch: the full `dynamic_row_group_pruning` rust module (9/9) and `dynamic_row_group_pruning.slt` pass; clippy clean. cc @timsaucer @alamb @adriangb
Backport of #24354 to
branch-55for the 55.0.0 release, per @timsaucer's request in #22393.Which issue does this PR close?
Rationale
#24352 is a silent wrong-results bug: with
pushdown_filters=true+ TopK dynamic filter pushdown (both on by default), a row group whose post-predicate selection is empty is finished by arrow-rs without handing back a reader, so DataFusion'srg_plantrails the decoder frontier by one and a later runtime prune rebuilds the decoder from a stale plan — re-reading an already-delivered row group, duplicating rows and dropping the true top-k tail. No error is raised.This is a clean cherry-pick of the squashed #24354 commit (
574fe67); it applies tobranch-55without conflicts.What changes are included?
push_decoder.rs: syncrg_planto the decoder frontier viapeek_next_row_group()before each runtime prune/rebuild (gated onrow_group_pruner.is_some()so ordinary scans pay nothing), with a defensiveinternal_err!if the frontier diverges from the plan. Plus the slt + rust regression tests from #24354.cc @timsaucer @alamb @adriangb