kosiew opened a new issue, #23666:
URL: https://github.com/apache/datafusion/issues/23666

   ## Related PR
   #22903 
   
   ## Problem
   
   Some logical plan nodes cache schemas derived from their children. When an 
optimizer rule rewrites a child and the child's output schema changes, the 
parent must refresh its cached schema before later optimizer rules inspect it.
   
   The current in-place optimizer traversal handles this directly in 
`rewrite_plan_in_place` by comparing child schemas before/after recursion and 
calling `LogicalPlan::recompute_schema()` when needed. That fixes the known 
stale-schema path, but the contract is still local to one traversal 
implementation.
   
   Other optimizer code still uses `LogicalPlan::map_children(...)` and then 
manually decides whether to call `recompute_schema()`. This keeps the invariant 
easy to miss when adding or changing optimizer rules.
   
   Relevant current code:
   
   - `datafusion/optimizer/src/optimizer.rs`
     - private `map_children_mut(...)`
     - `rewrite_plan_in_place(...)` detects child schema changes and recomputes 
the parent schema
   - `datafusion/expr/src/logical_plan/tree_node.rs`
     - `LogicalPlan::map_children(...)` rebuilds nodes while preserving cached 
parent schemas
   - `datafusion/expr/src/logical_plan/plan.rs`
     - `LogicalPlan::recompute_schema()` contains node-specific schema refresh 
logic
   - optimizer rules with manual recompute-after-child-rewrite logic, for 
example:
     - `datafusion/optimizer/src/optimize_projections/mod.rs`
     - `datafusion/optimizer/src/eliminate_cross_join.rs`
   
   ## Why it matters
   
   The invariant is correctness-sensitive but currently implicit:
   
   > If an optimizer child rewrite changes a child schema, the parent plan 
observed by later optimizer rules must not expose a stale cached schema.
   
   A stale parent schema can lead to projection pruning, column resolution, or 
later rule logic using field positions/names that no longer match the rewritten 
children. Failures then surface far from the rewrite that introduced the 
inconsistency.
   
   This issue is a maintainability refactor, not a claim that the current 
in-place optimizer path is broken.
   
   ## Invariant / desired behavior
   
   After an optimizer traversal rewrites direct children of a logical plan node:
   
   - if no child schema changed, preserve the current behavior and avoid 
unnecessary schema recomputation;
   - if any child schema changed, recompute the parent schema before returning 
it to later optimizer code;
   - if a node intentionally has an explicit/fixed schema contract, that 
behavior remains encoded in `LogicalPlan::recompute_schema()` or documented at 
the call site;
   - traversal semantics, including transformed/no-op status and recursion 
control, are preserved.
   
   ## Proposed direction
   
   Add a small optimizer-local helper rather than changing 
`LogicalPlan::map_children` globally.
   
   Possible shape for owned-plan optimizer rewrites:
   
   ```rust
   fn map_children_recompute_schema_if_needed<F>(
       plan: LogicalPlan,
       f: F,
   ) -> Result<Transformed<LogicalPlan>>
   where
       F: FnMut(LogicalPlan) -> Result<Transformed<LogicalPlan>>;
   ```
   
   The helper should:
   
   1. record each direct child schema before rewrite;
   2. rewrite direct children using the existing `map_children` behavior;
   3. detect whether any changed child now has a different schema;
   4. call `LogicalPlan::recompute_schema()` only when needed;
   5. preserve `Transformed` state and `TreeNodeRecursion` behavior from the 
child rewrite.
   
   If useful, add a mutating variant for the in-place optimizer traversal 
later, but avoid broad API changes until the owned helper proves useful.
   
   ## Scope
   
   ### In
   
   - Add a private helper under `datafusion/optimizer/src/` for schema-aware 
direct-child rewrites.
   - Use the helper in one or two existing optimizer rules that currently do 
`map_children(...)` followed by manual `recompute_schema()`.
   - Keep `rewrite_plan_in_place` behavior unchanged or migrate it only if the 
helper clearly fits without making the traversal harder to read.
   - Add focused unit coverage for the helper through an optimizer rule or 
small test-only rule.
   
   ### Out
   
   - Do not change `LogicalPlan::map_children` semantics globally.
   - Do not redesign `TreeNode` traversal APIs.
   - Do not require all optimizer rules to migrate in one patch.
   - Do not change SQL-visible schemas except where existing stale-schema 
behavior would already be considered a bug.
   - Do not broaden schema equality rules beyond the current `DFSchema` 
equality checks used by optimizer code.
   - Do not redesign extension-node APIs as part of this issue.
   
   ## Acceptance criteria
   
   - [ ] There is one documented optimizer-local helper for direct-child 
rewrites that refreshes parent schema when a changed child schema differs.
   - [ ] At least one manual `map_children(...)` + `recompute_schema()` 
optimizer site is migrated to the helper.
   - [ ] No-op child rewrites do not force unnecessary parent schema 
recomputation.
   - [ ] Parent schemas are refreshed when changed children produce different 
output schemas.
   - [ ] Existing in-place optimizer stale-schema protection remains intact.
   - [ ] Existing optimizer tests pass.
   
   ## Tests / verification
   
   Add focused tests that cover:
   
   - a child rewrite that changes schema and causes the parent schema to 
refresh;
   - a child rewrite that changes the plan but not the child schema and does 
not require parent schema refresh;
   - an existing rule migrated to the helper still preserves its expected 
optimized plan.
   
   Run targeted tests first:
   
   ```bash
   cargo test -p datafusion-optimizer
   ```
   
   If the migrated rule has focused tests, run those specifically as well, for 
example:
   
   ```bash
   cargo test -p datafusion-optimizer optimize_projections
   cargo test -p datafusion-optimizer eliminate_cross_join
   ```
   
   


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