From 020a96fe087baa8e02c890a7816fa3b7e3752ce5 Mon Sep 17 00:00:00 2001 From: 924060929 Date: Fri, 28 Nov 2025 20:23:17 +0800 Subject: [PATCH 1/2] [feature](nereids) Support dereference expression (#57532) Support dereference expression for map/struct/variant ```sql MySQL root@127.0.0.1:rqg> select -> named_struct('c', named_struct('c', 100)).c.c, -> map('a', 10, 'b', 20).a, -> map('a', 10, 'b', 20).b, -> cast('{"a":{"b":300}}' as variant).a.b; +-----------------------------------------------+-------------------------+-------------------------+----------------------------------------+ | named_struct('c', named_struct('c', 100)).c.c | map('a', 10, 'b', 20).a | map('a', 10, 'b', 20).b | cast('{"a":{"b":300}}' as variant).a.b | +-----------------------------------------------+-------------------------+-------------------------+----------------------------------------+ | 100 | 10 | 20 | 300 | +-----------------------------------------------+-------------------------+-------------------------+----------------------------------------+ ``` the slot name parts binding priority: 1. try to bind catalog, if binding failed, jump to 2 2. try to bind database, if binding failed, jump to 3 3. try to bind table, if binding failed, jump to 4 4. try to bind column for example, `a.a.a.a.a` first try to bind: catalog=a, database=a, table=a, column=a, sub fields=a second try to bind: catalog=<current catalog>, database=a, table=a, column=a, sub fields=a.a third try to bind: catalog=<current catalog>, database=<current database>, table=a, column=a, sub fields=a.a.a 4th try to bind: catalog=<current catalog>, database=<current database>, table=<underscore table>, column=a, sub fields=a.a.a.a (cherry picked from commit ac92fd4d89b57b941c52136759d9ad2e80576c8a) --- .../nereids/parser/LogicalPlanBuilder.java | 4 +- .../rules/analysis/BindExpression.java | 22 +-- .../rules/analysis/ExpressionAnalyzer.java | 156 ++++++++++++++++-- .../expressions/DereferenceExpression.java | 41 +++++ .../visitor/ExpressionVisitor.java | 5 + .../rules/analysis/TestDereference.java | 86 ++++++++++ .../suites/query_p0/test_dereference.groovy | 69 ++++++++ 7 files changed, 356 insertions(+), 27 deletions(-) create mode 100644 fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/DereferenceExpression.java create mode 100644 fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java create mode 100644 regression-test/suites/query_p0/test_dereference.groovy 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 905a6008c6c701..c78dff9554cf81 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 @@ -515,6 +515,7 @@ 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; @@ -3408,8 +3409,7 @@ public Expression visitDereference(DereferenceContext ctx) { 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 b3adae21841fef..bf082635c8d8a3 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 @@ private LogicalHaving bindHavingAggregate( Scope groupBySlotsScope = toScope(cascadesContext, groupBySlots.build()); return (analyzer, unboundSlot) -> { - List boundInGroupBy = analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope); + List boundInGroupBy = analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope); if (!boundInGroupBy.isEmpty()) { return ImmutableList.of(boundInGroupBy.get(0)); } - List boundInAggOutput = analyzer.bindSlotByScope(unboundSlot, aggOutputScope); + List boundInAggOutput = analyzer.bindSlotByScope(unboundSlot, aggOutputScope); if (!boundInAggOutput.isEmpty()) { return ImmutableList.of(boundInAggOutput.get(0)); } @@ -553,7 +553,7 @@ private LogicalHaving bindHavingByScopes( SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer( having, cascadesContext, defaultScope, false, true, (self, unboundSlot) -> { - List slots = self.bindSlotByScope(unboundSlot, defaultScope); + List slots = self.bindSlotByScope(unboundSlot, defaultScope); if (!slots.isEmpty()) { return slots; } @@ -1006,7 +1006,7 @@ private void bindQualifyByProject(LogicalProject project, Cascad SimpleExprAnalyzer analyzer = buildCustomSlotBinderAnalyzer( qualify, cascadesContext, defaultScope.get(), true, true, (self, unboundSlot) -> { - List slots = self.bindSlotByScope(unboundSlot, defaultScope.get()); + List slots = self.bindSlotByScope(unboundSlot, defaultScope.get()); if (!slots.isEmpty()) { return slots; } @@ -1044,11 +1044,11 @@ private void bindQualifyByAggregate(Aggregate aggregate, Cascade Scope groupBySlotsScope = toScope(cascadesContext, groupBySlots.build()); return (analyzer, unboundSlot) -> { - List boundInGroupBy = analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope); + List boundInGroupBy = analyzer.bindSlotByScope(unboundSlot, groupBySlotsScope); if (!boundInGroupBy.isEmpty()) { return ImmutableList.of(boundInGroupBy.get(0)); } - List boundInAggOutput = analyzer.bindSlotByScope(unboundSlot, aggOutputScope); + List boundInAggOutput = analyzer.bindSlotByScope(unboundSlot, aggOutputScope); if (!boundInAggOutput.isEmpty()) { return ImmutableList.of(boundInAggOutput.get(0)); } @@ -1368,7 +1368,7 @@ private List bindGroupBy( // see: https://github.com/apache/doris/pull/15240 // // first, try to bind by agg.child.output - List slotsInChildren = self.bindExactSlotsByThisScope(unboundSlot, childOutputScope); + List slotsInChildren = self.bindExactSlotsByThisScope(unboundSlot, childOutputScope); if (slotsInChildren.size() == 1) { // bind succeed return slotsInChildren; @@ -1376,7 +1376,7 @@ private List bindGroupBy( // 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 slotsInOutput = self.bindExactSlotsByThisScope( + List slotsInOutput = self.bindExactSlotsByThisScope( unboundSlot, aggOutputScopeWithoutAggFun.get()); if (slotsInOutput.isEmpty()) { // if slotsInChildren.size() > 1 && slotsInOutput.isEmpty(), @@ -1385,7 +1385,7 @@ private List bindGroupBy( } Builder 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 @@ private Plan bindSortWithoutSetOperation(MatchingContext> ctx) sort, cascadesContext, inputScope, true, false, (self, unboundSlot) -> { // first, try to bind slot in Scope(input.output) - List slotsInInput = self.bindExactSlotsByThisScope(unboundSlot, inputScope); + List slotsInInput = self.bindExactSlotsByThisScope(unboundSlot, inputScope); if (!slotsInInput.isEmpty()) { // bind succeed return ImmutableList.of(slotsInInput.get(0)); @@ -1678,7 +1678,7 @@ private SimpleExprAnalyzer getAnalyzerForOrderByAggFunc(Plan finalInput, Cascade sort, cascadesContext, inputScope, true, false, (analyzer, unboundSlot) -> { if (finalInput instanceof LogicalAggregate) { - List boundInOutputWithoutAggFunc = analyzer.bindSlotByScope(unboundSlot, + List 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 bb1b844520e662..e631ffe1c54b1e 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.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.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.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 Expression visitUnboundAlias(UnboundAlias unboundAlias, ExpressionRewrite } } + @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 outerScope = getScope().getOuterScope(); @@ -913,13 +937,13 @@ protected List bindSlotByThisScope(UnboundSlot unboundSlot return bindSlotByScope(unboundSlot, getScope()); } - protected List bindExactSlotsByThisScope(UnboundSlot unboundSlot, Scope scope) { - List candidates = bindSlotByScope(unboundSlot, scope); + protected List bindExactSlotsByThisScope(UnboundSlot unboundSlot, Scope scope) { + List candidates = bindSlotByScope(unboundSlot, scope); if (candidates.size() == 1) { return candidates; } - List extractSlots = Utils.filterImmutableList(candidates, bound -> - unboundSlot.getNameParts().size() == bound.getQualifier().size() + 1 + List 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 @@ private List addSqlIndexInfo(List slots, Optional bindSlotByScope(UnboundSlot unboundSlot, Scope scope) { + public List bindSlotByScope(UnboundSlot unboundSlot, Scope scope) { List nameParts = unboundSlot.getNameParts(); Optional> idxInSql = unboundSlot.getIndexInSqlString(); int namePartSize = nameParts.size(); switch (namePartSize) { // column case 1: { - return addSqlIndexInfo(bindSingleSlotByName(nameParts.get(0), scope), idxInSql); + return (List) bindExpressionByColumn(unboundSlot, nameParts, idxInSql, scope); } // table.column case 2: { - return addSqlIndexInfo(bindSingleSlotByTable(nameParts.get(0), nameParts.get(1), scope), idxInSql); + return (List) 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) 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) bindExpressionByCatalogDbTableColumn(unboundSlot, nameParts, idxInSql, scope); + } + } + } + + private List bindExpressionByCatalogDbTableColumn( + UnboundSlot unboundSlot, List nameParts, Optional> idxInSql, Scope scope) { + List 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 = bindNestedFields( + unboundSlot, slots.get(0), nameParts.subList(4, nameParts.size()) + ); + if (!expression.isPresent()) { + return slots; + } + return ImmutableList.of(expression.get()); + } + + private List bindExpressionByDbTableColumn( + UnboundSlot unboundSlot, List nameParts, Optional> idxInSql, Scope scope) { + List 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 = bindNestedFields( + unboundSlot, slots.get(0), nameParts.subList(3, nameParts.size()) + ); + if (!expression.isPresent()) { + return slots; + } + return ImmutableList.of(expression.get()); + } + + private List bindExpressionByTableColumn( + UnboundSlot unboundSlot, List nameParts, Optional> idxInSql, Scope scope) { + List 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 = bindNestedFields( + unboundSlot, slots.get(0), nameParts.subList(2, nameParts.size()) + ); + if (!expression.isPresent()) { + return slots; + } + return ImmutableList.of(expression.get()); + } + + private List bindExpressionByColumn( + UnboundSlot unboundSlot, List nameParts, Optional> idxInSql, Scope scope) { + List slots = addSqlIndexInfo(bindSingleSlotByName(nameParts.get(0), scope), idxInSql); + if (slots.size() != 1) { + return slots; + } + if (nameParts.size() == 1) { + return slots; + } + Optional expression = bindNestedFields( + unboundSlot, slots.get(0), nameParts.subList(1, nameParts.size()) + ); + if (!expression.isPresent()) { + return slots; + } + return ImmutableList.of(expression.get()); + } + + private Optional bindNestedFields(UnboundSlot unboundSlot, Slot slot, List 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 00000000000000..41f6048cf30c4b --- /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 accept(ExpressionVisitor 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 1b508a9aaa698a..981e0e964ce580 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.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 R visitUnboundStar(UnboundStar unboundStar, C context) { 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/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 00000000000000..4731a4c988b513 --- /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>> 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>> 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 00000000000000..30c123e3c37e49 --- /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, + m map, + s struct, + 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>>, + 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 From 47df24aa52a30fe5131c6d227347d5c06c920703 Mon Sep 17 00:00:00 2001 From: 924060929 Date: Mon, 1 Dec 2025 11:38:29 +0800 Subject: [PATCH 2/2] fix --- .../main/java/org/apache/doris/nereids/types/StructType.java | 4 ++++ 1 file changed, 4 insertions(+) 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 ffbff7c61e14d5..fb8557923aa36b 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 Map getNameToFields() { 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()));