linliu-code opened a new pull request, #748:
URL: https://github.com/apache/hudi-rs/pull/748

   ## Description
   
   A pushed predicate is evaluated by parquet against the base file's physical 
values, before
   `project_batch_to_schema` runs. That is sound only while a physical value 
means what its physical
   type says, which the apache/hudi#18132 logical-type repair breaks. A legacy 
base file labels a
   tz-aware column `timestamp-micros` while the stored i64 is milliseconds, and 
the repair
   reinterprets the buffer (relabels the unit, keeps the value) rather than 
rescaling it. A literal
   in table (millis) semantics, normalised against that label, therefore reads 
every value as a 1970
   instant: `ts > 2020-01-01` matches nothing and the scan drops rows that do 
match. A post-scan
   re-filter only subtracts rows, so nothing downstream recovers them.
   
   Both halves of the hazard are already here: the repair arm in 
`schema/batch_evolution.rs`, and the
   base-read pushdown surface (`ReaderContext::row_filter_builder` / 
`row_group_selector`, gated by
   `base_read_pushdown_is_safe`). That gate asks whether the read merges; it 
has never asked what a
   column's type is.
   
   This adds two gates, cheapest first. `batch_evolution::repair_risk_columns` 
answers, from the table
   schema alone, which of a predicate's columns could ever be misread. It is 
computed once per scan by
   whoever supplies the filter and routed in through the new
   `HoodieFileGroupReaderBuilder::with_repair_risk_columns`. It is empty for 
any table whose predicate
   touches no tz-aware millis column, which is every table Spark wrote with 
micros, so the common scan
   does no per-file work and loses no pushdown. Only when it is non-empty does 
`base_file_source`
   compare footer types, and then only for those named columns 
(`batch_evolution::reinterpreted_columns`,
   no extra IO since the footer schema is already fetched just above). On a 
conflict it withdraws the
   parquet `RowFilter` and the row-group selector together, and counts it on 
the new
   `ReadVolume::pushdown_suppressed_by_repair` so the refusal is visible rather 
than reading as "never
   installed".
   
   Declining rather than reconciling the literal is deliberate: rewriting a 
literal into a file's
   physical unit puts unit arithmetic exactly where a mistake means missing 
rows, which is the failure
   mode being fixed. It is also not available here, since hudi-core sees the 
predicate only as an
   opaque `RowFilterBuilder` closure.
   
   Two things reviewers should know. The pushdown surface has no in-crate 
production caller
   (`resolve_reader_context` leaves `row_filter_builder` as `None`, and none of 
`crates/jvm-ffi`,
   `crates/datafusion`, `python/` or `cpp/` sets it), as that field's own doc 
already states, so this
   guard is embedder-facing groundwork and nothing is wired live. And it is 
fail-open: an embedder that
   installs a row filter without calling `with_repair_risk_columns` gets no 
protection, because
   hudi-core cannot derive a predicate's columns from a closure. The new 
builder method is therefore
   documented as required alongside `with_row_filter_builder`, not as a tuning 
knob.
   
   Not closed here: the parquet log-block path still gates only on 
`mor_pk_safe`, so a record key that
   is itself a mislabelled timestamp column stays exposed. That needs a 
timestamp record key to matter.
   The unprojected fallback path (no `required_schema`) returns before the 
footer fetch and is left
   ungated, since with no required schema there is no evolution and so no 
reinterpretation.
   
   The diff is 1148 added lines, over the 1000 line guideline: 282 of those are 
behaviour (83 in
   `engine.rs`, 132 in `batch_evolution.rs`, 30 in `reader_context.rs`, 34 in 
`storage/mod.rs`, 3 in
   `resolver.rs`) and the other 866 are tests. Splitting the classifiers from 
their use site would land
   a gate nothing consults, so the halves are not independently meaningful.
   
   ## How are the changes test-covered
   
   - [ ] N/A
   - [x] Automated tests (unit and/or integration tests)
   - [ ] Manual tests
     - [ ] Details are described below
   
   `base_read_declines_pushdown_when_the_file_needs_a_reinterpreting_repair` 
was observed failing
   before the guard existed: the row filter was built once instead of never, 
and in the
   selector-installed twin the scan returned 0 of 2 rows, which is the row loss 
itself. Exactly four
   tests failed in that state and no others.
   `base_read_keeps_pushdown_when_the_file_is_honestly_labelled` fails if the 
guard over-declines, so
   the pair pins both directions. 
`base_read_keeps_pushdown_for_a_predicate_on_an_unaffected_column`
   pins the narrowing, 
`base_read_keeps_pushdown_when_no_predicate_column_is_at_risk` the unarmed gate,
   `base_read_declines_pushdown_for_an_unprojected_predicate_column` gate 2's 
table side, and
   `repair_suppression_is_counted_without_a_row_group_selector` the 
row-filter-side counter. Eleven
   unit tests pin the two classifiers.
   
   Each of the eight mechanisms was reverted in turn and every one fails at 
least one test: the pair
   classifier (10 tests), the table-side classifier (3), container recursion 
(1), the engine gate (4,
   exactly the four seen failing before the fix), gate 2's table side (1), the 
counter (2), the builder
   routing (1), and gate 2's candidate scoping (3, including the narrowing 
test). The sweep requires
   the unmutated tree to report zero failures before any mutant result is 
trusted.
   
   `make check` and `make test` are green (1708 tests), as is
   `cargo clippy -p hudi-core --lib --no-default-features -- -D warnings`.
   


-- 
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]

Reply via email to