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 994c92f736a99e..8d6a5865661a1a 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 @@ -3174,34 +3174,38 @@ private Expression processUnboundFunction(ParserRuleContext ctx, String dbName, WindowSpecContext windowContext, IdentifierContext hintContext) { List unboundStars = ExpressionUtils.collectAll(params, UnboundStar.class::isInstance); if (!unboundStars.isEmpty()) { - if (dbName == null && functionName.equalsIgnoreCase("count")) { + if (dbName != null + || (!functionName.equalsIgnoreCase("count") + && !functionName.equalsIgnoreCase("json_object"))) { + throw new ParseException("'*' can only be used in conjunction with" + + " COUNT or JSON_OBJECT: " + functionName, ctx); + } + if (functionName.equalsIgnoreCase("count")) { if (unboundStars.size() > 1) { throw new ParseException( "'*' can only be used once in conjunction with COUNT: " + functionName, ctx); } if (!unboundStars.get(0).getQualifier().isEmpty()) { - throw new ParseException("'*' can not has qualifier: " + unboundStars.size(), ctx); + throw new ParseException("'*' can not has qualifier with COUNT: " + unboundStars.size(), ctx); } if (windowContext != null) { return withWindowSpec(windowContext, new Count()); } return new Count(); } - throw new ParseException("'*' can only be used in conjunction with COUNT: " + functionName, ctx); - } else { - boolean isSkew = hintContext != null && hintContext.getText().equalsIgnoreCase("skew"); - UnboundFunction function = new UnboundFunction(dbName, functionName, isDistinct, params, isSkew); - if (windowContext != null) { - if (isDistinct - && !("count".equalsIgnoreCase(functionName)) - && !("sum".equalsIgnoreCase(functionName)) - && !("group_concat".equalsIgnoreCase(functionName))) { - throw new ParseException("DISTINCT not allowed in analytic function: " + functionName, ctx); - } - return withWindowSpec(windowContext, function); + } + boolean isSkew = hintContext != null && hintContext.getText().equalsIgnoreCase("skew"); + UnboundFunction function = new UnboundFunction(dbName, functionName, isDistinct, params, isSkew); + if (windowContext != null) { + if (isDistinct + && !("count".equalsIgnoreCase(functionName)) + && !("sum".equalsIgnoreCase(functionName)) + && !("group_concat".equalsIgnoreCase(functionName))) { + throw new ParseException("DISTINCT not allowed in analytic function: " + functionName, ctx); } - return function; + return withWindowSpec(windowContext, function); } + return function; } /** 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 2b5b9f62fe913d..b61e7923387fc6 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 @@ -403,19 +403,33 @@ UnboundFunction preProcessUnboundFunction(UnboundFunction unboundFunction, Expre return unboundFunction; } + List constructUnboundFunctionArguments(UnboundFunction unboundFunction) { + ImmutableList.Builder argumentsBuilder + = ImmutableList.builderWithExpectedSize(unboundFunction.arity() + 1); + if (unboundFunction.isDistinct()) { + argumentsBuilder.add(unboundFunction.isDistinct()); + } + for (Expression argument : unboundFunction.getArguments()) { + if (argument instanceof BoundStar) { + BoundStar boundStar = (BoundStar) argument; + for (Slot slot : boundStar.getSlots()) { + argumentsBuilder.add(new StringLiteral(slot.getName())); + argumentsBuilder.add(slot); + } + } else { + argumentsBuilder.add(argument); + } + } + return argumentsBuilder.build(); + } + @Override public Expression visitUnboundFunction(UnboundFunction unboundFunction, ExpressionRewriteContext context) { unboundFunction = preProcessUnboundFunction(unboundFunction, context); // bind function FunctionRegistry functionRegistry = Env.getCurrentEnv().getFunctionRegistry(); - List arguments = unboundFunction.isDistinct() - ? ImmutableList.builderWithExpectedSize(unboundFunction.arity() + 1) - .add(unboundFunction.isDistinct()) - .addAll(unboundFunction.getArguments()) - .build() - : (List) unboundFunction.getArguments(); - + List arguments = constructUnboundFunctionArguments(unboundFunction); String dbName = unboundFunction.getDbName(); if (StringUtils.isEmpty(dbName)) { // we will change arithmetic function like add(), subtract(), bitnot() diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/UnboundFunctionWithUnboundStarTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/UnboundFunctionWithUnboundStarTest.java new file mode 100644 index 00000000000000..aba40ccff1708a --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/UnboundFunctionWithUnboundStarTest.java @@ -0,0 +1,106 @@ +// 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.parser; + +import org.apache.doris.nereids.analyzer.UnboundFunction; +import org.apache.doris.nereids.analyzer.UnboundStar; +import org.apache.doris.nereids.exceptions.ParseException; +import org.apache.doris.nereids.trees.expressions.Expression; +import org.apache.doris.nereids.trees.expressions.functions.agg.Count; + +import com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class UnboundFunctionWithUnboundStarTest { + + @Test + public void testCount() { + NereidsParser parser = new NereidsParser(); + Expression result = parser.parseExpression("COUNT(*)"); + Assertions.assertEquals(new Count(), result); + } + + @Test + public void testJsonObject() { + NereidsParser parser = new NereidsParser(); + Expression result = parser.parseExpression("JSON_OBJECT(*)"); + Assertions.assertInstanceOf(UnboundFunction.class, result); + UnboundFunction unboundFunction = (UnboundFunction) result; + Assertions.assertEquals("JSON_OBJECT", unboundFunction.getName()); + Assertions.assertEquals(1, unboundFunction.arity()); + Assertions.assertEquals(new UnboundStar(ImmutableList.of()), unboundFunction.child(0)); + } + + @Test + public void testOtherFunctionName() { + NereidsParser parser = new NereidsParser(); + Exception exception = Assertions.assertThrowsExactly(ParseException.class, + () -> parser.parseExpression("OTHER(*)")); + Assertions.assertTrue(exception.getMessage() + .contains("'*' can only be used in conjunction with COUNT or JSON_OBJECT: "), + exception.getMessage()); + } + + @Test + public void testFunctionNameWithDbName() { + NereidsParser parser = new NereidsParser(); + Exception exception = Assertions.assertThrowsExactly(ParseException.class, + () -> parser.parseExpression("db.COUNT(*)")); + Assertions.assertTrue(exception.getMessage() + .contains("'*' can only be used in conjunction with COUNT or JSON_OBJECT: "), + exception.getMessage()); + } + + @Test + public void testStarWithQualifier() { + NereidsParser parser = new NereidsParser(); + Exception exception = Assertions.assertThrowsExactly(ParseException.class, + () -> parser.parseExpression("COUNT(t1.*)")); + Assertions.assertTrue(exception.getMessage() + .contains("'*' can not has qualifier with COUNT: "), + exception.getMessage()); + + Expression result = parser.parseExpression("JSON_OBJECT(t1.*)"); + Assertions.assertInstanceOf(UnboundFunction.class, result); + UnboundFunction unboundFunction = (UnboundFunction) result; + Assertions.assertEquals("JSON_OBJECT", unboundFunction.getName()); + Assertions.assertEquals(1, unboundFunction.arity()); + Assertions.assertEquals(new UnboundStar(ImmutableList.of("t1")), unboundFunction.child(0)); + } + + @Test + public void testMoreThanOneStar() { + NereidsParser parser = new NereidsParser(); + Exception exception = Assertions.assertThrowsExactly(ParseException.class, + () -> parser.parseExpression("COUNT(*, *)")); + Assertions.assertTrue(exception.getMessage() + .contains("'*' can only be used once in conjunction with COUNT: "), + exception.getMessage()); + + Expression result = parser.parseExpression("JSON_OBJECT(t2.*, t1.*, *)"); + Assertions.assertInstanceOf(UnboundFunction.class, result); + UnboundFunction unboundFunction = (UnboundFunction) result; + Assertions.assertEquals("JSON_OBJECT", unboundFunction.getName()); + Assertions.assertEquals(3, unboundFunction.arity()); + Assertions.assertEquals(new UnboundStar(ImmutableList.of("t2")), unboundFunction.child(0)); + Assertions.assertEquals(new UnboundStar(ImmutableList.of("t1")), unboundFunction.child(1)); + Assertions.assertEquals(new UnboundStar(ImmutableList.of()), unboundFunction.child(2)); + } + +} 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 fcd5a58cf9f356..052325cac35763 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 @@ -21,15 +21,21 @@ import org.apache.doris.nereids.analyzer.UnboundFunction; import org.apache.doris.nereids.analyzer.UnboundSlot; import org.apache.doris.nereids.exceptions.AnalysisException; +import org.apache.doris.nereids.trees.expressions.BoundStar; +import org.apache.doris.nereids.trees.expressions.ExprId; import org.apache.doris.nereids.trees.expressions.Expression; import org.apache.doris.nereids.trees.expressions.SlotReference; import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal; +import org.apache.doris.nereids.trees.expressions.literal.StringLiteral; import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral; +import org.apache.doris.nereids.types.BigIntType; import com.google.common.collect.ImmutableList; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.util.List; + public class ExpressionAnalyzerTest { @Test @@ -60,4 +66,34 @@ void testPreProcessUnboundFunctionForThreeArgsDataTimeFunction() { () -> analyzer.preProcessUnboundFunction(unboundFunction, null), " Unknown column 'YEAR' in 'table list"); } + + @Test + public void testConstructUnboundFunctionArguments() { + ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, new Scope(ImmutableList.of()), + null, true, true); + BoundStar boundStar1 = new BoundStar(ImmutableList.of( + new SlotReference(new ExprId(1), "c1", BigIntType.INSTANCE, true, ImmutableList.of()), + new SlotReference(new ExprId(2), "c2", BigIntType.INSTANCE, true, ImmutableList.of()) + )); + BoundStar boundStar2 = new BoundStar(ImmutableList.of( + new SlotReference(new ExprId(3), "c3", BigIntType.INSTANCE, true, ImmutableList.of()), + new SlotReference(new ExprId(4), "c4", BigIntType.INSTANCE, true, ImmutableList.of()) + )); + SlotReference slotReference = new SlotReference(new ExprId(5), "c5", BigIntType.INSTANCE, true, ImmutableList.of()); + UnboundFunction unboundFunction = new UnboundFunction("json_object", true, + ImmutableList.of(boundStar2, slotReference, boundStar1)); + List result = analyzer.constructUnboundFunctionArguments(unboundFunction); + List expectedResult = ImmutableList.of(true, + new StringLiteral("c3"), + new SlotReference(new ExprId(3), "c3", BigIntType.INSTANCE, true, ImmutableList.of()), + new StringLiteral("c4"), + new SlotReference(new ExprId(4), "c4", BigIntType.INSTANCE, true, ImmutableList.of()), + new SlotReference(new ExprId(5), "c5", BigIntType.INSTANCE, true, ImmutableList.of()), + new StringLiteral("c1"), + new SlotReference(new ExprId(1), "c1", BigIntType.INSTANCE, true, ImmutableList.of()), + new StringLiteral("c2"), + new SlotReference(new ExprId(2), "c2", BigIntType.INSTANCE, true, ImmutableList.of()) + ); + Assertions.assertEquals(expectedResult, result); + } } diff --git a/regression-test/data/nereids_p0/sql_functions/json_function/test_query_json_object.out b/regression-test/data/nereids_p0/sql_functions/json_function/test_query_json_object.out index 7b4f0f080be38d..b5c3a9d9749cd7 100644 --- a/regression-test/data/nereids_p0/sql_functions/json_function/test_query_json_object.out +++ b/regression-test/data/nereids_p0/sql_functions/json_function/test_query_json_object.out @@ -1,8 +1,22 @@ -- This file is automatically generated. You should know what you did if you want to edit this --- !sql -- +-- !test_normal -- {"k0":1,"k1":null,"k2":null,"k3":null,"k4":null,"k5":null,"k6":"k6"} {"k0":2,"k1":1,"k2":null,"k3":null,"k4":null,"k5":null,"k6":"k6"} {"k0":3,"k1":null,"k2":true,"k3":null,"k4":null,"k5":null,"k6":"k6"} {"k0":4,"k1":null,"k2":null,"k3":"test","k4":"2022-01-01 11:11:11","k5":null,"k6":"k6"} {"k0":5,"k1":1,"k2":true,"k3":"test","k4":"2022-01-01 11:11:11","k5":null,"k6":"k6"} +-- !test_star -- +{"k0":1,"k1":null,"k2":null,"k3":null,"k4":null} +{"k0":2,"k1":1,"k2":null,"k3":null,"k4":null} +{"k0":3,"k1":null,"k2":true,"k3":null,"k4":null} +{"k0":4,"k1":null,"k2":null,"k3":"test","k4":"2022-01-01 11:11:11"} +{"k0":5,"k1":1,"k2":true,"k3":"test","k4":"2022-01-01 11:11:11"} + +-- !test_star_with_qualifer -- +{"k0":1,"k1":null,"k2":null,"k3":null,"k4":null} +{"k0":2,"k1":1,"k2":null,"k3":null,"k4":null} +{"k0":3,"k1":null,"k2":true,"k3":null,"k4":null} +{"k0":4,"k1":null,"k2":null,"k3":"test","k4":"2022-01-01 11:11:11"} +{"k0":5,"k1":1,"k2":true,"k3":"test","k4":"2022-01-01 11:11:11"} + diff --git a/regression-test/suites/nereids_p0/sql_functions/json_function/test_query_json_object.groovy b/regression-test/suites/nereids_p0/sql_functions/json_function/test_query_json_object.groovy index 4797921d465549..2de65d670a7ebf 100644 --- a/regression-test/suites/nereids_p0/sql_functions/json_function/test_query_json_object.groovy +++ b/regression-test/suites/nereids_p0/sql_functions/json_function/test_query_json_object.groovy @@ -42,6 +42,9 @@ suite("test_query_json_object", "query") { sql "insert into ${tableName} values(3,null,true,null,null);" sql "insert into ${tableName} values(4,null,null,'test','2022-01-01 11:11:11');" sql "insert into ${tableName} values(5,1,true,'test','2022-01-01 11:11:11');" - order_qt_sql "select json_object('k0',k0,'k1',k1,'k2',k2,'k3',k3,'k4',cast(k4 as string),'k5', null,'k6','k6') from ${tableName};" + sql "sync" + order_qt_test_normal "select json_object('k0',k0,'k1',k1,'k2',k2,'k3',k3,'k4',cast(k4 as string),'k5', null,'k6','k6') from ${tableName};" + order_qt_test_star "select json_object(*) from ${tableName};" + order_qt_test_star_with_qualifer "select json_object(${tableName}.*) from ${tableName};" sql "DROP TABLE ${tableName};" }