adriangb commented on issue #23494:
URL: https://github.com/apache/datafusion/issues/23494#issuecomment-5219122892

   ## Post-migration cleanup
   
   With the plan-side checklist essentially complete (only the scan sources — 
#23516, #23517, #23518 — remain, and both foundations they were blocked on have 
landed), here is a round of follow-up cleanup, plus one real bug the audit 
turned up.
   
   ### The bug this found
   
   **#24165 — `fix(proto): preserve HashJoinExec fetch across serialization`**
   
   `protobuf::HashJoinExecNode` has no `fetch` field, so `HashJoinExec.fetch` 
round-trips `Some(n) -> None`. It matters because `limit_pushdown` pushes the 
limit *into* the join via `with_fetch` and then drops the enclosing 
`GlobalLimitExec` — so after a round-trip the plan has no limit at all and a 
distributed executor returns more rows than the query asked for.
   
   The new field is presence-tracked (`optional uint64`) on purpose: a plain 
proto3 scalar decodes an absent field as `0`, which under `SortExecNode`'s 
negative-sentinel convention would read as "fetch 0 rows" and silently empty 
out plans serialized by older versions.
   
   Worth noting *why* no existing test caught this: `roundtrip_test` compares 
`format!("{plan:?}")`, and `HashJoinExec`'s `Debug` output does not include 
`fetch`. This was verified rather than assumed — with the encode side reverted, 
the Debug comparison still passes and only a direct `fetch()` assertion fails. 
Display-string round-trip assertions cannot catch a dropped field that `Debug` 
does not print.
   
   ### Cleanup 1 — exhaustive destructuring in the serde hooks
   
   Today the encoders read state through getters, so adding a field to a plan 
struct is invisible to serialization: nothing breaks, the field is just 
silently not on the wire. That is exactly how the `fetch` bug happened.
   
   These PRs make both directions exhaustive — `try_to_proto` opens with a full 
`let Self { .. } = self` destructure (no `..`, every non-serialized field bound 
to `_` with a comment saying why), and `try_from_proto` destructures the prost 
node struct. Adding a field to a plan struct or to a proto message then becomes 
a compile error in every affected encoder/decoder. The prost-generated structs 
are plain, all-`pub` and not `#[non_exhaustive]`, so this works on the decode 
side too.
   
   Split by family so each stays reviewable; all three are pure refactors with 
the wire format byte-for-byte unchanged:
   
   - **#24164** — joins: `SymmetricHashJoinExec`, `NestedLoopJoinExec`, 
`CrossJoinExec`, `SortMergeJoinExec`, and the hash-join expressions
   - **#24167** — core plans: sorts, limits, filter, projection, repartition, 
union/interleave, coalesce/cooperative/buffer, leaves, explain, scalar subquery 
(17 plans)
   - **#24166** — aggregate and window: `AggregateExec`, both window execs, 
unnest, async func, analyze
   
   `HashJoinExec` itself is deliberately absent — it belongs to the bug fix 
above; its destructure is a small follow-up once that merges.
   
   ### Cleanup 2 — deprecate accessors that only existed for proto
   
   ****#24168** (stacked on #24166 — its diff carries that PR's commits until 
it merges)**
   
   Several `pub fn` accessors were added purely so the central encoder could 
reach private fields. Now that each plan's `try_to_proto` lives in the plan's 
own module and (after cleanup 1) reads fields directly, they have no callers at 
all:
   
   | Accessor | Introduced by |
   |---|---|
   | `AnalyzeExec::verbose`, `::show_statistics` | #7574 "Implement protobuf 
serialization for AnalyzeExec" |
   | `UnnestExec::list_column_indices`, `::struct_column_indices` | #12344 
"Support encoding and decoding UnnestExec" |
   | `AsyncFuncExec::async_exprs` | #19118 "[Proto]: Serialization support for 
`AsyncFuncExec`" |
   
   Deprecated rather than removed, per the API health policy, on the same 
`since = "55.0.0"` schedule as the `PhysicalPlanNodeExt` scaffolding.
   
   A second tier of accessors also turns out to have no callers 
(`SymmetricHashJoinExec::{left,right}_sort_exprs`, 
`WindowAggExec::partition_by_sort_keys`, `ScalarSubqueryExec::subqueries`, 
`CoalesceBatchesExec::target_batch_size`, `HashJoinExec::join_schema`, 
`CastExpr::cast_options`), but those predate the proto work and do not meet the 
"only existed for serialization" bar — happy to handle them separately if 
people think they should go.
   
   ### Gaps the refactor documented but did not fix
   
   Each of these is a field that does not survive a round-trip. They are left 
behaving exactly as today, with a comment recording the state, since changing 
the wire format inside a cleanup PR would be worse than the gap:
   
   - **`AnalyzeExec::metric_types`** — no field on `AnalyzeExecNode`, and 
`AnalyzeExecBuilder` unconditionally resets it to `[Summary, Dev]`, so a 
non-default metric type selection is lost. This is the same class of bug as the 
`fetch` one and probably wants its own fix.
   - `SortPreservingMergeExec::enable_round_robin_repartition` — not on the 
wire; decode restores the `true` default. Only set to `false` in tests/benches 
today, so no production path is affected.
   - `Global/LocalLimitExec::required_ordering` — not on the wire; set by 
`enforce_sorting`, so decode yields `None`.
   - `HashTableLookupExpr` — serializes none of its state by design (runtime 
map, replaced with `lit(true)`). Pre-existing and intentional; it now has an 
all-`_` destructure so a future field addition forces an explicit decision.
   
   ### Possible further follow-ups
   
   - The same destructuring treatment for the `PhysicalExpr` hooks from #22418.
   - There is no guard test asserting that every `PhysicalPlanType` variant 
round-trips — 157 hand-written tests, no exhaustive enumeration.
   


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

Reply via email to