github-actions[bot] commented on code in PR #65682:
URL: https://github.com/apache/doris/pull/65682#discussion_r3594102575


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/joinorder/hypergraphv2/HyperGraph.java:
##########
@@ -385,17 +434,44 @@ public boolean addAlias(Alias alias, long subTreeNodes) {
             if (bitmap == 0) {
                 bitmap = subTreeNodes;
                 addToReplaceMap = false;
-                List<NamedExpression> aliasList = 
nodeToLiteralAlias.get(bitmap);
+                List<NamedExpression> aliasList = 
nodeToProjectedAliases.get(bitmap);
                 if (aliasList == null) {
                     aliasList = new ArrayList<>(1);
-                    nodeToLiteralAlias.put(bitmap, aliasList);
+                    nodeToProjectedAliases.put(bitmap, aliasList);
                 }
                 aliasList.add(alias);
             }
             Preconditions.checkArgument(bitmap > 0, "slot must belong to some 
table");
             slotToHyperNodeMap.put(aliasSlot, bitmap);
+            // Do not add aliases on the nullable side of outer joins to 
aliasReplaceMap.
+            // Aliases on the nullable side (e.g., COALESCE(v, 0) AS dv on the 
right side of
+            // a LEFT JOIN) must execute BEFORE the outer join's 
null-extension.
+            // If added to aliasReplaceMap, they would be unwrapped and 
reconstructed above
+            // the outer join by PlanReceiver.proposeProject(), changing 
execution order
+            // and producing wrong results.
+            // Instead, add them to nodeToProjectedAliases keyed by 
subTreeNodes (the entire
+            // subtree under the source Project), so the alias is only 
projected when the full
+            // original subtree is available, preserving the execution 
boundary.
+            // Using the minimal referenced bitmap (e.g., {B}) would wrongly 
emit the alias at
+            // a leaf level, where an inner nullable-side join could still 
null-extend it,
+            // e.g. X LEFT JOIN (Project over A LEFT JOIN B): emitting at {B} 
loses the inner
+            // join's null-extension context.
+            // Before storing, resolve chained nullable-side aliases (e.g. 
y=x+1 referencing
+            // x=coalesce(...)) via nullableAliasReplaceMap so that all stored 
expressions
+            // reference only base columns — avoiding sibling references in a 
flat LogicalProject.
+            if (addToReplaceMap && isNullableSide) {
+                alias = (Alias) ExpressionUtils.replaceNameExpression(alias, 
nullableAliasReplaceMap);
+                alias = (Alias) ExpressionUtils.replaceNameExpression(alias, 
aliasReplaceMap);
+                nullableAliasReplaceMap.put(aliasSlot, alias.child());

Review Comment:
   [P2] Preserve the expression-limit fallback
   
   Storing each fully expanded child in this map makes the next chained 
replacement grow without the expression-limit fallback used by normal Project 
merging. `PlanUtils.tryMergeProjections` keeps separate layers when expansion 
raises `EXPRESSION_EXCEEDS_LIMIT`; a valid nullable-side chain of small 
Projects such as `x1=B.v+B.v`, `x2=x1+x1`, ... is therefore intentionally left 
layered once the combined expression would exceed `expr_children_limit`. This 
builder expands the chain again until `Expression.checkLimit` throws, causing 
DPHyp graph construction to fail even though normal merging preserves the 
layered plan. Preserve those layers (or catch the limit and retain a 
materialization boundary), and add a limit-path regression.
   



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/joinorder/hypergraphv2/HyperGraph.java:
##########
@@ -385,17 +434,44 @@ public boolean addAlias(Alias alias, long subTreeNodes) {
             if (bitmap == 0) {
                 bitmap = subTreeNodes;
                 addToReplaceMap = false;
-                List<NamedExpression> aliasList = 
nodeToLiteralAlias.get(bitmap);
+                List<NamedExpression> aliasList = 
nodeToProjectedAliases.get(bitmap);
                 if (aliasList == null) {
                     aliasList = new ArrayList<>(1);
-                    nodeToLiteralAlias.put(bitmap, aliasList);
+                    nodeToProjectedAliases.put(bitmap, aliasList);
                 }
                 aliasList.add(alias);
             }
             Preconditions.checkArgument(bitmap > 0, "slot must belong to some 
table");
             slotToHyperNodeMap.put(aliasSlot, bitmap);
+            // Do not add aliases on the nullable side of outer joins to 
aliasReplaceMap.
+            // Aliases on the nullable side (e.g., COALESCE(v, 0) AS dv on the 
right side of
+            // a LEFT JOIN) must execute BEFORE the outer join's 
null-extension.
+            // If added to aliasReplaceMap, they would be unwrapped and 
reconstructed above
+            // the outer join by PlanReceiver.proposeProject(), changing 
execution order
+            // and producing wrong results.
+            // Instead, add them to nodeToProjectedAliases keyed by 
subTreeNodes (the entire
+            // subtree under the source Project), so the alias is only 
projected when the full
+            // original subtree is available, preserving the execution 
boundary.
+            // Using the minimal referenced bitmap (e.g., {B}) would wrongly 
emit the alias at
+            // a leaf level, where an inner nullable-side join could still 
null-extend it,
+            // e.g. X LEFT JOIN (Project over A LEFT JOIN B): emitting at {B} 
loses the inner
+            // join's null-extension context.
+            // Before storing, resolve chained nullable-side aliases (e.g. 
y=x+1 referencing
+            // x=coalesce(...)) via nullableAliasReplaceMap so that all stored 
expressions
+            // reference only base columns — avoiding sibling references in a 
flat LogicalProject.
+            if (addToReplaceMap && isNullableSide) {
+                alias = (Alias) ExpressionUtils.replaceNameExpression(alias, 
nullableAliasReplaceMap);

Review Comment:
   [P1] Preserve volatile alias materialization
   
   Unconditionally replacing a chained alias here bypasses the 
volatile-expression guard used by normal Project merging. For this nullable 
subtree:
   
   ```text
   Project(x, x AS y)
     Project(concat(B.v, uuid()) AS x)
       B
   ```
   
   the original layers evaluate the volatile producer once and guarantee `x = 
y`. `PlanUtils.canMergeWithProjections` deliberately rejects flattening this 
shape, but this map expands `y` to another `concat(B.v, uuid())` while the 
original `x` alias is also retained, so the rebuilt flat Project can return 
different UUIDs. This is distinct from the existing boundary and 
unresolved-sibling threads: the replacement succeeds but changes values. 
Preserve ordered Project layers; when the normal volatile-reference guard 
rejects expansion, retain the producer materialization boundary rather than 
flattening the aliases, and cover the `x = y` invariant in a nullable-side 
regression.
   



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/joinorder/hypergraphv2/HyperGraph.java:
##########
@@ -385,17 +434,44 @@ public boolean addAlias(Alias alias, long subTreeNodes) {
             if (bitmap == 0) {
                 bitmap = subTreeNodes;
                 addToReplaceMap = false;
-                List<NamedExpression> aliasList = 
nodeToLiteralAlias.get(bitmap);
+                List<NamedExpression> aliasList = 
nodeToProjectedAliases.get(bitmap);
                 if (aliasList == null) {
                     aliasList = new ArrayList<>(1);
-                    nodeToLiteralAlias.put(bitmap, aliasList);
+                    nodeToProjectedAliases.put(bitmap, aliasList);
                 }
                 aliasList.add(alias);
             }
             Preconditions.checkArgument(bitmap > 0, "slot must belong to some 
table");
             slotToHyperNodeMap.put(aliasSlot, bitmap);
+            // Do not add aliases on the nullable side of outer joins to 
aliasReplaceMap.
+            // Aliases on the nullable side (e.g., COALESCE(v, 0) AS dv on the 
right side of
+            // a LEFT JOIN) must execute BEFORE the outer join's 
null-extension.
+            // If added to aliasReplaceMap, they would be unwrapped and 
reconstructed above
+            // the outer join by PlanReceiver.proposeProject(), changing 
execution order
+            // and producing wrong results.
+            // Instead, add them to nodeToProjectedAliases keyed by 
subTreeNodes (the entire
+            // subtree under the source Project), so the alias is only 
projected when the full
+            // original subtree is available, preserving the execution 
boundary.
+            // Using the minimal referenced bitmap (e.g., {B}) would wrongly 
emit the alias at
+            // a leaf level, where an inner nullable-side join could still 
null-extend it,
+            // e.g. X LEFT JOIN (Project over A LEFT JOIN B): emitting at {B} 
loses the inner
+            // join's null-extension context.
+            // Before storing, resolve chained nullable-side aliases (e.g. 
y=x+1 referencing
+            // x=coalesce(...)) via nullableAliasReplaceMap so that all stored 
expressions
+            // reference only base columns — avoiding sibling references in a 
flat LogicalProject.
+            if (addToReplaceMap && isNullableSide) {
+                alias = (Alias) ExpressionUtils.replaceNameExpression(alias, 
nullableAliasReplaceMap);
+                alias = (Alias) ExpressionUtils.replaceNameExpression(alias, 
aliasReplaceMap);
+                nullableAliasReplaceMap.put(aliasSlot, alias.child());
+                List<NamedExpression> aliasList = 
nodeToProjectedAliases.get(subTreeNodes);
+                if (aliasList == null) {
+                    aliasList = new ArrayList<>(1);
+                    nodeToProjectedAliases.put(subTreeNodes, aliasList);

Review Comment:
   [P1] Bind consumers to the alias emission bitmap
   
   This stores a nullable alias at its full source subtree, but 
`slotToHyperNodeMap` still maps the alias slot only to its referenced inputs. A 
reduced failing tree is:
   
   ```text
   X LEFT JOIN
     X
     InnerJoin(s = C.k)
       Project(s = coalesce(B.v, 0)) over InnerJoin(A, B)  // emits at {A,B}
       C
   ```
   
   Here `calculateEndsBySlots` makes the upper predicate a `{B}--{C}` edge. 
Both child edges are inner, so conflict rules do not add A and DPHyp can build 
`B JOIN C ON s=C.k`; neither child emits `s`, because 
`getProjectedAliases(B,B)` requires the full `{A,B}` key, and slot validation 
fails. This is distinct from the existing boundary/input-pruning threads: the 
consumer runs before the alias exists. Map retained alias slots to 
`subTreeNodes` too, or otherwise enforce the source Project boundary, and add a 
parent-join-predicate regression.
   



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