adriangb commented on PR #23696:
URL: https://github.com/apache/datafusion/pull/23696#issuecomment-5346992228
@zhuqi-lucas I wanted to make sure this doesn't get lost. I think the best
way to de-risk it is to split up the PR some more to get a smaller review
surface. I worked with Claude to come up with a proposal, let me know what you
think:
### PR 1 — Strip empty row groups in `PreparedAccessPlan::prepare` (closes
#24287)
**Files:** `datafusion/datasource-parquet/src/access_plan.rs`, `src/sort.rs`
**Est.** ~110 lines changed
Land `strip_empty_row_groups` with the *generic* signature only:
```rust
fn strip_empty_row_groups(
row_group_indexes: Vec<usize>,
row_selection: Option<RowSelection>,
row_group_meta_data: &[RowGroupMetaData],
) -> (Vec<usize>, Option<RowSelection>)
```
No `fully_matched` parameter. That is exactly the decoupling #24287 asks
for, and
doing it first means it never has to be untangled later.
`sort.rs::test_prepared_access_plan_reverse_empty_selection` needs updating:
once
empty segments are stripped, an all-skipped plan strips to an *empty* plan,
so
the assertion becomes `row_group_indexes.is_empty()` and
`row_selection.is_none()`
rather than "the selection selects 0 rows".
**Why it stands alone:** it restores the 1:1 correspondence between the
prepared
plan and the readers `try_next_reader` hands back. The runtime dynamic pruner
already relies on that invariant today — this is the same family as #24352
and
#24355, both of which shipped as standalone fixes. It is a correctness fix
that
the feature happens to need, not a part of the feature.
No public API change, no new metric, no slt churn.
### PR 2 — Prebuild row-filter candidates once per file (needs a new issue)
**Files:** `src/row_filter.rs`, `src/push_decoder.rs`, `src/opener/mod.rs`
**Est.** ~230 lines changed
Add `PrebuiltRowFilterCandidate`, `prebuild_row_filter_candidates`,
`row_filter_from_prebuilt`, `RowFilterContext`,
`PrebuiltRowFilterCandidateList`;
delete `RowFilterGenerator`; have the opener prebuild once per file and
instantiate once.
**Also reimplement the existing `pub fn build_row_filter` on top of the two
new
functions.** On the current branch both paths survive — `row_filter.rs:397`
and
`row_filter.rs:502` — with duplicated conjunct splitting, ordering, and
metric
assignment. Two paths that must agree on predicate order *and* on "only the
last
predicate counts `pushdown_rows_matched`" will drift. `build_row_filter` is
public and used by the `parquet_nested_filter_pushdown` and
`parquet_struct_filter_pushdown` benches, so its signature has to stay.
Parity worth stating explicitly in the PR body: same conjunct order
(`sort_unstable_by_key(required_bytes)` when `reorder_predicates`), same
metric
wiring (every predicate shares `pushdown_rows_pruned`, only the last gets
`pushdown_rows_matched`). One genuine behavioural delta:
`reassign_expr_columns`
errors now surface once at open time instead of being swallowed per-build by
`log::debug!`.
**Why it stands alone:** today `RowFilterGenerator::build()` redoes
`split_conjunction` + `FilterCandidateBuilder::build` +
`reassign_expr_columns`
for **every row group**. Moving that to once per file is a win on any
multi-row-group scan with a pushdown predicate. It needs nothing from
`fully_matched`.
**Benchmarks:** #24328's `parquet_row_filter_skip`, plus a many-row-group
multi-conjunct case. This is the piece with a clean, attributable perf story.
### PR 3 — Extract `InitialDecoderState` (closes #24286)
**Files:** `src/opener/mod.rs`
**Est.** ~60 lines changed
Do this *before* the feature, not after. The block at `opener/mod.rs:1437`
currently returns a 3-tuple `(decoder, rg_plan, has_row_selection)`; the
feature
turns it into a 5-tuple. Landing the named struct first means the feature PR
adds
two fields to a struct instead of reshaping a tuple destructure.
Optional rider: the `prune_boundary_row_groups` extraction from
`push_decoder.rs`
is pure refactor of existing pruner code and fits the same "make the boundary
path legible" theme.
### PR 4 — The feature: skip RowFilter on fully-matched row groups (#23696)
**Est.** ~380 lines changed
What remains after the three above:
- `RgPlanEntry.fully_matched`
- `PreparedAccessPlan.fully_matched`, plus its permutation in
`reorder_by_statistics` and `reverse`
- `strip_empty_row_groups` gaining the parallel `fully_matched` vector
- `rebuild_decoder_at_boundary`, `filter_installed`, and the open-time
first-RG skip
- the `row_filter_skipped_fully_matched` metric
- `dynamic_row_group_pruning.rs` (+88) and `dynamic_row_group_pruning.slt`
(+42)
One idea, one benchmark question.
## Dependency order
```
PR1 (#24287) ───────────────────────┐
├──> PR4 (#23696, feature)
PR2 (new issue) ──> PR3 (#24286) ───┘
```
PR 1 and PR 2 are independent and can both go up immediately. PR 3 touches
the
same opener block as PR 2, so it follows PR 2. PR 4 rebases onto all three.
## Three fixes worth making while splitting
### 1. Register the metric lazily — it kills the semver flag *and* the slt
churn
`row_filter_skipped_fully_matched` is currently a `pub` field on
`ParquetFileMetrics`, created eagerly in `new()`. That is what trips
`Check semver`, and it is also why 34 unrelated EXPLAIN ANALYZE baselines
across
`push_down_filter_parquet.slt`, `explain_analyze.slt`, and
`dynamic_filter_pushdown_config.slt` had to be regenerated to carry
`row_filter_skipped_fully_matched=0`.
The sibling metric from #21637 already solves this:
`add_page_index_pages_skipped_by_fully_matched` (`metrics.rs:243`) registers
the
counter only when the value is non-zero — which is why
`page_index_pages_skipped_by_fully_matched=1` shows up in
`explain_analyze.slt`
on the one plan that actually uses it, and nowhere else.
Same pattern here: no public field, register on first suppression. The stream
needs a live handle rather than a fire-once call, so hold an `Option<Count>`
in
`PushDecoderStreamState` and build it via `MetricBuilder` the first time a
toggle
fires. Result: no public API change, no semver flag, and the slt diff
shrinks to
just the new test file.
### 2. Revert the `test_input_file_name_projection` move
In `opener/mod.rs` this test is deleted at one location and re-added ~150
lines
earlier, byte-identical. It is ~80 lines of pure diff noise that makes the
file
look far more touched than it is.
### 3. Consider one vector instead of two parallel ones
`PreparedAccessPlan::new` gets a `debug_assert_eq!(row_group_indexes.len(),
fully_matched.len())`, and `reorder_by_statistics`, `reverse`, and
`strip_empty_row_groups` each have to maintain that alignment by hand. Since
`RgPlanEntry` downstream already has exactly the `{ index, fully_matched }`
shape, it may be worth carrying one `Vec` of that struct instead — which
makes
the permutation unmisusable rather than merely asserted.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]