This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.0
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.0 by this push:
new 9546f5c7cf8 branch-4.0: [feature](nereids) Support dereference
expression #57532 (#58546)
9546f5c7cf8 is described below
commit 9546f5c7cf82c33b27a1a2ba4bc60eb3449e201c
Author: 924060929 <[email protected]>
AuthorDate: Tue Dec 2 10:03:55 2025 +0800
branch-4.0: [feature](nereids) Support dereference expression #57532
(#58546)
picked from #57532
---
.../doris/nereids/parser/LogicalPlanBuilder.java | 4 +-
.../nereids/rules/analysis/BindExpression.java | 22 +--
.../nereids/rules/analysis/ExpressionAnalyzer.java | 156 +++++++++++++++++++--
.../trees/expressions/DereferenceExpression.java | 41 ++++++
.../expressions/visitor/ExpressionVisitor.java | 5 +
.../org/apache/doris/nereids/types/StructType.java | 4 +
.../nereids/rules/analysis/TestDereference.java | 86 ++++++++++++
.../suites/query_p0/test_dereference.groovy | 69 +++++++++
8 files changed, 360 insertions(+), 27 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index 7d08db6595e..72b4317021c 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -522,6 +522,7 @@ import org.apache.doris.nereids.trees.expressions.BitXor;
import org.apache.doris.nereids.trees.expressions.CaseWhen;
import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.DefaultValueSlot;
+import org.apache.doris.nereids.trees.expressions.DereferenceExpression;
import org.apache.doris.nereids.trees.expressions.Divide;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Exists;
@@ -3418,8 +3419,7 @@ public class LogicalPlanBuilder extends
DorisParserBaseVisitor<Object> {
UnboundSlot slot = new UnboundSlot(nameParts,
Optional.empty());
return slot;
} else {
- // todo: base is an expression, may be not a table name.
- throw new ParseException("Unsupported dereference expression:
" + ctx.getText(), ctx);
+ return new DereferenceExpression(e, new
StringLiteral(ctx.identifier().getText()));
}
});
}
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 b3adae21841..bf082635c8d 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
@@ -469,12 +469,12 @@ public class BindExpression implements
AnalysisRuleFactory {
Scope groupBySlotsScope = toScope(cascadesContext,
groupBySlots.build());
return (analyzer, unboundSlot) -> {
- List<Slot> boundInGroupBy =
analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope);
+ List<Expression> boundInGroupBy =
analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope);
if (!boundInGroupBy.isEmpty()) {
return ImmutableList.of(boundInGroupBy.get(0));
}
- List<Slot> boundInAggOutput =
analyzer.bindSlotByScope(unboundSlot, aggOutputScope);
+ List<Expression> boundInAggOutput =
analyzer.bindSlotByScope(unboundSlot, aggOutputScope);
if (!boundInAggOutput.isEmpty()) {
return ImmutableList.of(boundInAggOutput.get(0));
}
@@ -553,7 +553,7 @@ public class BindExpression implements AnalysisRuleFactory {
SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer(
having, cascadesContext, defaultScope, false, true,
(self, unboundSlot) -> {
- List<Slot> slots = self.bindSlotByScope(unboundSlot,
defaultScope);
+ List<Expression> slots = self.bindSlotByScope(unboundSlot,
defaultScope);
if (!slots.isEmpty()) {
return slots;
}
@@ -1006,7 +1006,7 @@ public class BindExpression implements
AnalysisRuleFactory {
SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer(
qualify, cascadesContext, defaultScope.get(), true, true,
(self, unboundSlot) -> {
- List<Slot> slots = self.bindSlotByScope(unboundSlot,
defaultScope.get());
+ List<Expression> slots = self.bindSlotByScope(unboundSlot,
defaultScope.get());
if (!slots.isEmpty()) {
return slots;
}
@@ -1044,11 +1044,11 @@ public class BindExpression implements
AnalysisRuleFactory {
Scope groupBySlotsScope = toScope(cascadesContext,
groupBySlots.build());
return (analyzer, unboundSlot) -> {
- List<Slot> boundInGroupBy =
analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope);
+ List<Expression> boundInGroupBy =
analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope);
if (!boundInGroupBy.isEmpty()) {
return ImmutableList.of(boundInGroupBy.get(0));
}
- List<Slot> boundInAggOutput =
analyzer.bindSlotByScope(unboundSlot, aggOutputScope);
+ List<Expression> boundInAggOutput =
analyzer.bindSlotByScope(unboundSlot, aggOutputScope);
if (!boundInAggOutput.isEmpty()) {
return ImmutableList.of(boundInAggOutput.get(0));
}
@@ -1368,7 +1368,7 @@ public class BindExpression implements
AnalysisRuleFactory {
// see: https://github.com/apache/doris/pull/15240
//
// first, try to bind by agg.child.output
- List<Slot> slotsInChildren =
self.bindExactSlotsByThisScope(unboundSlot, childOutputScope);
+ List<Expression> slotsInChildren =
self.bindExactSlotsByThisScope(unboundSlot, childOutputScope);
if (slotsInChildren.size() == 1) {
// bind succeed
return slotsInChildren;
@@ -1376,7 +1376,7 @@ public class BindExpression implements
AnalysisRuleFactory {
// 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<Slot> slotsInOutput = self.bindExactSlotsByThisScope(
+ List<Expression> slotsInOutput =
self.bindExactSlotsByThisScope(
unboundSlot, aggOutputScopeWithoutAggFun.get());
if (slotsInOutput.isEmpty()) {
// if slotsInChildren.size() > 1 &&
slotsInOutput.isEmpty(),
@@ -1385,7 +1385,7 @@ public class BindExpression implements
AnalysisRuleFactory {
}
Builder<Expression> useOutputExpr =
ImmutableList.builderWithExpectedSize(slotsInOutput.size());
- for (Slot slotInOutput : slotsInOutput) {
+ for (Expression slotInOutput : slotsInOutput) {
// mappingSlot is provided by
aggOutputScopeWithoutAggFun
// and no non-MappingSlot slot exist in the Scope, so
we
// can direct cast it safely
@@ -1476,7 +1476,7 @@ public class BindExpression implements
AnalysisRuleFactory {
sort, cascadesContext, inputScope, true, false,
(self, unboundSlot) -> {
// first, try to bind slot in Scope(input.output)
- List<Slot> slotsInInput =
self.bindExactSlotsByThisScope(unboundSlot, inputScope);
+ List<Expression> slotsInInput =
self.bindExactSlotsByThisScope(unboundSlot, inputScope);
if (!slotsInInput.isEmpty()) {
// bind succeed
return ImmutableList.of(slotsInInput.get(0));
@@ -1678,7 +1678,7 @@ public class BindExpression implements
AnalysisRuleFactory {
sort, cascadesContext, inputScope, true, false,
(analyzer, unboundSlot) -> {
if (finalInput instanceof LogicalAggregate) {
- List<Slot> boundInOutputWithoutAggFunc =
analyzer.bindSlotByScope(unboundSlot,
+ List<Expression> boundInOutputWithoutAggFunc =
analyzer.bindSlotByScope(unboundSlot,
outputWithoutAggFunc);
if (!boundInOutputWithoutAggFunc.isEmpty()) {
return
ImmutableList.of(boundInOutputWithoutAggFunc.get(0));
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 bb1b844520e..e631ffe1c54 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
@@ -49,6 +49,7 @@ import org.apache.doris.nereids.trees.expressions.BoundStar;
import org.apache.doris.nereids.trees.expressions.CaseWhen;
import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.ComparisonPredicate;
+import org.apache.doris.nereids.trees.expressions.DereferenceExpression;
import org.apache.doris.nereids.trees.expressions.Divide;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.ExprId;
@@ -74,7 +75,9 @@ import
org.apache.doris.nereids.trees.expressions.functions.RewriteWhenAnalyze;
import
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
import
org.apache.doris.nereids.trees.expressions.functions.agg.NullableAggregateFunction;
import
org.apache.doris.nereids.trees.expressions.functions.agg.SupportMultiDistinct;
+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.expressions.functions.scalar.StructElement;
import
org.apache.doris.nereids.trees.expressions.functions.udf.AliasUdfBuilder;
import org.apache.doris.nereids.trees.expressions.functions.udf.JavaUdaf;
import org.apache.doris.nereids.trees.expressions.functions.udf.JavaUdf;
@@ -93,6 +96,8 @@ import org.apache.doris.nereids.types.BigIntType;
import org.apache.doris.nereids.types.BooleanType;
import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.StructField;
+import org.apache.doris.nereids.types.StructType;
import org.apache.doris.nereids.types.TinyIntType;
import org.apache.doris.nereids.util.ExpressionUtils;
import org.apache.doris.nereids.util.TypeCoercionUtils;
@@ -240,6 +245,25 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
}
}
+ @Override
+ public Expression visitDereferenceExpression(DereferenceExpression
dereferenceExpression,
+ ExpressionRewriteContext context) {
+ Expression expression = dereferenceExpression.child(0).accept(this,
context);
+ DataType dataType = expression.getDataType();
+ if (dataType.isStructType()) {
+ StructType structType = (StructType) dataType;
+ StructField field =
structType.getField(dereferenceExpression.fieldName);
+ if (field != null) {
+ return new StructElement(expression,
dereferenceExpression.child(1));
+ }
+ } else if (dataType.isMapType()) {
+ return new ElementAt(expression, dereferenceExpression.child(1));
+ } else if (dataType.isVariantType()) {
+ return new ElementAt(expression, dereferenceExpression.child(1));
+ }
+ throw new AnalysisException("Can not dereference field: " +
dereferenceExpression.fieldName);
+ }
+
@Override
public Expression visitUnboundSlot(UnboundSlot unboundSlot,
ExpressionRewriteContext context) {
Optional<Scope> outerScope = getScope().getOuterScope();
@@ -913,13 +937,13 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
return bindSlotByScope(unboundSlot, getScope());
}
- protected List<Slot> bindExactSlotsByThisScope(UnboundSlot unboundSlot,
Scope scope) {
- List<Slot> candidates = bindSlotByScope(unboundSlot, scope);
+ protected List<Expression> bindExactSlotsByThisScope(UnboundSlot
unboundSlot, Scope scope) {
+ List<Expression> candidates = bindSlotByScope(unboundSlot, scope);
if (candidates.size() == 1) {
return candidates;
}
- List<Slot> extractSlots = Utils.filterImmutableList(candidates, bound
->
- unboundSlot.getNameParts().size() ==
bound.getQualifier().size() + 1
+ 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
@@ -938,33 +962,137 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
}
/** bindSlotByScope */
- public List<Slot> bindSlotByScope(UnboundSlot unboundSlot, Scope scope) {
+ public List<Expression> bindSlotByScope(UnboundSlot unboundSlot, Scope
scope) {
List<String> nameParts = unboundSlot.getNameParts();
Optional<Pair<Integer, Integer>> idxInSql =
unboundSlot.getIndexInSqlString();
int namePartSize = nameParts.size();
switch (namePartSize) {
// column
case 1: {
- return addSqlIndexInfo(bindSingleSlotByName(nameParts.get(0),
scope), idxInSql);
+ return (List<Expression>) bindExpressionByColumn(unboundSlot,
nameParts, idxInSql, scope);
}
// table.column
case 2: {
- return addSqlIndexInfo(bindSingleSlotByTable(nameParts.get(0),
nameParts.get(1), scope), idxInSql);
+ return (List<Expression>)
bindExpressionByTableColumn(unboundSlot, nameParts, idxInSql, scope);
}
// db.table.column
case 3: {
- return addSqlIndexInfo(bindSingleSlotByDb(nameParts.get(0),
nameParts.get(1), nameParts.get(2), scope),
- idxInSql);
+ return (List<Expression>)
bindExpressionByDbTableColumn(unboundSlot, nameParts, idxInSql, scope);
}
// catalog.db.table.column
- case 4: {
- return addSqlIndexInfo(bindSingleSlotByCatalog(
- nameParts.get(0), nameParts.get(1), nameParts.get(2),
nameParts.get(3), scope), idxInSql);
- }
default: {
- throw new AnalysisException("Not supported name: " +
StringUtils.join(nameParts, "."));
+ return (List<Expression>)
bindExpressionByCatalogDbTableColumn(unboundSlot, nameParts, idxInSql, scope);
+ }
+ }
+ }
+
+ private List<? extends Expression> bindExpressionByCatalogDbTableColumn(
+ UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+ List<Slot> slots = addSqlIndexInfo(bindSingleSlotByCatalog(
+ nameParts.get(0), nameParts.get(1), nameParts.get(2),
nameParts.get(3), scope), idxInSql);
+ if (slots.isEmpty()) {
+ return bindExpressionByDbTableColumn(unboundSlot, nameParts,
idxInSql, scope);
+ } else if (slots.size() > 1) {
+ return slots;
+ }
+ if (nameParts.size() == 4) {
+ return slots;
+ }
+
+ Optional<Expression> expression = bindNestedFields(
+ unboundSlot, slots.get(0), nameParts.subList(4,
nameParts.size())
+ );
+ if (!expression.isPresent()) {
+ return slots;
+ }
+ return ImmutableList.of(expression.get());
+ }
+
+ private List<? extends Expression> bindExpressionByDbTableColumn(
+ UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+ List<Slot> slots = addSqlIndexInfo(
+ bindSingleSlotByDb(nameParts.get(0), nameParts.get(1),
nameParts.get(2), scope), idxInSql);
+ if (slots.isEmpty()) {
+ return bindExpressionByTableColumn(unboundSlot, nameParts,
idxInSql, scope);
+ } else if (slots.size() > 1) {
+ return slots;
+ }
+ if (nameParts.size() == 3) {
+ return slots;
+ }
+
+ Optional<Expression> expression = bindNestedFields(
+ unboundSlot, slots.get(0), nameParts.subList(3,
nameParts.size())
+ );
+ if (!expression.isPresent()) {
+ return slots;
+ }
+ return ImmutableList.of(expression.get());
+ }
+
+ private List<? extends Expression> bindExpressionByTableColumn(
+ UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+ List<Slot> slots =
addSqlIndexInfo(bindSingleSlotByTable(nameParts.get(0), nameParts.get(1),
scope), idxInSql);
+ if (slots.isEmpty()) {
+ return bindExpressionByColumn(unboundSlot, nameParts, idxInSql,
scope);
+ } else if (slots.size() > 1) {
+ return slots;
+ }
+ if (nameParts.size() == 2) {
+ return slots;
+ }
+
+ Optional<Expression> expression = bindNestedFields(
+ unboundSlot, slots.get(0), nameParts.subList(2,
nameParts.size())
+ );
+ if (!expression.isPresent()) {
+ return slots;
+ }
+ return ImmutableList.of(expression.get());
+ }
+
+ private List<? extends Expression> bindExpressionByColumn(
+ UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
+ List<Slot> slots =
addSqlIndexInfo(bindSingleSlotByName(nameParts.get(0), scope), idxInSql);
+ if (slots.size() != 1) {
+ return slots;
+ }
+ if (nameParts.size() == 1) {
+ return slots;
+ }
+ Optional<Expression> expression = bindNestedFields(
+ unboundSlot, slots.get(0), nameParts.subList(1,
nameParts.size())
+ );
+ if (!expression.isPresent()) {
+ return slots;
+ }
+ return ImmutableList.of(expression.get());
+ }
+
+ private Optional<Expression> bindNestedFields(UnboundSlot unboundSlot,
Slot slot, List<String> fieldNames) {
+ Expression expression = slot;
+ String lastFieldName = slot.getName();
+ for (String fieldName : fieldNames) {
+ DataType dataType = expression.getDataType();
+ if (dataType.isStructType()) {
+ StructType structType = (StructType) dataType;
+ StructField field = structType.getField(fieldName);
+ if (field == null) {
+ throw new AnalysisException("No such struct field '" +
fieldName + "' in '" + lastFieldName + "'");
+ }
+ lastFieldName = fieldName;
+ expression = new StructElement(expression, new
StringLiteral(fieldName));
+ continue;
+ } else if (dataType.isMapType()) {
+ expression = new ElementAt(expression, new
StringLiteral(fieldName));
+ continue;
+ } else if (dataType.isVariantType()) {
+ expression = new ElementAt(expression, new
StringLiteral(fieldName));
+ continue;
}
+ throw new AnalysisException("No such field '" + fieldName + "' in
'" + lastFieldName + "'");
}
+ return Optional.of(new Alias(expression));
}
public static boolean sameTableName(String boundSlot, String unboundSlot) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/DereferenceExpression.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/DereferenceExpression.java
new file mode 100644
index 00000000000..41f6048cf30
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/DereferenceExpression.java
@@ -0,0 +1,41 @@
+// 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.trees.expressions;
+
+import org.apache.doris.nereids.analyzer.Unbound;
+import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+
+import com.google.common.collect.ImmutableList;
+
+/** DereferenceExpression*/
+public class DereferenceExpression extends Expression implements
BinaryExpression, PropagateNullable, Unbound {
+ public final String fieldName;
+
+ public DereferenceExpression(Expression expression, StringLiteral
fieldName) {
+ super(ImmutableList.of(expression, fieldName));
+ this.fieldName = fieldName.getValue();
+ }
+
+ @Override
+ public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+ return visitor.visitDereferenceExpression(this, context);
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ExpressionVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ExpressionVisitor.java
index 1b508a9aaa6..981e0e964ce 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ExpressionVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ExpressionVisitor.java
@@ -41,6 +41,7 @@ import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.ComparisonPredicate;
import org.apache.doris.nereids.trees.expressions.CompoundPredicate;
import org.apache.doris.nereids.trees.expressions.DefaultValueSlot;
+import org.apache.doris.nereids.trees.expressions.DereferenceExpression;
import org.apache.doris.nereids.trees.expressions.Divide;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Exists;
@@ -557,4 +558,8 @@ public abstract class ExpressionVisitor<R, C>
public R visitUnboundVariable(UnboundVariable unboundVariable, C context) {
return visit(unboundVariable, context);
}
+
+ public R visitDereferenceExpression(DereferenceExpression
dereferenceExpression, C context) {
+ return visit(dereferenceExpression, context);
+ }
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
index ffbff7c61e1..fb8557923aa 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
@@ -73,6 +73,10 @@ public class StructType extends DataType implements
ComplexDataType {
return nameToFields;
}
+ public StructField getField(String name) {
+ return nameToFields.get(name.toLowerCase());
+ }
+
@Override
public DataType conversion() {
return new
StructType(fields.stream().map(StructField::conversion).collect(Collectors.toList()));
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
new file mode 100644
index 00000000000..4731a4c988b
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
@@ -0,0 +1,86 @@
+// 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.rules.analysis;
+
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.PrimitiveType;
+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.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.Test;
+
+import java.util.List;
+import java.util.Map;
+
+public class TestDereference extends TestWithFeService {
+
+ private static final Map<String, Map<String, List<Column>>> CATALOG_META =
ImmutableMap.of(
+ "t", ImmutableMap.of(
+ "t", ImmutableList.of(
+ new Column("id", PrimitiveType.INT),
+ new Column("t", new VariantType())
+ )
+ )
+ );
+
+ @Override
+ protected void runBeforeAll() throws Exception {
+ FeConstants.runningUnitTest = true;
+ createCatalog("create catalog t properties("
+ + " \"type\"=\"test\","
+ + "
\"catalog_provider.class\"=\"org.apache.doris.nereids.rules.analysis.TestDereference$CustomCatalogProvider\""
+ + ")");
+ connectContext.changeDefaultCatalog("t");
+ useDatabase("t");
+ }
+
+ @Test
+ public void testBindPriority() {
+ // column
+ testBind("select t from t");
+ // table.column
+ testBind("select t.t from t");
+ // db.table.column
+ testBind("select t.t.t from t");
+ // catalog.db.table.column
+ testBind("select t.t.t.t from t");
+ // catalog.db.table.column.subColumn
+ testBind("select t.t.t.t.t from t");
+ // catalog.db.table.column.subColumn.subColumn2
+ testBind("select t.t.t.t.t.t from t");
+ }
+
+ private void testBind(String sql) {
+ PlanChecker.from(connectContext)
+ .analyze(sql)
+ .rewrite();
+ }
+
+ public static class CustomCatalogProvider implements TestCatalogProvider {
+
+ @Override
+ public Map<String, Map<String, List<Column>>> getMetadata() {
+ return CATALOG_META;
+ }
+ }
+}
diff --git a/regression-test/suites/query_p0/test_dereference.groovy
b/regression-test/suites/query_p0/test_dereference.groovy
new file mode 100644
index 00000000000..30c123e3c37
--- /dev/null
+++ b/regression-test/suites/query_p0/test_dereference.groovy
@@ -0,0 +1,69 @@
+// 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.
+
+import com.google.common.collect.Lists
+
+suite("test_dereference") {
+ multi_sql """
+ drop table if exists test_dereference;
+ create table test_dereference(
+ id int,
+ a array<int>,
+ m map<string, int>,
+ s struct<a: int, b: double>,
+ v variant
+ )
+ distributed by hash(id)
+ properties(
+ 'replication_num'='1'
+ );
+
+ insert into test_dereference
+ values (1, array(1, 2, 3, 4, 5), map('a', 1, 'b', 2, 'c', 3),
struct(1, 2), '{"v": {"v":200}}')
+ """
+
+ test {
+ sql "select cardinality(a), map_size(m), map_keys(m), map_values(m),
m.a, m.b, m.c, s.a, s.b, v.v.v from test_dereference"
+ result([[5L, 3L, '["a", "b", "c"]', '[1, 2, 3]', 1, 2, 3, 1, 2d,
"200"]])
+ }
+
+ multi_sql """
+ drop table if exists test_dereference2;
+ create table test_dereference2(
+ id int,
+ s struct<s:struct<s:struct<s:int>>>,
+ v variant
+ )
+ distributed by hash(id)
+ properties(
+ 'replication_num'='1'
+ );
+
+ insert into test_dereference2
+ values (1, struct(struct(struct(100))), '{"v": {"v": 200}}')
+ """
+
+ test {
+ sql "select s.s.s.s, v.v.v from test_dereference2"
+ result([[100, "200"]])
+ }
+
+ test {
+ sql "select s.a from test_dereference2"
+ exception "No such struct field 'a' in 's'"
+ }
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]