2010YOUY01 opened a new pull request, #26360:
URL: https://github.com/apache/datafusion/pull/26360

   ## Which issue does this PR close?
   
   <!--
   We generally require a GitHub issue to be filed for all bug fixes and 
enhancements and this helps us generate change logs for our releases. You can 
link an issue to this PR using the GitHub syntax. For example `Closes #123` 
indicates that this PR will close issue #123.
   -->
   
   - Closes #.
   
   ## Rationale for this change
   
   <!--
    Why are you proposing this change? If this is already explained clearly in 
the issue then this section is not needed.
    Explaining clearly why changes are proposed helps reviewers understand your 
changes and offer better suggestions for fixes.  
   
   Please explain the problem you are trying to solve in terms of the 
user-visible
   behavior, rather than the implementation.
   
   For example, "The code in `foo.rs` doesn't handle nulls" is a symptom of the
   implementation. "COUNT(DISTINCT) returns wrong results when the column 
contains
   nulls" is the user-visible problem.
   -->
   
   The default physical optimizer pipeline works for the optimizations 
implemented in DataFusion core. However, there are many hidden assumptions 
between optimizer rules that make the pipeline work as a whole (e.g., rule A 
must run before rule B to produce a valid plan).
   
   These implicit constraints make extensions harder, whether adding new rules 
within DataFusion core or implementing custom pipelines downstream.
   
   This PR introduces **explicit invariant regions** to make these hidden 
constraints visible and verifiable, making the optimizer easier to understand, 
extend, and maintain.
   
   Note that this PR does not introduce new constraints. I think of this 
primarily as a **"removing uncertainty"** step. The assumptions already exist; 
this PR provides a mechanism to surface them so that:
   
   1. Developers can understand the expectations between optimizer rules.
   2. We can automatically test whether rules preserve those expectations.
   3. Extensions can make more informed decisions about where to insert or 
rearrange rules.
   
   The tradeoff is a certain learning curve for this mechanism. After 
prototyping the idea, I feel that once we become familiar with the concept, it 
can make long-term maintenance easier. However, it definitely looks confusing 
at first glance, and I'm still thinking about how to improve this part.
   
   (Note: The implementation is currently underdocumented; most of the ideas 
are explained in this writeup.)
   
   ### Two Types of Constraints
   
   This section explains two types of constraints, with one example each. The 
following figure illustrates two invariant regions in an example optimizer 
pipeline, which will be explained further below.
   
   ```text
                              Invariant Regions
   Rule List                   Aggr              Req
   --------------------------------------------------------
   InitialPhysicalPlanner
                               +-- BEGIN
   ...                         |
   LimitedDistinctAggregation  |
   ...                         |
                               +-- END
   EnsureRequirements
                                                 +-- BEGIN
   ...                                           |
                                                 +-- END
   ExtensionRule1                                (inactive)
   ExtensionRule2                                (inactive)
   EnsureRequirements                            (restore)
                                                 +-- BEGIN
   ...                                           |
   SanityCheckPlan                               |
                                                 +-- END
   ```
   
   - **Region 1, CanonicalAggregatePlan (Aggr):** Maintains canonical 
aggregation shapes in this region, the rules inside this region can kept 
simpler by only handling the canonical aggregate shape
   - **Region 2, InputRequirementsSatisfied (Req):** Ensures execution 
requirements remain satisfied. The example demonstrated temporary turning 
off/on in the middle to make extension easier.
   
   ### Type I: Optimization-Coverage Constraints (Soft)
   
   #### Progressive refinement in the optimizer pipeline
   
   The default optimizer pipeline is a progressive refinement process, where 
the possible plan shapes vary across stages.
   
   For example:
   
   - At rule 1, a hash join might have only one canonical representation.
   - At rule 20, the same logical join might have five possible physical 
representations.
   
   A rule that rewrites joins could be much simpler when placed at an earlier 
stage.
   
   Requiring every rule to handle every possible representation, regardless of 
its position, can unnecessarily bloat the implementation of earlier rules.
   
   #### Making soft constraints explicit
   
   Built-in rules already rely on such canonicalization assumptions, allowing 
them to match only canonical plan shapes and keep their implementations simple.
   
   Soft constraints provide a mechanism to make these assumptions explicit and 
verifiable, so custom optimizer pipelines can either:
   
   - Follow the constraints to preserve the same optimization coverage. 
(Another interpretation is that these constraints help extension rules find 
suitable positions in the pipeline.)
   - Opt out of the constraints, accepting the possibility of losing 
optimization coverage from built-in rules.
   
   #### Example: LimitedDistinctAggregation
   
   See https://github.com/apache/datafusion/pull/26069 for a concrete example 
of how relying on a canonical aggregation shape can simplify an optimizer rule.
   
   The corresponding invariant region would be:
   
   ```text
   CanonicalAggregatePlan
   - Established: After InitialPhysicalPlanner
   - Ends: After LimitedDistinctAggregation
   ```
   
   This is illustrated as Region 1 (Aggr) in the figure above.
   
   ### Type II: Execution-Validity Constraints (Hard)
   
   These constraints are required for correct execution. Extensions must 
preserve them to ensure that plans remain runnable.
   
   #### Example: EnsureRequirements
   
   Consider a partitioned hash join.
   
   Before enforcing requirements:
   
   ```text
   HashJoinExec(mode=Partitioned, on=[t1.k = t2.k])
     Scan(t1)  <-- Not hash-partitioned on t1.k
     Scan(t2)  <-- Not hash-partitioned on t2.k
   ```
   
   The plan represents the intended computation, but its children do not 
satisfy the hash join's distribution requirements.
   
   After `EnsureRequirements`:
   
   ```text
   HashJoinExec(mode=Partitioned, on=[t1.k = t2.k])
     RepartitionExec(hash(t1.k), N)
       Scan(t1)
     RepartitionExec(hash(t2.k), N)
       Scan(t2)
   ```
   
   Now the join's distribution requirements are satisfied.
   
   We can express this as an invariant region, as shown in the figure above.
   
   Within this region, every optimizer rule must preserve the invariant. If a 
rule violates it, the checker reports the failure immediately after that rule 
(when validation is enabled).
   
   For extensions, the responsibility is explicitly one of the following:
   
   1. Preserve the existing ordering/distribution guarantees.
   2. If a transformation breaks those guarantees, repair the plan internally.
   3. Alternatively, explicitly leave the region, perform transformations 
without those guarantees, and restore them before reentering. (demonstrated in 
the figure above)
   
   
   
   
   ## What changes are included in this PR?
   
   <!--
   There is no need to duplicate the description in the issue here, but it is 
sometimes worth providing a summary of the individual changes in this PR.
   -->
   1. Add a configuration option, `validate_optimizer_invariants`, to control 
invariant checking. Use a feature flag to enable validation in tests/CI while 
skipping it in release builds.
   2. The main change is in `physical-optimizer/src/optimizer.rs`, introducing 
invariant regions into the existing optimizer rule list.
   
   The PoC implements the following regions, which are not exactly the same as 
the examples above:
   
   - **NoEmbeddedProjection:** Soft constraint for projection shape 
canonicalization.
   - **HashJoinModeResolved:** Soft constraint for hash join mode resolution.
   - **InputRequirementsSatisfied:** Hard constraint; see the example above.
   
   One thing that remains implicit is **which optimizer rules establish the 
invariants**.
   
   For example, in the figure above, the canonical aggregation region is 
established by initial physical planning, while the 
`InputRequirementsSatisfied` region is established by `EnsureRequirements`.
   
   Therefore, downstream custom optimizer pipelines must follow the same 
assumptions and manually declare these regions after the corresponding rules 
have established their invariants.
   
   ## What is the testing strategy for this PR?
   
   <!--
   We typically require tests for all PRs in order to:
   1. Prevent the code from being accidentally broken by subsequent changes
   2. Serve as another way to document the expected behavior of the code
   
   Briefly describe how this PR is tested, and point to the specific tests you 
added. For example: 'This new feature is covered by the `sqllogictest` cases 
added in `foo.slt`'.
   
   If this PR does not add tests, explain why. For example, if the change is 
already covered by existing tests, please mention it.
   
   You should also check the `codecov` bot reply on this PR to confirm the 
changed code is exercised.
   -->
   
   ## Are there any user-facing changes?
   
   <!--
   If there are user-facing changes then we may require documentation to be 
updated before approving the PR.
   
   If there are any breaking changes to public APIs, please add the `api 
change` label.
   -->
   


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