This is an automated email from the ASF dual-hosted git repository.
CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 4d9468cdba3 [fix](subquery) Resolve correlated qualified columns
before dereference (#67438)
4d9468cdba3 is described below
commit 4d9468cdba36a227f071ad9d7a56fcc8c03b8f16
Author: Calvin Kirs <[email protected]>
AuthorDate: Mon Sep 7 10:47:28 2026 +0800
[fix](subquery) Resolve correlated qualified columns before dereference
(#67438)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
In a correlated subquery, a multipart reference can represent either a
relation-qualified column (`table_alias.column`) or a nested-field
dereference (`column.field`). Nereids previously searched the inner
scope completely before checking the outer scope. If an inner table had
a physical column with the same name as an outer table alias, the outer
reference could therefore be interpreted as a nested field of that inner
column.
For scalar inner columns this raised a `No such field` analysis error.
For complex inner columns it could bind successfully to the wrong
expression and produce incorrect results.
#### Example
```sql
CREATE TABLE outer_events (
id INT,
`@event_name` VARCHAR(32)
)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES ("replication_num" = "1");
CREATE TABLE inner_events (
id INT,
t1 INT
)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES ("replication_num" = "1");
INSERT INTO outer_events VALUES (1, 'blocked'), (2, 'kept');
INSERT INTO inner_events VALUES (1, 0);
SELECT t1.id, t1.`@event_name`
FROM outer_events t1
WHERE NOT EXISTS (
SELECT 1
FROM inner_events inner_alias
WHERE t1.`@event_name` = 'blocked'
)
ORDER BY t1.id;
```
Before this fix, the outer alias `t1` conflicted with the physical inner
column `inner_events.t1`. Nereids treated ``t1.`@event_name``` as a
nested-field access on the inner scalar column and failed during
analysis:
```text
No such field '@event_name' in 't1'
```
After this fix, ``t1.`@event_name``` is correctly bound to the outer
relation alias and the query returns:
```text
+------+-------------+
| id | @event_name |
+------+-------------+
| 2 | kept |
+------+-------------+
```
This change resolves relation-qualified columns in the current and outer
scopes before falling back to first-part-as-column dereference. The
relation-only phase follows each analyzer's complete local scope order
before searching the outer scope, so custom HAVING and QUALIFY scopes
preserve normal nearest-relation shadowing.
Lambda lexical scope is preserved: in `array_map(x -> x.value,
x.items)`, the first `x` inside the lambda resolves to the lambda
argument while the second `x` resolves to the enclosing table alias.
When an outer qualified reference contains nested fields, such as
`outer_alias.payload.k`, the underlying `payload` slot is also recorded
as a correlated slot.
### Release note
Fix incorrect Nereids column binding when an outer table alias conflicts
with an inner column name in a correlated subquery.
### Check List (For Author)
- Test: Regression test / Unit Test
- `./run-fe-ut.sh --run
org.apache.doris.nereids.rules.analysis.TestDereference`
- Result: 10 tests passed, 0 failures
- Added `query_p0/test_dereference` regression coverage for scalar and
complex inner columns, lambda lexical scope, outer nested-field
correlation, reused inner/outer aliases in HAVING and QUALIFY, and local
qualifier shadowing
- `./run-regression-test.sh --run -d query_p0 -s test_dereference`
- Result: suite passed against the generated snapshot
- Behavior changed: Yes (relation-qualified correlated references now
take priority over inner column-field dereference while preserving
complete local-scope precedence)
- Does this need documentation: No
---
.../org/apache/doris/nereids/analyzer/Scope.java | 19 ++
.../nereids/rules/analysis/BindExpression.java | 158 ++++++++++------
.../nereids/rules/analysis/ExpressionAnalyzer.java | 203 +++++++++++++++++++--
.../apache/doris/nereids/analyzer/ScopeTest.java | 48 +++++
.../rules/analysis/ExpressionAnalyzerTest.java | 51 ++++++
.../nereids/rules/analysis/TestDereference.java | 135 ++++++++++++++
regression-test/data/query_p0/test_dereference.out | 40 ++++
.../suites/query_p0/test_dereference.groovy | 168 ++++++++++++++++-
8 files changed, 752 insertions(+), 70 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 74019610b15..3ee5312f4b0 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
@@ -625,7 +625,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());
@@ -640,19 +641,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);
};
});
@@ -693,9 +698,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);
}
}
};
@@ -724,12 +739,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);
@@ -1358,12 +1375,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()) {
@@ -1383,7 +1402,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(() -> {
@@ -1396,17 +1416,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);
};
});
@@ -1414,7 +1438,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);
}
};
@@ -1704,27 +1734,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, aggOutputScope.get());
- if (slotsInOutput.isEmpty()) {
+ ExpressionAnalyzer.SlotBinding slotsInOutput =
self.bindExactSlotsByThisScope(
+ unboundSlot, aggOutputScope.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 aggOutputScope
// and no non-MappingSlot slot exist in the Scope, so
we
// can direct cast it safely
@@ -1741,7 +1774,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());
@@ -1816,17 +1851,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,
@@ -1951,7 +1989,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);
@@ -1979,7 +2022,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) {
@@ -2019,15 +2063,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 bf7e872de20..569dd874a44 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
@@ -124,6 +124,8 @@ import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@@ -312,14 +314,36 @@ 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). In a correlated subquery, try the
relation-qualified interpretation
+ // in both visible scopes first, so an inner column named "t" does not
hide an outer alias "t".
+ if (canBindOuterScope && shouldPrioritizeRelationQualifier()
+ && unboundSlot.getNameParts().size() > 1) {
+ SlotBinding localRelationBinding =
bindSlotByRelationQualifierInThisScope(unboundSlot);
+ bounded = localRelationBinding.getBoundSlots();
+ foundInThisScope = !bounded.isEmpty();
+ if (!foundInThisScope) {
+ relationQualifierOccupied =
localRelationBinding.isRelationQualifierOccupied();
+ }
+ if (!foundInThisScope && !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());
}
@@ -333,14 +357,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()) {
@@ -456,6 +481,11 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
ExpressionAnalyzer lambdaAnalyzer = new
ExpressionAnalyzer(currentPlan, new Scope(Optional.of(getScope()),
boundedSlots), context == null ? null :
context.cascadesContext,
true, true) {
+ @Override
+ protected boolean shouldPrioritizeRelationQualifier() {
+ return false;
+ }
+
@Override
protected void couldNotFoundColumn(UnboundSlot unboundSlot, String
tableName) {
throw new AnalysisException("Unknown lambda slot '"
@@ -466,6 +496,11 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
return lambdaAnalyzer.analyze(lambdaFunction, context);
}
+ /** Whether relation-qualified columns should be resolved across scopes
before nested fields. */
+ protected boolean shouldPrioritizeRelationQualifier() {
+ return true;
+ }
+
UnboundFunction preProcessUnboundFunction(UnboundFunction unboundFunction,
ExpressionRewriteContext context) {
// NOTICE: some trick code here. because for time arithmetic functions,
// the first argument of them is TimeUnit, but is cannot distinguish
with UnboundSlot in parser.
@@ -1165,17 +1200,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) {
@@ -1214,12 +1259,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);
}
@@ -1238,9 +1399,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);
}
@@ -1259,9 +1426,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/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 4f15f00ab56..c09ad82fb65 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
@@ -51,9 +51,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..ba932cb5b4c 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,23 @@ 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())
)
)
);
@@ -70,6 +99,112 @@ 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());
+ }
+
+ 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/regression-test/data/query_p0/test_dereference.out
b/regression-test/data/query_p0/test_dereference.out
new file mode 100644
index 00000000000..0bdaece517c
--- /dev/null
+++ b/regression-test/data/query_p0/test_dereference.out
@@ -0,0 +1,40 @@
+-- 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
+
diff --git a/regression-test/suites/query_p0/test_dereference.groovy
b/regression-test/suites/query_p0/test_dereference.groovy
index 30c123e3c37..d6c855ac217 100644
--- a/regression-test/suites/query_p0/test_dereference.groovy
+++ b/regression-test/suites/query_p0/test_dereference.groovy
@@ -66,4 +66,170 @@ 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
+ """
+
+ 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]