This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 8475be88741 branch-4.1: [fix](dereference) Resolve correlated
qualified columns before dereference (#67438) (#68260) (#68062)
8475be88741 is described below
commit 8475be88741ab0e688d5c7b7f96006d5ea1f3fe1
Author: Calvin Kirs <[email protected]>
AuthorDate: Wed Sep 23 10:24:09 2026 +0800
branch-4.1: [fix](dereference) Resolve correlated qualified columns before
dereference (#67438) (#68260) (#68062)
https://github.com/apache/doris/pull/67438
https://github.com/apache/doris/pull/68260
---
.../org/apache/doris/nereids/analyzer/Scope.java | 19 ++
.../nereids/rules/analysis/BindExpression.java | 159 +++++++----
.../nereids/rules/analysis/ExpressionAnalyzer.java | 221 ++++++++++++++--
...ProjectOtherJoinConditionForNestedLoopJoin.java | 10 +
.../apache/doris/nereids/analyzer/ScopeTest.java | 48 ++++
.../rules/analysis/ExpressionAnalyzerTest.java | 51 ++++
.../nereids/rules/analysis/TestDereference.java | 285 ++++++++++++++++++++
...ectOtherJoinConditionForNestedLoopJoinTest.java | 56 ++++
regression-test/data/query_p0/test_dereference.out | 99 +++++++
.../suites/query_p0/test_dereference.groovy | 293 ++++++++++++++++++++-
10 files changed, 1166 insertions(+), 75 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/Scope.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/Scope.java
index cb74698a62b..58590e74f02 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/Scope.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/Scope.java
@@ -21,6 +21,7 @@ import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.util.Utils;
import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
@@ -66,6 +67,7 @@ public class Scope {
private final boolean buildNameToSlot;
private final Supplier<ListMultimap<String, Slot>> nameToSlot;
private final Supplier<ListMultimap<String, Slot>> nameToAsteriskSlot;
+ private final Supplier<ImmutableSetMultimap<String, List<String>>>
relationNameToQualifiers;
public Scope(List<Slot> slots) {
this(Optional.empty(), slots);
@@ -87,6 +89,7 @@ public class Scope {
this.buildNameToSlot = slots.size() > 500;
this.nameToSlot = buildNameToSlot ?
Suppliers.memoize(this::buildNameToSlot) : null;
this.nameToAsteriskSlot = buildNameToSlot ?
Suppliers.memoize(this::buildNameToAsteriskSlot) : null;
+ this.relationNameToQualifiers =
Suppliers.memoize(this::buildRelationNameToQualifiers);
this.asteriskSlots = Utils.fastToImmutableList(
Objects.requireNonNull(asteriskSlots, "asteriskSlots can not
be null"));
}
@@ -107,6 +110,11 @@ public class Scope {
return correlatedSlots;
}
+ /** Find distinct relation qualifiers by relation name, ignoring case. */
+ public Set<List<String>> findRelationQualifiersIgnoreCase(String
relationName) {
+ return
relationNameToQualifiers.get().get(relationName.toUpperCase(Locale.ROOT));
+ }
+
/** findSlotIgnoreCase */
public List<Slot> findSlotIgnoreCase(String slotName, boolean all) {
List<Slot> slots = all ? this.slots : this.asteriskSlots;
@@ -140,4 +148,15 @@ public class Scope {
}
return map;
}
+
+ private ImmutableSetMultimap<String, List<String>>
buildRelationNameToQualifiers() {
+ ImmutableSetMultimap.Builder<String, List<String>> builder =
ImmutableSetMultimap.builder();
+ for (Slot slot : slots) {
+ if (!slot.getQualifier().isEmpty()) {
+ List<String> qualifier = slot.getQualifier();
+ builder.put(qualifier.get(qualifier.size() -
1).toUpperCase(Locale.ROOT), qualifier);
+ }
+ }
+ return builder.build();
+ }
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
index 02737980a3a..f6b4d49396b 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
@@ -669,7 +669,8 @@ public class BindExpression implements AnalysisRuleFactory {
Supplier<CustomSlotBinderAnalyzer> bindByAggChild =
Suppliers.memoize(() -> {
Scope aggChildOutputScope
= toScope(cascadesContext,
PlanUtils.fastGetChildrenOutputs(aggregate.children()));
- return (analyzer, unboundSlot) ->
analyzer.bindSlotByScope(unboundSlot, aggChildOutputScope);
+ return (analyzer, unboundSlot, bindRelationQualifierOnly) ->
+ analyzer.bindSlotByScope(unboundSlot, aggChildOutputScope,
bindRelationQualifierOnly);
});
Scope aggOutputScope = toScope(cascadesContext, aggregate.getOutput());
@@ -684,19 +685,23 @@ public class BindExpression implements
AnalysisRuleFactory {
}
Scope groupBySlotsScope = toScope(cascadesContext,
groupBySlots.build());
- return (analyzer, unboundSlot) -> {
- List<Expression> boundInGroupBy =
analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope);
- if (!boundInGroupBy.isEmpty()) {
- return ImmutableList.of(boundInGroupBy.get(0));
+ return (analyzer, unboundSlot, bindRelationQualifierOnly) -> {
+ ExpressionAnalyzer.SlotBinding boundInGroupBy =
analyzer.bindSlotByScope(
+ unboundSlot, groupBySlotsScope,
bindRelationQualifierOnly);
+ if (!boundInGroupBy.getBoundSlots().isEmpty()) {
+ return boundInGroupBy.firstOrEmpty();
}
- List<Expression> boundInAggOutput =
analyzer.bindSlotByScope(unboundSlot, aggOutputScope);
- if (!boundInAggOutput.isEmpty()) {
- return ImmutableList.of(boundInAggOutput.get(0));
+ ExpressionAnalyzer.SlotBinding boundInAggOutput =
analyzer.bindSlotByScope(
+ unboundSlot, aggOutputScope,
bindRelationQualifierOnly);
+ if (!boundInAggOutput.getBoundSlots().isEmpty()) {
+ return
boundInAggOutput.firstOrEmpty().withQualifierOccupancyFrom(boundInGroupBy);
}
- List<? extends Expression> expressions =
bindByAggChild.get().bindSlot(analyzer, unboundSlot);
- return expressions.isEmpty() ? expressions :
ImmutableList.of(expressions.get(0));
+ return bindByAggChild.get().bindSlot(analyzer, unboundSlot,
bindRelationQualifierOnly)
+ .firstOrEmpty()
+ .withQualifierOccupancyFrom(boundInGroupBy)
+ .withQualifierOccupancyFrom(boundInAggOutput);
};
});
@@ -737,9 +742,19 @@ public class BindExpression implements AnalysisRuleFactory
{
@Override
protected List<? extends Expression>
bindSlotByThisScope(UnboundSlot unboundSlot) {
if (currentIsInAggregateFunction) {
- return bindByAggChild.get().bindSlot(this, unboundSlot);
+ return bindByAggChild.get().bindSlot(this, unboundSlot,
false).getBoundSlots();
} else {
- return
bindByGroupByThenAggOutputThenAggChild.get().bindSlot(this, unboundSlot);
+ return bindByGroupByThenAggOutputThenAggChild.get()
+ .bindSlot(this, unboundSlot,
false).getBoundSlots();
+ }
+ }
+
+ @Override
+ protected SlotBinding
bindSlotByRelationQualifierInThisScope(UnboundSlot unboundSlot) {
+ if (currentIsInAggregateFunction) {
+ return bindByAggChild.get().bindSlot(this, unboundSlot,
true);
+ } else {
+ return
bindByGroupByThenAggOutputThenAggChild.get().bindSlot(this, unboundSlot, true);
}
}
};
@@ -768,12 +783,14 @@ public class BindExpression implements
AnalysisRuleFactory {
SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer(
having, cascadesContext, defaultScope, false, true,
- (self, unboundSlot) -> {
- List<Expression> slots = self.bindSlotByScope(unboundSlot,
defaultScope);
- if (!slots.isEmpty()) {
+ (self, unboundSlot, bindRelationQualifierOnly) -> {
+ ExpressionAnalyzer.SlotBinding slots =
self.bindSlotByScope(
+ unboundSlot, defaultScope,
bindRelationQualifierOnly);
+ if (!slots.getBoundSlots().isEmpty()) {
return slots;
}
- return self.bindSlotByScope(unboundSlot,
backupScope.get());
+ return self.bindSlotByScope(unboundSlot,
backupScope.get(), bindRelationQualifierOnly)
+ .withQualifierOccupancyFrom(slots);
});
ImmutableSet.Builder<Expression> boundConjuncts =
ImmutableSet.builder();
Map<Expression, Expression> bindUniqueIdReplaceMap =
getBelowAggregateGroupByUniqueFuncReplaceMap(having);
@@ -1264,12 +1281,14 @@ public class BindExpression implements
AnalysisRuleFactory {
SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer(
qualify, cascadesContext, defaultScope.get(), true, true,
- (self, unboundSlot) -> {
- List<Expression> slots = self.bindSlotByScope(unboundSlot,
defaultScope.get());
- if (!slots.isEmpty()) {
- return slots;
- }
- return self.bindSlotByScope(unboundSlot, backupScope);
+ (self, unboundSlot, bindRelationQualifierOnly) -> {
+ ExpressionAnalyzer.SlotBinding slots =
self.bindSlotByScope(
+ unboundSlot, defaultScope.get(),
bindRelationQualifierOnly);
+ if (!slots.getBoundSlots().isEmpty()) {
+ return slots;
+ }
+ return self.bindSlotByScope(unboundSlot, backupScope,
bindRelationQualifierOnly)
+ .withQualifierOccupancyFrom(slots);
});
Map<Expression, Expression> bindUniqueIdReplaceMap =
getBelowAggregateGroupByUniqueFuncReplaceMap(qualify);
for (Expression expr : qualify.getConjuncts()) {
@@ -1289,7 +1308,8 @@ public class BindExpression implements
AnalysisRuleFactory {
Supplier<CustomSlotBinderAnalyzer> bindByAggChild =
Suppliers.memoize(() -> {
Scope aggChildOutputScope
= toScope(cascadesContext,
PlanUtils.fastGetChildrenOutputs(aggregate.children()));
- return (analyzer, unboundSlot) ->
analyzer.bindSlotByScope(unboundSlot, aggChildOutputScope);
+ return (analyzer, unboundSlot, bindRelationQualifierOnly) ->
+ analyzer.bindSlotByScope(unboundSlot, aggChildOutputScope,
bindRelationQualifierOnly);
});
Scope aggOutputScope = toScope(cascadesContext, aggregate.getOutput());
Supplier<CustomSlotBinderAnalyzer>
bindByGroupByThenAggOutputThenAggChildOutput = Suppliers.memoize(() -> {
@@ -1302,17 +1322,21 @@ public class BindExpression implements
AnalysisRuleFactory {
}
Scope groupBySlotsScope = toScope(cascadesContext,
groupBySlots.build());
- return (analyzer, unboundSlot) -> {
- List<Expression> boundInGroupBy =
analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope);
- if (!boundInGroupBy.isEmpty()) {
- return ImmutableList.of(boundInGroupBy.get(0));
+ return (analyzer, unboundSlot, bindRelationQualifierOnly) -> {
+ ExpressionAnalyzer.SlotBinding boundInGroupBy =
analyzer.bindSlotByScope(
+ unboundSlot, groupBySlotsScope,
bindRelationQualifierOnly);
+ if (!boundInGroupBy.getBoundSlots().isEmpty()) {
+ return boundInGroupBy.firstOrEmpty();
}
- List<Expression> boundInAggOutput =
analyzer.bindSlotByScope(unboundSlot, aggOutputScope);
- if (!boundInAggOutput.isEmpty()) {
- return ImmutableList.of(boundInAggOutput.get(0));
+ ExpressionAnalyzer.SlotBinding boundInAggOutput =
analyzer.bindSlotByScope(
+ unboundSlot, aggOutputScope,
bindRelationQualifierOnly);
+ if (!boundInAggOutput.getBoundSlots().isEmpty()) {
+ return
boundInAggOutput.firstOrEmpty().withQualifierOccupancyFrom(boundInGroupBy);
}
- List<? extends Expression> expressions =
bindByAggChild.get().bindSlot(analyzer, unboundSlot);
- return expressions.isEmpty() ? expressions :
ImmutableList.of(expressions.get(0));
+ return bindByAggChild.get().bindSlot(analyzer, unboundSlot,
bindRelationQualifierOnly)
+ .firstOrEmpty()
+ .withQualifierOccupancyFrom(boundInGroupBy)
+ .withQualifierOccupancyFrom(boundInAggOutput);
};
});
@@ -1320,7 +1344,13 @@ public class BindExpression implements
AnalysisRuleFactory {
true, true) {
@Override
protected List<? extends Expression>
bindSlotByThisScope(UnboundSlot unboundSlot) {
- return
bindByGroupByThenAggOutputThenAggChildOutput.get().bindSlot(this, unboundSlot);
+ return bindByGroupByThenAggOutputThenAggChildOutput.get()
+ .bindSlot(this, unboundSlot, false).getBoundSlots();
+ }
+
+ @Override
+ protected SlotBinding
bindSlotByRelationQualifierInThisScope(UnboundSlot unboundSlot) {
+ return
bindByGroupByThenAggOutputThenAggChildOutput.get().bindSlot(this, unboundSlot,
true);
}
};
@@ -1626,28 +1656,30 @@ public class BindExpression implements
AnalysisRuleFactory {
SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer(
agg, cascadesContext, childOutputScope, true, true,
- (self, unboundSlot) -> {
+ (self, unboundSlot, bindRelationQualifierOnly) -> {
// see: https://github.com/apache/doris/pull/15240
//
// first, try to bind by agg.child.output
- List<Expression> slotsInChildren =
self.bindExactSlotsByThisScope(unboundSlot, childOutputScope);
- if (slotsInChildren.size() == 1) {
+ ExpressionAnalyzer.SlotBinding slotsInChildren =
self.bindExactSlotsByThisScope(
+ unboundSlot, childOutputScope,
bindRelationQualifierOnly);
+ if (slotsInChildren.getBoundSlots().size() == 1) {
// bind succeed
return slotsInChildren;
}
// second, bind failed:
// if the slot not found, or more than one candidate slots
found in agg.child.output,
// then try to bind by agg.output
- List<Expression> slotsInOutput =
self.bindExactSlotsByThisScope(
- unboundSlot, aggOutputScopeWithoutAggFun.get());
- if (slotsInOutput.isEmpty()) {
+ ExpressionAnalyzer.SlotBinding slotsInOutput =
self.bindExactSlotsByThisScope(
+ unboundSlot, aggOutputScopeWithoutAggFun.get(),
bindRelationQualifierOnly);
+ if (slotsInOutput.getBoundSlots().isEmpty()) {
// if slotsInChildren.size() > 1 &&
slotsInOutput.isEmpty(),
// we return slotsInChildren to throw an ambiguous
slots exception
- return slotsInChildren;
+ return
slotsInChildren.withQualifierOccupancyFrom(slotsInOutput);
}
- Builder<Expression> useOutputExpr =
ImmutableList.builderWithExpectedSize(slotsInOutput.size());
- for (Expression slotInOutput : slotsInOutput) {
+ Builder<Expression> useOutputExpr =
ImmutableList.builderWithExpectedSize(
+ slotsInOutput.getBoundSlots().size());
+ for (Expression slotInOutput :
slotsInOutput.getBoundSlots()) {
// mappingSlot is provided by
aggOutputScopeWithoutAggFun
// and no non-MappingSlot slot exist in the Scope, so
we
// can direct cast it safely
@@ -1664,7 +1696,9 @@ public class BindExpression implements
AnalysisRuleFactory {
// we should rewrite to: select k + 1 as k1 from tbl
group by k + 1
useOutputExpr.add(mappingSlot.getMappingExpression());
}
- return useOutputExpr.build();
+ return new
ExpressionAnalyzer.SlotBinding(useOutputExpr.build(), false)
+ .withQualifierOccupancyFrom(slotsInChildren)
+ .withQualifierOccupancyFrom(slotsInOutput);
});
ImmutableList.Builder<Expression> boundGroupByBuilder =
ImmutableList.builderWithExpectedSize(groupBy.size());
@@ -1741,17 +1775,20 @@ public class BindExpression implements
AnalysisRuleFactory {
() -> toScope(cascadesContext,
PlanUtils.fastGetChildrenOutputs(finalInput.children())));
SimpleExprAnalyzer bindInInputScopeThenInputChildScope =
buildCustomSlotBinderAnalyzer(
sort, cascadesContext, inputScope, true, false,
- (self, unboundSlot) -> {
+ (self, unboundSlot, bindRelationQualifierOnly) -> {
// first, try to bind slot in Scope(input.output)
- List<Expression> slotsInInput =
self.bindExactSlotsByThisScope(unboundSlot, inputScope);
- if (!slotsInInput.isEmpty()) {
+ ExpressionAnalyzer.SlotBinding slotsInInput =
self.bindExactSlotsByThisScope(
+ unboundSlot, inputScope,
bindRelationQualifierOnly);
+ if (!slotsInInput.getBoundSlots().isEmpty()) {
// bind succeed
- return ImmutableList.of(slotsInInput.get(0));
+ return slotsInInput.firstOrEmpty();
}
// second, bind failed:
// if the slot not found, or more than one candidate slots
found in input.output,
// then try to bind by input.children.output
- return self.bindExactSlotsByThisScope(unboundSlot,
inputChildrenScope.get());
+ return self.bindExactSlotsByThisScope(
+ unboundSlot, inputChildrenScope.get(),
bindRelationQualifierOnly)
+ .withQualifierOccupancyFrom(slotsInInput);
});
SimpleExprAnalyzer bindInInputChildScope =
getAnalyzerForOrderByAggFunc(finalInput, cascadesContext, sort,
@@ -1947,7 +1984,12 @@ public class BindExpression implements
AnalysisRuleFactory {
enableExactMatch, bindSlotInOuterScope) {
@Override
protected List<? extends Expression>
bindSlotByThisScope(UnboundSlot unboundSlot) {
- return customSlotBinder.bindSlot(this, unboundSlot);
+ return customSlotBinder.bindSlot(this, unboundSlot,
false).getBoundSlots();
+ }
+
+ @Override
+ protected SlotBinding
bindSlotByRelationQualifierInThisScope(UnboundSlot unboundSlot) {
+ return customSlotBinder.bindSlot(this, unboundSlot, true);
}
};
return expr -> expressionAnalyzer.analyze(expr, rewriteContext);
@@ -1975,7 +2017,8 @@ public class BindExpression implements
AnalysisRuleFactory {
}
private interface CustomSlotBinderAnalyzer {
- List<? extends Expression> bindSlot(ExpressionAnalyzer analyzer,
UnboundSlot unboundSlot);
+ ExpressionAnalyzer.SlotBinding bindSlot(
+ ExpressionAnalyzer analyzer, UnboundSlot unboundSlot, boolean
bindRelationQualifierOnly);
}
public String toSqlWithBackquote(List<Slot> slots) {
@@ -2015,15 +2058,19 @@ public class BindExpression implements
AnalysisRuleFactory {
Scope outputWithoutAggFunc = toScope(cascadesContext,
outputSlots.build());
SimpleExprAnalyzer bindInInputChildScope =
buildCustomSlotBinderAnalyzer(
sort, cascadesContext, inputScope, true, false,
- (analyzer, unboundSlot) -> {
+ (analyzer, unboundSlot, bindRelationQualifierOnly) -> {
if (finalInput instanceof LogicalAggregate) {
- List<Expression> boundInOutputWithoutAggFunc =
analyzer.bindSlotByScope(unboundSlot,
- outputWithoutAggFunc);
- if (!boundInOutputWithoutAggFunc.isEmpty()) {
- return
ImmutableList.of(boundInOutputWithoutAggFunc.get(0));
+ ExpressionAnalyzer.SlotBinding
boundInOutputWithoutAggFunc = analyzer.bindSlotByScope(
+ unboundSlot, outputWithoutAggFunc,
bindRelationQualifierOnly);
+ if
(!boundInOutputWithoutAggFunc.getBoundSlots().isEmpty()) {
+ return boundInOutputWithoutAggFunc.firstOrEmpty();
}
+ return analyzer.bindExactSlotsByThisScope(
+ unboundSlot, inputChildrenScope.get(),
bindRelationQualifierOnly)
+
.withQualifierOccupancyFrom(boundInOutputWithoutAggFunc);
}
- return analyzer.bindExactSlotsByThisScope(unboundSlot,
inputChildrenScope.get());
+ return analyzer.bindExactSlotsByThisScope(
+ unboundSlot, inputChildrenScope.get(),
bindRelationQualifierOnly);
});
return bindInInputChildScope;
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
index 8a3eade3f20..73049cf58c9 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
@@ -120,6 +120,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@@ -286,14 +288,38 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
@Override
public Expression visitUnboundSlot(UnboundSlot unboundSlot,
ExpressionRewriteContext context) {
Optional<Scope> outerScope = getScope().getOuterScope();
- Optional<List<? extends Expression>> boundedOpt =
Optional.of(bindSlotByThisScope(unboundSlot));
- boolean foundInThisScope = !boundedOpt.get().isEmpty();
+ List<? extends Expression> bounded = ImmutableList.of();
+ boolean foundInThisScope = false;
+ boolean canBindOuterScope = bindSlotInOuterScope &&
outerScope.isPresent();
+ boolean relationQualifierOccupied = false;
+
+ // A multipart name can be either a relation-qualified column (t.col)
or a nested field
+ // reference (col.field). Try the relation-qualified interpretation in
every visible scope
+ // first, so a nearer name "t" does not hide a farther relation alias
"t". The visible scopes
+ // are the local ones, which HAVING, QUALIFY and ORDER BY layer from
the select output and its
+ // child output in a clause specific order, and then the outer scope
of a correlated subquery:
+ // select q.v as q from t q order by q.v -- q.v is the column of
relation q, not alias q
+ if (shouldPrioritizeRelationQualifier() &&
unboundSlot.getNameParts().size() > 1) {
+ SlotBinding localRelationBinding =
bindSlotByRelationQualifierInThisScope(unboundSlot);
+ bounded = localRelationBinding.getBoundSlots();
+ foundInThisScope = !bounded.isEmpty();
+ if (!foundInThisScope && canBindOuterScope) {
+ relationQualifierOccupied =
localRelationBinding.isRelationQualifierOccupied();
+ if (!relationQualifierOccupied) {
+ bounded = bindSlotsByRelationQualifier(unboundSlot,
outerScope.get());
+ }
+ }
+ }
+
+ if (bounded.isEmpty()) {
+ bounded = bindSlotByThisScope(unboundSlot);
+ foundInThisScope = !bounded.isEmpty();
+ }
// Currently only looking for symbols on the previous level.
- if (bindSlotInOuterScope && !foundInThisScope &&
outerScope.isPresent()) {
- boundedOpt = Optional.of(bindSlotByScope(unboundSlot,
outerScope.get()));
+ if (canBindOuterScope && bounded.isEmpty() &&
!relationQualifierOccupied) {
+ bounded = bindSlotByScope(unboundSlot, outerScope.get());
}
// it is heavy to deduplicate slots in scope. So we deduplicates
bounded here
- List<? extends Expression> bounded = boundedOpt.get();
if (bounded.size() > 1) {
bounded = bounded.stream().distinct().collect(Collectors.toList());
}
@@ -307,14 +333,15 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
return unboundSlot;
case 1:
Expression firstBound = bounded.get(0);
- if (!foundInThisScope && firstBound instanceof Slot
- &&
!outerScope.get().getCorrelatedSlots().contains(firstBound)) {
+ Set<Slot> inputSlots = firstBound.getInputSlots();
+ if (!foundInThisScope
+ &&
!outerScope.get().getCorrelatedSlots().containsAll(inputSlots)) {
if (currentPlan instanceof LogicalJoin) {
throw new AnalysisException(
"Unsupported correlated subquery with
correlated slot in join conjuncts "
+ currentPlan);
}
- outerScope.get().getCorrelatedSlots().add((Slot)
firstBound);
+ outerScope.get().getCorrelatedSlots().addAll(inputSlots);
}
if (firstBound.getDataType() instanceof NestedColumnPrunable
|| firstBound.getDataType().isVariantType()) {
@@ -419,14 +446,27 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
.map(ArrayItemReference::toSlot)
.collect(ImmutableList.toImmutableList());
+ ExpressionAnalyzer enclosingAnalyzer = this;
ExpressionAnalyzer lambdaAnalyzer = new
ExpressionAnalyzer(currentPlan, new Scope(Optional.of(getScope()),
boundedSlots), context == null ? null :
context.cascadesContext,
true, true) {
@Override
- protected void couldNotFoundColumn(UnboundSlot unboundSlot, String
tableName) {
- throw new AnalysisException("Unknown lambda slot '"
- +
unboundSlot.getNameParts().get(unboundSlot.getNameParts().size() - 1)
- + " in lambda arguments" +
lambda.getLambdaArgumentNames());
+ public Expression visitUnboundSlot(UnboundSlot unboundSlot,
ExpressionRewriteContext context) {
+ // The lambda arguments are the nearest lexical scope. Every
other name is resolved by the
+ // enclosing analyzer rather than by its default scope,
because ORDER BY, HAVING and QUALIFY
+ // layer several local scopes and a correlated subquery sees
its outer scope:
+ // select id from t order by array_sum(array_map(x -> x + v,
arr)) -- v is not in the output
+ if (bindSlotByThisScope(unboundSlot).isEmpty()) {
+ return enclosingAnalyzer.visitUnboundSlot(unboundSlot,
context);
+ }
+ return super.visitUnboundSlot(unboundSlot, context);
+ }
+
+ @Override
+ protected boolean shouldPrioritizeRelationQualifier() {
+ // a name that starts with a lambda argument is a field of it,
even if a relation of the
+ // enclosing scope has the same name: array_map(x -> x.value,
x.items) from t x
+ return false;
}
};
lambdaFunction = lambdaAnalyzer.analyze(lambdaFunction, context);
@@ -437,6 +477,11 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
return unboundFunction.withChildren(ImmutableList.of(lambdaClosure));
}
+ /** Whether relation-qualified columns should be resolved across scopes
before nested fields. */
+ protected boolean shouldPrioritizeRelationQualifier() {
+ return true;
+ }
+
UnboundFunction preProcessUnboundFunction(UnboundFunction unboundFunction,
ExpressionRewriteContext context) {
if (unboundFunction.isHighOrder()) {
unboundFunction = processHighOrderFunction(unboundFunction,
context);
@@ -1031,17 +1076,27 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
return bindSlotByScope(unboundSlot, getScope());
}
+ protected SlotBinding bindSlotByRelationQualifierInThisScope(UnboundSlot
unboundSlot) {
+ return bindSlotByRelationQualifier(unboundSlot, getScope());
+ }
+
protected List<Expression> bindExactSlotsByThisScope(UnboundSlot
unboundSlot, Scope scope) {
- List<Expression> candidates = bindSlotByScope(unboundSlot, scope);
+ return bindExactSlotsByThisScope(unboundSlot, scope,
false).getBoundSlots();
+ }
+
+ protected SlotBinding bindExactSlotsByThisScope(
+ UnboundSlot unboundSlot, Scope scope, boolean
bindRelationQualifierOnly) {
+ SlotBinding binding = bindSlotByScope(unboundSlot, scope,
bindRelationQualifierOnly);
+ List<Expression> candidates = binding.getBoundSlots();
if (candidates.size() == 1) {
- return candidates;
+ return binding;
}
List<Expression> extractSlots = Utils.filterImmutableList(candidates,
bound ->
bound instanceof Slot && unboundSlot.getNameParts().size() ==
((Slot) bound).getQualifier().size() + 1
);
// we should return origin candidates slots if extract slots is empty,
// and then throw an ambiguous exception
- return !extractSlots.isEmpty() ? extractSlots : candidates;
+ return binding.withBoundSlots(!extractSlots.isEmpty() ? extractSlots :
candidates);
}
private List<Slot> addSqlIndexInfo(List<Slot> slots,
Optional<Pair<Integer, Integer>> indexInSql) {
@@ -1080,12 +1135,128 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
}
}
+ protected SlotBinding bindSlotByScope(
+ UnboundSlot unboundSlot, Scope scope, boolean
bindRelationQualifierOnly) {
+ return bindRelationQualifierOnly
+ ? bindSlotByRelationQualifier(unboundSlot, scope)
+ : new SlotBinding(bindSlotByScope(unboundSlot, scope), false);
+ }
+
+ /** Bind a multipart slot as a relation-qualified column, without treating
its first part as a column. */
+ protected SlotBinding bindSlotByRelationQualifier(UnboundSlot unboundSlot,
Scope scope) {
+ List<? extends Expression> bounded =
bindSlotsByRelationQualifier(unboundSlot, scope);
+ return bounded.isEmpty()
+ ? new SlotBinding(bounded,
+ () ->
containsRelationQualifier(unboundSlot.getNameParts(), scope))
+ : new SlotBinding(bounded, false);
+ }
+
+ private List<? extends Expression>
bindSlotsByRelationQualifier(UnboundSlot unboundSlot, Scope scope) {
+ List<String> nameParts = unboundSlot.getNameParts();
+ Optional<Pair<Integer, Integer>> idxInSql =
unboundSlot.getIndexInSqlString();
+ List<? extends Expression> bounded;
+ switch (nameParts.size()) {
+ case 1:
+ bounded = ImmutableList.of();
+ break;
+ case 2:
+ bounded = bindExpressionByTableColumn(
+ unboundSlot, nameParts, idxInSql, scope, false);
+ break;
+ case 3:
+ bounded = bindExpressionByDbTableColumn(
+ unboundSlot, nameParts, idxInSql, scope, false);
+ break;
+ default:
+ bounded = bindExpressionByCatalogDbTableColumn(
+ unboundSlot, nameParts, idxInSql, scope, false);
+ break;
+ }
+ return bounded;
+ }
+
+ private boolean containsRelationQualifier(List<String> nameParts, Scope
scope) {
+ int lastRelationNameIndex = Math.min(2, nameParts.size() - 2);
+ for (int relationNameIndex = 0; relationNameIndex <=
lastRelationNameIndex; relationNameIndex++) {
+ for (List<String> qualifier
+ :
scope.findRelationQualifiersIgnoreCase(nameParts.get(relationNameIndex))) {
+ String catalogName = extractCatalogName(qualifier);
+ int lowerCaseTableNames =
resolveLowerCaseTableNames(catalogName);
+ int lowerCaseDatabaseNames =
resolveLowerCaseDatabaseNames(catalogName);
+ if (nameParts.size() >= 4 && qualifier.size() >= 3
+ && qualifier.get(qualifier.size() -
3).equalsIgnoreCase(nameParts.get(0))
+ &&
compareDbNameIgnoreClusterName(qualifier.get(qualifier.size() - 2),
+ nameParts.get(1), lowerCaseDatabaseNames)
+ && sameTableName(qualifier.get(qualifier.size() - 1),
+ nameParts.get(2), lowerCaseTableNames)) {
+ return true;
+ }
+ if (nameParts.size() >= 3 && qualifier.size() >= 2
+ &&
compareDbNameIgnoreClusterName(qualifier.get(qualifier.size() - 2),
+ nameParts.get(0), lowerCaseDatabaseNames)
+ && sameTableName(qualifier.get(qualifier.size() - 1),
+ nameParts.get(1), lowerCaseTableNames)) {
+ return true;
+ }
+ if (sameTableName(qualifier.get(qualifier.size() - 1),
+ nameParts.get(0), lowerCaseTableNames)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Relation-qualified binding candidates and whether that qualifier
exists in the searched scope. */
+ protected static class SlotBinding {
+ private final List<Expression> boundSlots;
+ private final Supplier<Boolean> relationQualifierOccupied;
+
+ protected SlotBinding(List<? extends Expression> boundSlots, boolean
relationQualifierOccupied) {
+ this(boundSlots, () -> relationQualifierOccupied);
+ }
+
+ private SlotBinding(List<? extends Expression> boundSlots,
Supplier<Boolean> relationQualifierOccupied) {
+ this.boundSlots = ImmutableList.copyOf(boundSlots);
+ this.relationQualifierOccupied = relationQualifierOccupied;
+ }
+
+ protected List<Expression> getBoundSlots() {
+ return boundSlots;
+ }
+
+ protected boolean isRelationQualifierOccupied() {
+ return relationQualifierOccupied.get();
+ }
+
+ protected SlotBinding firstOrEmpty() {
+ return boundSlots.isEmpty()
+ ? this
+ : new SlotBinding(ImmutableList.of(boundSlots.get(0)),
relationQualifierOccupied);
+ }
+
+ private SlotBinding withBoundSlots(List<? extends Expression>
boundSlots) {
+ return new SlotBinding(boundSlots, relationQualifierOccupied);
+ }
+
+ protected SlotBinding withQualifierOccupancyFrom(SlotBinding other) {
+ return new SlotBinding(boundSlots,
+ () -> relationQualifierOccupied.get() ||
other.relationQualifierOccupied.get());
+ }
+ }
+
private List<? extends Expression> bindExpressionByCatalogDbTableColumn(
UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+ return bindExpressionByCatalogDbTableColumn(unboundSlot, nameParts,
idxInSql, scope, true);
+ }
+
+ private List<? extends Expression> bindExpressionByCatalogDbTableColumn(
+ UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql,
+ Scope scope, boolean fallbackToColumn) {
List<Slot> slots = bindSingleSlotByCatalog(
nameParts.get(0), nameParts.get(1), nameParts.get(2),
nameParts.get(3), scope);
if (slots.isEmpty()) {
- return bindExpressionByDbTableColumn(unboundSlot, nameParts,
idxInSql, scope);
+ return bindExpressionByDbTableColumn(unboundSlot, nameParts,
idxInSql, scope, fallbackToColumn);
} else if (slots.size() > 1) {
return addSqlIndexInfo(slots, idxInSql);
}
@@ -1104,9 +1275,15 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
private List<? extends Expression> bindExpressionByDbTableColumn(
UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+ return bindExpressionByDbTableColumn(unboundSlot, nameParts, idxInSql,
scope, true);
+ }
+
+ private List<? extends Expression> bindExpressionByDbTableColumn(
+ UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql,
+ Scope scope, boolean fallbackToColumn) {
List<Slot> slots = bindSingleSlotByDb(nameParts.get(0),
nameParts.get(1), nameParts.get(2), scope);
if (slots.isEmpty()) {
- return bindExpressionByTableColumn(unboundSlot, nameParts,
idxInSql, scope);
+ return bindExpressionByTableColumn(unboundSlot, nameParts,
idxInSql, scope, fallbackToColumn);
} else if (slots.size() > 1) {
return addSqlIndexInfo(slots, idxInSql);
}
@@ -1125,9 +1302,17 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
private List<? extends Expression> bindExpressionByTableColumn(
UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+ return bindExpressionByTableColumn(unboundSlot, nameParts, idxInSql,
scope, true);
+ }
+
+ private List<? extends Expression> bindExpressionByTableColumn(
+ UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql,
+ Scope scope, boolean fallbackToColumn) {
List<Slot> slots = bindSingleSlotByTable(nameParts.get(0),
nameParts.get(1), scope);
if (slots.isEmpty()) {
- return bindExpressionByColumn(unboundSlot, nameParts, idxInSql,
scope);
+ return fallbackToColumn
+ ? bindExpressionByColumn(unboundSlot, nameParts, idxInSql,
scope)
+ : ImmutableList.of();
} else if (slots.size() > 1) {
return addSqlIndexInfo(slots, idxInSql);
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
index 72f1752c375..2d0a032fd9c 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
@@ -23,6 +23,7 @@ import org.apache.doris.nereids.trees.expressions.Alias;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Lambda;
import
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
@@ -103,6 +104,15 @@ public class ProjectOtherJoinConditionForNestedLoopJoin
extends OneRewriteRuleFa
private static class AliasReplacer extends
DefaultExpressionRewriter<ReplacerContext> {
public static AliasReplacer INSTANCE = new AliasReplacer();
+ @Override
+ public Expression visitLambda(Lambda lambda, ReplacerContext ctx) {
+ // A lambda body is evaluated per array item. An expression in it
may reference the lambda
+ // arguments, which are not input slots and which no child of the
join outputs, so it can
+ // not be evaluated in a child Project:
+ // array_map(x -> x + t2.b, [0]) > t1.a -- `x + t2.b` must
stay inside the lambda
+ return lambda;
+ }
+
@Override
public Expression visit(Expression expression, ReplacerContext ctx) {
Set<Slot> input = expression.getInputSlots();
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/analyzer/ScopeTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/analyzer/ScopeTest.java
new file mode 100644
index 00000000000..859874bdbe8
--- /dev/null
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/analyzer/ScopeTest.java
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.analyzer;
+
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.types.IntegerType;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+class ScopeTest {
+
+ @Test
+ void testFindRelationQualifiersIgnoreCase() {
+ List<String> qualifier1 = ImmutableList.of("internal", "db", "t1");
+ List<String> qualifier2 = ImmutableList.of("internal", "db", "t2");
+ Scope scope = new Scope(ImmutableList.of(
+ new SlotReference(new ExprId(1), "c1", IntegerType.INSTANCE,
true, qualifier1),
+ new SlotReference(new ExprId(2), "c2", IntegerType.INSTANCE,
true, qualifier1),
+ new SlotReference(new ExprId(3), "c1", IntegerType.INSTANCE,
true, qualifier2),
+ new SlotReference(new ExprId(4), "unqualified",
IntegerType.INSTANCE, true, ImmutableList.of())));
+
+ Assertions.assertEquals(ImmutableList.of(qualifier1),
+
ImmutableList.copyOf(scope.findRelationQualifiersIgnoreCase("T1")));
+ Assertions.assertEquals(ImmutableList.of(qualifier2),
+
ImmutableList.copyOf(scope.findRelationQualifiersIgnoreCase("t2")));
+
Assertions.assertTrue(scope.findRelationQualifiersIgnoreCase("unqualified").isEmpty());
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
index 052325cac35..7cd3e3ef79b 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
@@ -35,9 +35,60 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
+import java.util.Optional;
+import java.util.Set;
public class ExpressionAnalyzerTest {
+ @Test
+ void testSkipQualifierOccupancyForLocalBindingHit() {
+ SlotReference localSlot = new SlotReference(
+ new ExprId(1), "c", BigIntType.INSTANCE, true,
ImmutableList.of("t"));
+ Scope outerScope = new Scope(ImmutableList.of());
+ Scope localScope = new Scope(Optional.of(outerScope),
ImmutableList.of(localSlot)) {
+ @Override
+ public Set<List<String>> findRelationQualifiersIgnoreCase(String
relationName) {
+ throw new AssertionError("Qualifier occupancy should not be
evaluated for a binding hit");
+ }
+ };
+ ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, localScope,
null, true, true);
+
+ Assertions.assertEquals(localSlot, analyzer.analyze(new
UnboundSlot("t", "c")));
+ }
+
+ @Test
+ void testOuterRelationProbeDoesNotEvaluateQualifierOccupancy() {
+ SlotReference outerSlot = new SlotReference(
+ new ExprId(1), "c", BigIntType.INSTANCE, true,
ImmutableList.of("t"));
+ Scope outerScope = new Scope(ImmutableList.of(outerSlot)) {
+ @Override
+ public Set<List<String>> findRelationQualifiersIgnoreCase(String
relationName) {
+ throw new AssertionError("Outer relation probe should only
bind slots");
+ }
+ };
+ Scope localScope = new Scope(Optional.of(outerScope),
ImmutableList.of());
+ ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, localScope,
null, true, true);
+
+ Assertions.assertEquals(outerSlot, analyzer.analyze(new
UnboundSlot("t", "c")));
+ Assertions.assertEquals(ImmutableList.of(outerSlot),
ImmutableList.copyOf(outerScope.getCorrelatedSlots()));
+ }
+
+ @Test
+ void testKeepQualifierOccupancyLazyInExactBinding() {
+ Scope scope = new Scope(ImmutableList.of()) {
+ @Override
+ public Set<List<String>> findRelationQualifiersIgnoreCase(String
relationName) {
+ throw new AssertionError("Exact binding should preserve lazy
qualifier occupancy");
+ }
+ };
+ ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, scope,
null, true, true);
+
+ ExpressionAnalyzer.SlotBinding binding = Assertions.assertDoesNotThrow(
+ () -> analyzer.bindExactSlotsByThisScope(new UnboundSlot("t",
"c"), scope, true));
+ Assertions.assertTrue(binding.getBoundSlots().isEmpty());
+ Assertions.assertThrows(AssertionError.class,
binding::isRelationQualifierOccupied);
+ }
+
@Test
void testPreProcessUnboundFunctionForThreeArgsDataTimeFunction() {
ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, new
Scope(ImmutableList.of()),
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
index 4731a4c988b..fd44b4d003e 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
@@ -17,18 +17,30 @@
package org.apache.doris.nereids.rules.analysis;
+import org.apache.doris.catalog.ArrayType;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.StructField;
+import org.apache.doris.catalog.StructType;
+import org.apache.doris.catalog.Type;
import org.apache.doris.catalog.VariantType;
import org.apache.doris.common.FeConstants;
import
org.apache.doris.datasource.test.TestExternalCatalog.TestCatalogProvider;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import
org.apache.doris.nereids.trees.expressions.ArrayItemReference.ArrayItemSlot;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ElementAt;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Lambda;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalApply;
import org.apache.doris.nereids.util.PlanChecker;
import org.apache.doris.utframe.TestWithFeService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -39,6 +51,32 @@ public class TestDereference extends TestWithFeService {
"t", ImmutableList.of(
new Column("id", PrimitiveType.INT),
new Column("t", new VariantType())
+ ),
+ "outer_table", ImmutableList.of(
+ new Column("id", PrimitiveType.INT),
+ new Column("value", PrimitiveType.INT),
+ new Column("@event_name", PrimitiveType.VARCHAR),
+ new Column("payload", new StructType(new
StructField("k", Type.INT))),
+ new Column("items", new ArrayType(
+ new StructType(new StructField("value",
Type.INT))))
+ ),
+ "inner_table", ImmutableList.of(
+ new Column("id", PrimitiveType.INT),
+ new Column("t1", PrimitiveType.INT),
+ new Column("t", new StructType(new
StructField("value", Type.INT)))
+ ),
+ "inner_variant_table", ImmutableList.of(
+ new Column("id", PrimitiveType.INT),
+ new Column("outer_alias", new VariantType())
+ ),
+ "shadow_table", ImmutableList.of(
+ new Column("id", PrimitiveType.INT),
+ new Column("v", PrimitiveType.INT),
+ new Column("s", new StructType(new
StructField("v", Type.INT))),
+ new Column("arr", new ArrayType(Type.INT))
+ ),
+ "plain_table", ImmutableList.of(
+ new Column("id", PrimitiveType.INT)
)
)
);
@@ -70,6 +108,253 @@ public class TestDereference extends TestWithFeService {
testBind("select t.t.t.t.t.t from t");
}
+ @Test
+ public void testCorrelatedSubqueryPrefersOuterTableAlias() {
+ testBind("select t1.`@event_name` from outer_table t1 where exists ("
+ + "select 1 from inner_table inner_alias where
t1.`@event_name` = 'click')");
+ }
+
+ @Test
+ public void testOuterTableAliasTakesPriorityOverInnerVariantColumn() {
+ Plan plan = PlanChecker.from(connectContext)
+ .analyze("select outer_alias.id from outer_table outer_alias
where exists ("
+ + "select 1 from inner_variant_table inner_alias where
outer_alias.value = 1)")
+ .getPlan();
+
+ LogicalApply<?, ?> apply = getOnlyApply(plan);
+ Assertions.assertEquals(1, apply.getCorrelationSlot().size());
+ Assertions.assertEquals("value",
apply.getCorrelationSlot().get(0).getName());
+ List<String> qualifier =
apply.getCorrelationSlot().get(0).getQualifier();
+ Assertions.assertEquals("outer_alias", qualifier.get(qualifier.size()
- 1));
+ }
+
+ @Test
+ public void testInnerAliasShadowsOuterAliasInFilter() {
+ Plan plan = PlanChecker.from(connectContext)
+ .analyze("select t.id from outer_table t where exists ("
+ + "select 1 from inner_table t where t.id = 1)")
+ .getPlan();
+
+
Assertions.assertTrue(getOnlyApply(plan).getCorrelationSlot().isEmpty());
+ }
+
+ @Test
+ public void testInnerAliasKeepsNestedFieldFallback() {
+ Plan plan = PlanChecker.from(connectContext)
+ .analyze("select t.id from outer_table t where exists ("
+ + "select 1 from inner_table t where t.value = 1)")
+ .getPlan();
+
+
Assertions.assertTrue(getOnlyApply(plan).getCorrelationSlot().isEmpty());
+ }
+
+ @Test
+ public void testInnerAliasKeepsScalarFieldError() {
+ AnalysisException exception =
Assertions.assertThrows(AnalysisException.class,
+ () -> PlanChecker.from(connectContext)
+ .analyze("select t1.id from outer_table t1 where
exists ("
+ + "select 1 from inner_table t1 where
t1.`@event_name` = 'click')"));
+ Assertions.assertTrue(exception.getMessage().contains("No such field
'@event_name' in 't1'"));
+ }
+
+ @Test
+ public void testLambdaArgumentTakesPriorityOverOuterTableAlias() {
+ Plan plan = PlanChecker.from(connectContext)
+ .analyze("select array_map(x -> x.value, x.items) from
outer_table x")
+ .getPlan();
+
+ List<Lambda> lambdas = new ArrayList<>();
+ for (Plan node : plan.<Plan>collectToList(ignored -> true)) {
+ node.getExpressions().forEach(expression ->
+
lambdas.addAll(expression.collectToList(Lambda.class::isInstance)));
+ }
+ Assertions.assertEquals(1, lambdas.size());
+
Assertions.assertTrue(lambdas.get(0).getLambdaFunction().containsType(ElementAt.class));
+
Assertions.assertTrue(lambdas.get(0).getLambdaFunction().anyMatch(ArrayItemSlot.class::isInstance));
+ }
+
+ @Test
+ public void testOuterNestedFieldRegistersCorrelationSlot() {
+ Plan plan = PlanChecker.from(connectContext)
+ .analyze("select outer_alias.id from outer_table outer_alias
where exists ("
+ + "select 1 from inner_variant_table inner_alias where
outer_alias.payload.k = 1)")
+ .getPlan();
+
+ LogicalApply<?, ?> apply = getOnlyApply(plan);
+ Assertions.assertEquals(1, apply.getCorrelationSlot().size());
+ Assertions.assertEquals("payload",
apply.getCorrelationSlot().get(0).getName());
+ List<String> qualifier =
apply.getCorrelationSlot().get(0).getQualifier();
+ Assertions.assertEquals("outer_alias", qualifier.get(qualifier.size()
- 1));
+ }
+
+ @Test
+ public void testInnerHavingAliasShadowsOuterAlias() {
+ Plan plan = PlanChecker.from(connectContext)
+ .analyze("select t.id from outer_table t where exists ("
+ + "select 1 from inner_table t having max(t.id) > 0)")
+ .getPlan();
+
+
Assertions.assertTrue(getOnlyApply(plan).getCorrelationSlot().isEmpty());
+ }
+
+ @Test
+ public void testInnerQualifyAliasShadowsOuterAlias() {
+ Plan plan = PlanChecker.from(connectContext)
+ .analyze("select t.id from outer_table t where exists ("
+ + "select 1 from inner_table t group by t.id "
+ + "qualify row_number() over (order by id) = t.id)")
+ .getPlan();
+
+
Assertions.assertTrue(getOnlyApply(plan).getCorrelationSlot().isEmpty());
+ }
+
+ @Test
+ public void testOutputAliasDoesNotShadowRelationQualifier() {
+ // the select output is a nearer scope than the relation for ORDER BY,
HAVING and QUALIFY,
+ // q.v should still be the column v of relation q rather than a field
of the scalar output alias q
+ List<String> sqls = ImmutableList.of(
+ "select q.v as q from (select 7 as v) q order by q.v",
+ "select q.v as q from shadow_table q order by q.v",
+ "select distinct q.v as q from shadow_table q order by q.v",
+ "select q.v as p, p.id as q from shadow_table q join
plain_table p on q.id = p.id order by q.v, p.id",
+ "select * from plain_table o where o.id in (select q.v as q
from shadow_table q order by q.v limit 1)",
+ // db.table.column, the alias has the same name as the database
+ "select q.v as t from shadow_table q order by t.q.v",
+ // aggregate
+ "select q.id as q from shadow_table q group by q.id order by
max(q.v)",
+ "select max(q.v) as q from shadow_table q group by q.id order
by q.id",
+ "select q.id + 1 as q from shadow_table q group by q.id + 1
order by q.id + 1",
+ "select q.v as q from shadow_table q having q.v > 0",
+ "select q.id + 1 as q from shadow_table q group by q.id + 1
having q.id + 1 > 0",
+ "select max(q.id) as q from shadow_table q group by q.v having
q.v > 0",
+ "select q.v as q from shadow_table q qualify row_number() over
(order by q.id) = 1 and q.v > 0",
+ "select q.id + 1 as q from shadow_table q group by q.id + 1 "
+ + "qualify row_number() over (order by q.id + 1) = 1"
+ );
+ for (String sql : sqls) {
+ Assertions.assertDoesNotThrow(() ->
PlanChecker.from(connectContext).analyze(sql), sql);
+ }
+ }
+
+ @Test
+ public void testRelationQualifierTakesPriorityOverComplexOutputAlias() {
+ // the output alias q is a struct with field v, q.v should not
silently become element_at(q, 'v')
+ assertBoundToColumnV("select q.s as q from shadow_table q order by
q.v");
+ assertBoundToColumnV("select q.s as q from shadow_table q having q.v >
0");
+ assertBoundToColumnV("select q.s as q from shadow_table q qualify
row_number() over (order by q.v) = 1");
+ }
+
+ @Test
+ public void testOutputAliasWithoutRelationQualifierKeepsScalarFieldError()
{
+ // q is only an output alias here, the relation is p, so q.v is a
field of the scalar alias q
+ AnalysisException exception =
Assertions.assertThrows(AnalysisException.class,
+ () -> PlanChecker.from(connectContext)
+ .analyze("select p.v as q from shadow_table p order by
q.v"));
+ Assertions.assertTrue(exception.getMessage().contains("No such field
'v' in 'q'"),
+ exception.getMessage());
+ }
+
+ @Test
+ public void testRelationQualifierOccupiesNestedOutputAliasPath() {
+ // q.v is the scalar column v of relation q, so q.v.b is not the path
v.b of the struct alias q
+ AnalysisException exception =
Assertions.assertThrows(AnalysisException.class,
+ () -> PlanChecker.from(connectContext)
+ .analyze("select named_struct('v', named_struct('b',
1)) as q "
+ + "from shadow_table q order by q.v.b"));
+ Assertions.assertTrue(exception.getMessage().contains("No such field
'b' in 'v'"),
+ exception.getMessage());
+ }
+
+ @Test
+ public void testOutputAliasKeepsNestedFieldFallback() {
+ // no relation-qualified column matches, so the first part falls back
to the output alias
+ assertBoundToNestedField("select q.s as a from shadow_table q order by
a.v");
+ assertBoundToNestedField("select p.s as q from shadow_table p join
plain_table q on p.id = q.id order by q.v");
+ assertBoundToNestedField("select p.s as q from shadow_table p join
plain_table q on p.id = q.id "
+ + "having q.v > 0");
+ }
+
+ @Test
+ public void testLambdaBodyBindsByEnclosingClauseScopes() {
+ // a name that is not a lambda argument is resolved the same way as
outside the lambda,
+ // ORDER BY, HAVING and QUALIFY can see the child output behind the
select output
+ List<String> sqls = ImmutableList.of(
+ "select id from shadow_table order by array_sum(array_map(x ->
x + v, arr))",
+ "select id from shadow_table q order by array_sum(array_map(x
-> x + q.v, q.arr))",
+ "select q.v as q from shadow_table q order by
array_sum(array_map(x -> x + q.v, q.arr))",
+ "select q.v as q from shadow_table q having
array_sum(array_map(x -> x + q.v, q.arr)) > 0",
+ "select q.id as q from shadow_table q group by q.id "
+ + "having sum(array_sum(array_map(x -> x + q.v,
q.arr))) > 0",
+ "select q.v as q from shadow_table q "
+ + "qualify row_number() over (order by
array_sum(array_map(x -> x + q.v, q.arr))) = 1"
+ );
+ for (String sql : sqls) {
+ Assertions.assertDoesNotThrow(() ->
PlanChecker.from(connectContext).analyze(sql), sql);
+ }
+
+ // a nested lambda resolves through the lambda around it
+ Assertions.assertDoesNotThrow(() ->
PlanChecker.from(connectContext).analyze(
+ "select id from shadow_table q order by "
+ + "array_sum(array_map(x -> array_sum(array_map(y -> y
+ x + q.v, q.arr)), q.arr))"));
+ // the enclosing clause is a join condition, both sides are its own
scope rather than an outer scope
+ Assertions.assertDoesNotThrow(() ->
PlanChecker.from(connectContext).analyze(
+ "select q.id from shadow_table q join plain_table p "
+ + "on array_sum(array_map(x -> x + p.id, q.arr)) >
0"));
+
+ AnalysisException exception =
Assertions.assertThrows(AnalysisException.class,
+ () -> PlanChecker.from(connectContext)
+ .analyze("select id from shadow_table order by
array_map(x -> x + unknown_column, arr)"));
+ Assertions.assertTrue(exception.getMessage().contains("Unknown column
'unknown_column'"),
+ exception.getMessage());
+ }
+
+ @Test
+ public void testLambdaBodyFollowsAmbiguityOfEnclosingClause() {
+ // id is both the output alias of q.id and the output slot p.id,
HAVING does not pick the exact match
+ String having = "select q.id as id, p.id from shadow_table q join
plain_table p on q.id = p.id having ";
+ for (String predicate : ImmutableList.of("id > 0",
"array_sum(array_map(x -> x + id, q.arr)) > 0")) {
+ AnalysisException exception =
Assertions.assertThrows(AnalysisException.class,
+ () -> PlanChecker.from(connectContext).analyze(having +
predicate), predicate);
+ Assertions.assertTrue(exception.getMessage().contains("id is
ambiguous"), exception.getMessage());
+ }
+ }
+
+ @Test
+ public void testLambdaBodyRegistersCorrelationSlot() {
+ // the enclosing analyzer of the subquery filter sees the outer scope,
so does the lambda body
+ Plan plan = PlanChecker.from(connectContext)
+ .analyze("select o.id from plain_table o where exists ("
+ + "select 1 from shadow_table q where
array_sum(array_map(x -> x + o.id, q.arr)) > 0)")
+ .getPlan();
+
+ LogicalApply<?, ?> apply = getOnlyApply(plan);
+ Assertions.assertEquals(1, apply.getCorrelationSlot().size());
+ Assertions.assertEquals("id",
apply.getCorrelationSlot().get(0).getName());
+ List<String> qualifier =
apply.getCorrelationSlot().get(0).getQualifier();
+ Assertions.assertEquals("o", qualifier.get(qualifier.size() - 1));
+ }
+
+ private void assertBoundToColumnV(String sql) {
+ Plan plan = PlanChecker.from(connectContext).analyze(sql).getPlan();
+ Assertions.assertFalse(containsElementAt(plan), sql);
+ }
+
+ private void assertBoundToNestedField(String sql) {
+ Plan plan = PlanChecker.from(connectContext).analyze(sql).getPlan();
+ Assertions.assertTrue(containsElementAt(plan), sql);
+ }
+
+ private boolean containsElementAt(Plan plan) {
+ return plan.anyMatch(node -> ((Plan) node).getExpressions().stream()
+ .anyMatch(expression ->
expression.containsType(ElementAt.class)));
+ }
+
+ private LogicalApply<?, ?> getOnlyApply(Plan plan) {
+ List<LogicalApply<?, ?>> applies =
plan.collectToList(LogicalApply.class::isInstance);
+ Assertions.assertEquals(1, applies.size());
+ return applies.get(0);
+ }
+
private void testBind(String sql) {
PlanChecker.from(connectContext)
.analyze(sql)
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoinTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoinTest.java
index 960500c75f3..ee1ecef9bc7 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoinTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoinTest.java
@@ -18,10 +18,16 @@
package org.apache.doris.nereids.rules.rewrite;
import org.apache.doris.nereids.trees.expressions.Add;
+import org.apache.doris.nereids.trees.expressions.ArrayItemReference;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.LessThan;
import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayMap;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ArraySum;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Lambda;
+import org.apache.doris.nereids.trees.expressions.literal.ArrayLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.plans.JoinType;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
@@ -32,13 +38,23 @@ import org.apache.doris.nereids.util.PlanChecker;
import org.apache.doris.nereids.util.PlanConstructor;
import org.apache.doris.qe.ConnectContext;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class ProjectOtherJoinConditionForNestedLoopJoinTest implements
MemoPatternMatchSupported {
private final LogicalOlapScan scan1 =
PlanConstructor.newLogicalOlapScan(0, "t1", 0);
private final LogicalOlapScan scan2 =
PlanConstructor.newLogicalOlapScan(1, "t2", 0);
+ @AfterEach
+ public void tearDown() {
+ // the scans of the next test should not take their slot ids from the
statement scope of this test,
+ // they would collide with the ids of the aliases the rule creates
+ ConnectContext.remove();
+ }
+
@Test
public void testNestedLoopJoin() {
Slot a = scan1.getOutput().get(1);
@@ -59,6 +75,46 @@ public class ProjectOtherJoinConditionForNestedLoopJoinTest
implements MemoPatte
).printlnTree();
}
+ @Test
+ public void testLambdaBodyIsNotProjected() {
+ // t1.id < array_sum(array_map(x -> x + t2.id, [0]))
+ // all input slots of the higher order function come from t2, it is
projected as a whole with its lambda
+ Slot a = scan1.getOutput().get(0);
+ Slot b = scan2.getOutput().get(0);
+ ArrayItemReference item = new ArrayItemReference("x",
+ new ArrayLiteral(ImmutableList.of(new IntegerLiteral(0))));
+ Lambda lambda = new Lambda(ImmutableList.of("x"), new
Add(item.toSlot(), b), ImmutableList.of(item));
+ Expression otherCondition = new LessThan(a, new ArraySum(new
ArrayMap(lambda)));
+
+ LogicalPlan join = new LogicalPlanBuilder(scan1).join(scan2,
JoinType.CROSS_JOIN,
+ Lists.newArrayList(),
Lists.newArrayList(otherCondition)).build();
+ PlanChecker.from(MemoTestUtils.createConnectContext(), join)
+ .applyTopDown(new ProjectOtherJoinConditionForNestedLoopJoin())
+ .matchesFromRoot(
+ logicalJoin(
+ logicalOlapScan(),
+ // proj list: id, name, array_sum(array_map(x
-> x + id, [0])) AS alias
+ logicalProject().when(proj ->
proj.getProjects().size() == 3
+ &&
proj.getProjects().get(2).containsType(Lambda.class))
+ ).when(j -> j.getOtherJoinConjuncts().stream()
+ .noneMatch(conjunct ->
conjunct.containsType(Lambda.class)))
+ );
+
+ // x + t1.id + t2.id references both sides, the lambda stays in the
join condition as a whole.
+ // `x + t1.id` has only t1.id as input slot, but x is a lambda
argument that no child outputs,
+ // so it can not be projected to the left child
+ Lambda mixed = new Lambda(ImmutableList.of("x"), new Add(new
Add(item.toSlot(), a), b),
+ ImmutableList.of(item));
+ Expression mixedCondition = new LessThan(a, new ArraySum(new
ArrayMap(mixed)));
+ LogicalPlan mixedJoin = new LogicalPlanBuilder(scan1).join(scan2,
JoinType.CROSS_JOIN,
+ Lists.newArrayList(),
Lists.newArrayList(mixedCondition)).build();
+ LogicalPlan rewritten = (LogicalPlan)
PlanChecker.from(MemoTestUtils.createConnectContext(), mixedJoin)
+ .applyTopDown(new ProjectOtherJoinConditionForNestedLoopJoin())
+ .getPlan();
+ Assertions.assertTrue(rewritten.child(0) instanceof LogicalOlapScan);
+ Assertions.assertTrue(rewritten.child(1) instanceof LogicalOlapScan);
+ }
+
@Test
public void testHashJoin() {
Slot id1 = scan1.getOutput().get(0);
diff --git a/regression-test/data/query_p0/test_dereference.out
b/regression-test/data/query_p0/test_dereference.out
new file mode 100644
index 00000000000..039f73c3a5c
--- /dev/null
+++ b/regression-test/data/query_p0/test_dereference.out
@@ -0,0 +1,99 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !correlated_scalar_alias --
+2 kept
+
+-- !correlated_complex_alias --
+2 20
+
+-- !correlated_db_table_qualifier --
+2
+
+-- !correlated_catalog_db_table_qualifier --
+1
+
+-- !lambda_alias --
+1 [1, 2]
+2 [3]
+
+-- !nested_correlation --
+1
+
+-- !having_inner_alias --
+1
+2
+
+-- !qualify_inner_alias --
+1
+2
+
+-- !filter_inner_alias --
+1
+2
+
+-- !filter_inner_nested_field --
+1
+2
+
+-- !group_by_inner_nested_field --
+1
+2
+
+-- !alias_shadow_order_by_subquery_alias --
+7
+
+-- !alias_shadow_order_by --
+10
+20
+30
+
+-- !alias_shadow_having --
+20
+30
+
+-- !alias_shadow_order_by_agg_func --
+3
+2
+1
+
+-- !alias_shadow_order_by_over_agg --
+30
+20
+10
+
+-- !alias_shadow_having_group_by_expr --
+4
+
+-- !alias_shadow_qualify_group_by_expr --
+2
+
+-- !alias_shadow_struct_alias_order_by --
+3
+
+-- !alias_shadow_struct_alias_having --
+1
+
+-- !alias_shadow_keep_alias_field --
+3
+
+-- !alias_shadow_lambda_order_by --
+3
+2
+1
+
+-- !alias_shadow_lambda_having --
+20
+30
+
+-- !alias_shadow_lambda_join_on --
+1 1
+1 2
+2 1
+
+-- !alias_shadow_lambda_correlated_exists --
+1
+2
+
+-- !alias_shadow_lambda_correlated_in --
+1
+2
+
diff --git a/regression-test/suites/query_p0/test_dereference.groovy
b/regression-test/suites/query_p0/test_dereference.groovy
index 30c123e3c37..20a24747846 100644
--- a/regression-test/suites/query_p0/test_dereference.groovy
+++ b/regression-test/suites/query_p0/test_dereference.groovy
@@ -66,4 +66,295 @@ suite("test_dereference") {
sql "select s.a from test_dereference2"
exception "No such struct field 'a' in 's'"
}
-}
\ No newline at end of file
+
+ multi_sql """
+ drop table if exists test_correlated_dereference_outer;
+ drop table if exists test_correlated_dereference_inner_scalar;
+ drop table if exists test_correlated_dereference_inner_struct;
+ create table test_correlated_dereference_outer(
+ id int,
+ value int,
+ `@event_name` varchar(32),
+ payload struct<k:int>,
+ items array<struct<value:int>>
+ )
+ distributed by hash(id)
+ properties('replication_num'='1');
+
+ create table test_correlated_dereference_inner_scalar(
+ id int,
+ t1 int,
+ t struct<value:int>,
+ `${context.dbName}`
struct<test_correlated_dereference_outer:struct<value:int>>,
+ internal
struct<`${context.dbName}`:struct<test_correlated_dereference_outer:struct<value:int>>>
+ )
+ distributed by hash(id)
+ properties('replication_num'='1');
+
+ create table test_correlated_dereference_inner_struct(
+ id int,
+ outer_alias struct<value:int>
+ )
+ distributed by hash(id)
+ properties('replication_num'='1');
+
+ insert into test_correlated_dereference_outer values
+ (1, 10, 'blocked', struct(1), array(struct(1), struct(2))),
+ (2, 20, 'kept', struct(2), array(struct(3)));
+ insert into test_correlated_dereference_inner_scalar values
+ (1, 0, struct(1), struct(struct(0)), struct(struct(struct(0)))),
+ (1, 0, struct(2), struct(struct(0)), struct(struct(struct(0))));
+ insert into test_correlated_dereference_inner_struct values (1,
struct(10));
+ """
+
+ order_qt_correlated_scalar_alias """
+ select t1.id, t1.`@event_name`
+ from test_correlated_dereference_outer t1
+ where not exists (
+ select 1 from test_correlated_dereference_inner_scalar
inner_alias
+ where t1.`@event_name` = 'blocked'
+ )
+ order by t1.id
+ """
+
+ order_qt_correlated_complex_alias """
+ select outer_alias.id, outer_alias.value
+ from test_correlated_dereference_outer outer_alias
+ where not exists (
+ select 1 from test_correlated_dereference_inner_struct
inner_alias
+ where outer_alias.value = 10
+ )
+ order by outer_alias.id
+ """
+
+ order_qt_correlated_db_table_qualifier """
+ select test_correlated_dereference_outer.id
+ from test_correlated_dereference_outer
+ where not exists (
+ select 1 from test_correlated_dereference_inner_scalar
inner_alias
+ where
`${context.dbName}`.`test_correlated_dereference_outer`.value = 10
+ )
+ order by test_correlated_dereference_outer.id
+ """
+
+ order_qt_correlated_catalog_db_table_qualifier """
+ select test_correlated_dereference_outer.id
+ from test_correlated_dereference_outer
+ where not exists (
+ select 1 from test_correlated_dereference_inner_scalar
inner_alias
+ where
internal.`${context.dbName}`.`test_correlated_dereference_outer`.value = 20
+ )
+ order by test_correlated_dereference_outer.id
+ """
+
+ order_qt_lambda_alias """
+ select x.id, array_map(x -> x.value, x.items)
+ from test_correlated_dereference_outer x
+ order by x.id
+ """
+
+ order_qt_nested_correlation """
+ select outer_alias.id
+ from test_correlated_dereference_outer outer_alias
+ where exists (
+ select 1 from test_correlated_dereference_inner_struct
inner_alias
+ where outer_alias.payload.k = 1
+ )
+ order by outer_alias.id
+ """
+
+ order_qt_having_inner_alias """
+ select t.id
+ from test_correlated_dereference_outer t
+ where exists (
+ select 1
+ from test_correlated_dereference_inner_scalar t
+ having max(t.id) < 2
+ )
+ order by t.id
+ """
+
+ order_qt_qualify_inner_alias """
+ select t.id
+ from test_correlated_dereference_outer t
+ where exists (
+ select 1
+ from test_correlated_dereference_inner_scalar t
+ group by t.id
+ qualify row_number() over (order by id) = t.id
+ )
+ order by t.id
+ """
+
+ order_qt_filter_inner_alias """
+ select t.id
+ from test_correlated_dereference_outer t
+ where exists (
+ select 1
+ from test_correlated_dereference_inner_scalar t
+ where t.id = 1
+ )
+ order by t.id
+ """
+
+ order_qt_filter_inner_nested_field """
+ select t.id
+ from test_correlated_dereference_outer t
+ where exists (
+ select 1
+ from test_correlated_dereference_inner_scalar t
+ where t.value = 1
+ )
+ order by t.id
+ """
+
+ order_qt_group_by_inner_nested_field """
+ select t.id
+ from test_correlated_dereference_outer t
+ where not exists (
+ select 1
+ from test_correlated_dereference_inner_scalar t
+ group by t.value
+ having count(*) > 1
+ )
+ order by t.id
+ """
+
+ // An output alias is a nearer scope than the relation for ORDER BY,
HAVING and QUALIFY.
+ // A relation-qualified column should still bind to the relation when an
output alias reuses its name.
+ multi_sql """
+ drop table if exists test_dereference_alias_shadow;
+ create table test_dereference_alias_shadow(
+ id int,
+ v int,
+ s struct<v:int>
+ )
+ distributed by hash(id) buckets 1
+ properties(
+ 'replication_num'='1'
+ );
+
+ insert into test_dereference_alias_shadow
+ values (1, 30, struct(1)), (2, 20, struct(2)), (3, 10, struct(3));
+ """
+
+ qt_alias_shadow_order_by_subquery_alias "select q.v as q from (select 7 as
v) q order by q.v"
+
+ qt_alias_shadow_order_by "select q.v as q from
test_dereference_alias_shadow q order by q.v"
+
+ qt_alias_shadow_having "select q.v as q from test_dereference_alias_shadow
q having q.v > 15 order by q.v"
+
+ qt_alias_shadow_order_by_agg_func """
+ select q.id as q from test_dereference_alias_shadow q group by
q.id order by max(q.v)
+ """
+
+ qt_alias_shadow_order_by_over_agg """
+ select max(q.v) as q from test_dereference_alias_shadow q group by
q.id order by q.id
+ """
+
+ qt_alias_shadow_having_group_by_expr """
+ select q.id + 1 as q from test_dereference_alias_shadow q
+ group by q.id + 1 having q.id + 1 > 3
+ """
+
+ qt_alias_shadow_qualify_group_by_expr """
+ select q.id + 1 as q from test_dereference_alias_shadow q
+ group by q.id + 1 qualify row_number() over (order by q.id + 1) = 1
+ """
+
+ // the output alias q is a struct that has a field v: q.v is still the
column v of relation q
+ qt_alias_shadow_struct_alias_order_by """
+ select id from (
+ select q.id as id, q.s as q from test_dereference_alias_shadow
q order by q.v limit 1
+ ) x
+ """
+
+ qt_alias_shadow_struct_alias_having """
+ select id from (
+ select q.id as id, q.s as q from test_dereference_alias_shadow
q having q.v > 25
+ ) x
+ """
+
+ // no relation-qualified column matches, fall back to the nested field of
the output alias
+ qt_alias_shadow_keep_alias_field """
+ select id from (
+ select p.id as id, p.s as q from test_dereference_alias_shadow
p order by q.v desc limit 1
+ ) x
+ """
+
+ // a lambda body resolves names the same way as the clause around it
+ qt_alias_shadow_lambda_order_by """
+ select id from test_dereference_alias_shadow q
+ order by array_sum(array_map(x -> x + q.v, [1]))
+ """
+
+ qt_alias_shadow_lambda_having """
+ select q.v as q from test_dereference_alias_shadow q
+ having array_sum(array_map(x -> x + q.v, [1])) > 16
+ order by array_sum(array_map(x -> x + q.v, [1]))
+ """
+
+ // a lambda body in a join condition references columns of both sides of
the join
+ qt_alias_shadow_lambda_join_on """
+ select q.id, p.id
+ from test_dereference_alias_shadow q join
test_dereference_alias_shadow p
+ on array_sum(array_map(x -> x + p.v + q.v, [0])) > 40
+ order by q.id, p.id
+ """
+
+ // a lambda body in a correlated subquery references a column of the outer
query
+ qt_alias_shadow_lambda_correlated_exists """
+ select o.id from test_dereference_alias_shadow o
+ where exists (
+ select 1 from test_dereference_alias_shadow q
+ where array_sum(array_map(x -> x + o.v + q.v, [0])) > 45
+ )
+ order by o.id
+ """
+
+ qt_alias_shadow_lambda_correlated_in """
+ select o.id from test_dereference_alias_shadow o
+ where o.id in (
+ select q.id from test_dereference_alias_shadow q
+ where array_sum(array_map(x -> x + o.v, [0])) > 15
+ )
+ order by o.id
+ """
+
+ // q is only a scalar output alias here, the relation is p
+ test {
+ sql "select p.v as q from test_dereference_alias_shadow p order by q.v"
+ exception "No such field 'v' in 'q'"
+ }
+
+ // q.v is the scalar column v of relation q, so q.v.b is not the path v.b
of the struct alias q
+ test {
+ sql """
+ select named_struct('v', named_struct('b', 1)) as q
+ from test_dereference_alias_shadow q order by q.v.b
+ """
+ exception "No such field 'b' in 'v'"
+ }
+
+ // an unknown name in a lambda body reports the error of the clause around
it
+ test {
+ sql """
+ select id from test_dereference_alias_shadow
+ order by array_sum(array_map(x -> x + unknown_column, [1]))
+ """
+ exception "Unknown column 'unknown_column'"
+ }
+
+ test {
+ sql """
+ select t1.id
+ from test_correlated_dereference_outer t1
+ where exists (
+ select 1
+ from test_correlated_dereference_inner_scalar t1
+ where t1.`@event_name` = 'blocked'
+ )
+ """
+ exception "No such field '@event_name' in 't1'"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]