diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBTableAggregationIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBTableAggregationIT.java index 460fe83a397d..13380947af79 100644 --- a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBTableAggregationIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBTableAggregationIT.java @@ -5477,6 +5477,75 @@ public void singleInputDistinctAggregationTest() { DATABASE_NAME); } + @Test + public void selectAliasInGroupByAndOrderByTest() { + String[] expectedHeader = new String[] {"hour_time", "_col1"}; + String[] retArray = + new String[] { + "2024-09-24T06:15:30.000Z,1,", + "2024-09-24T06:15:35.000Z,1,", + "2024-09-24T06:15:40.000Z,1,", + "2024-09-24T06:15:50.000Z,1,", + "2024-09-24T06:15:55.000Z,1,", + }; + tableResultSetEqualTest( + "select date_bin(5s, time) as hour_time, count(*) from table1 where device_id = 'd01' group by hour_time order by hour_time", + expectedHeader, + retArray, + DATABASE_NAME); + + expectedHeader = new String[] {"province", "input_province"}; + retArray = new String[] {"d01,shanghai,", "d01,shanghai,"}; + tableResultSetEqualTest( + "select device_id as province, province as input_province from table1 where device_id in ('d01', 'd09') order by province limit 2", + expectedHeader, + retArray, + DATABASE_NAME); + + expectedHeader = new String[] {"x"}; + retArray = new String[] {"30,", "40,", "55,"}; + tableResultSetEqualTest( + "select distinct s1 as x from table1 where device_id = 'd01' and s1 is not null order by x", + expectedHeader, + retArray, + DATABASE_NAME); + tableResultSetEqualTest( + "select s1 as x from table1 where device_id = 'd01' and s1 is not null order by x + 1", + expectedHeader, + retArray, + DATABASE_NAME); + + expectedHeader = new String[] {"rn"}; + retArray = new String[] {"1,", "2,", "3,", "4,", "5,"}; + tableResultSetEqualTest( + "select row_number() over (order by time) as rn from table1 where device_id = 'd01' order by rn", + expectedHeader, + retArray, + DATABASE_NAME); + + tableAssertTestFail( + "select s1 as x, count(*) from table1 where device_id = 'd01' and s1 is not null group by rollup(x) order by x nulls last", + "Only support one groupingSet now", + DATABASE_NAME); + tableAssertTestFail( + "select s1 as x, count(*) from table1 where device_id = 'd01' and s1 is not null group by cube(x) order by x nulls last", + "Only support one groupingSet now", + DATABASE_NAME); + + expectedHeader = new String[] {"x", "_col1"}; + retArray = new String[] {"30,1,", "40,1,", "55,1,"}; + tableResultSetEqualTest( + "select s1 as x, count(*) from table1 where device_id = 'd01' and s1 is not null group by grouping sets ((x)) order by x", + expectedHeader, + retArray, + DATABASE_NAME); + + tableAssertTestFail( + "select s1 as x, s2 as x, count(*) from table1 group by rollup(x)", + "Column alias 'x' is ambiguous", + DATABASE_NAME); + } + @Test public void exceptionTest2() { tableAssertTestFail( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java index d0779ee24a5b..4a09e0deb0d6 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java @@ -367,6 +367,68 @@ private enum UpdateKind { MERGE, } + private static final class SelectAnalysis { + private final List outputExpressions; + private final List aliases; + + private SelectAnalysis(List outputExpressions, List aliases) { + this.outputExpressions = + ImmutableList.copyOf(requireNonNull(outputExpressions, "outputExpressions is null")); + this.aliases = ImmutableList.copyOf(requireNonNull(aliases, "aliases is null")); + } + + private List getOutputExpressions() { + return outputExpressions; + } + + private List getAliases() { + return aliases; + } + } + + private static final class SelectAlias { + private final String canonicalName; + private final int position; + + private SelectAlias(String canonicalName, int position) { + this.canonicalName = requireNonNull(canonicalName, "canonicalName is null"); + this.position = position; + } + + private String getCanonicalName() { + return canonicalName; + } + + private int getPosition() { + return position; + } + } + + private static boolean resolvesToInputColumn(Scope scope, Identifier identifier) { + return scope + .tryResolveField(identifier, QualifiedName.of(identifier.getValue())) + .filter(ResolvedField::isLocal) + .isPresent(); + } + + private static Optional resolveSelectAlias( + Identifier identifier, List aliases) { + List matches = + aliases.stream() + .filter(alias -> alias.getCanonicalName().equals(identifier.getCanonicalValue())) + .collect(toImmutableList()); + if (matches.size() > 1) { + throw new SemanticException( + String.format( + "Column alias '%s' is ambiguous at positions %s", + identifier.getValue(), + matches.stream() + .map(alias -> Integer.toString(alias.getPosition())) + .collect(Collectors.joining(", ")))); + } + return matches.stream().findFirst(); + } + /** * Visitor context represents local query scope (if exists). The invariant is that the local query * scopes hierarchy should always have outer query scope (if provided) as ancestor. @@ -901,7 +963,8 @@ public Scope visitQuery(Query node, Optional context) { List orderByExpressions = emptyList(); if (node.getOrderBy().isPresent()) { orderByExpressions = - analyzeOrderBy(node, getSortItemsFromOrderBy(node.getOrderBy()), queryBodyScope); + analyzeOrderBy( + node, getSortItemsFromOrderBy(node.getOrderBy()), queryBodyScope, emptyList()); if ((queryBodyScope.getOuterQueryParent().isPresent() || !isTopLevel) && !node.getLimit().isPresent() @@ -1193,9 +1256,10 @@ public Scope visitQuerySpecification(QuerySpecification node, Optional sc node.getWhere().ifPresent(where -> analyzeWhere(node, sourceScope, where)); - List outputExpressions = analyzeSelect(node, sourceScope); + SelectAnalysis selectAnalysis = analyzeSelect(node, sourceScope); + List outputExpressions = selectAnalysis.getOutputExpressions(); Analysis.GroupingSetAnalysis groupByAnalysis = - analyzeGroupBy(node, sourceScope, outputExpressions); + analyzeGroupBy(node, sourceScope, outputExpressions, selectAnalysis.getAliases()); analyzeHaving(node, sourceScope); Scope outputScope = computeAndAssignOutputScope(node, scope, sourceScope); @@ -1213,7 +1277,9 @@ public Scope visitQuerySpecification(QuerySpecification node, Optional sc OrderBy orderBy = node.getOrderBy().get(); orderByScope = Optional.of(computeAndAssignOrderByScope(orderBy, sourceScope, outputScope)); - orderByExpressions = analyzeOrderBy(node, orderBy.getSortItems(), orderByScope.get()); + orderByExpressions = + analyzeOrderBy( + node, orderBy.getSortItems(), orderByScope.get(), selectAnalysis.getAliases()); if ((sourceScope.getOuterQueryParent().isPresent() || !isTopLevel) && !node.getLimit().isPresent() @@ -1569,15 +1635,18 @@ private void analyzeWhere(Node node, Scope scope, Expression predicate) { analysis.setWhere(node, predicate); } - private List analyzeSelect(QuerySpecification node, Scope scope) { + private SelectAnalysis analyzeSelect(QuerySpecification node, Scope scope) { ImmutableList.Builder outputExpressionBuilder = ImmutableList.builder(); ImmutableList.Builder selectExpressionBuilder = ImmutableList.builder(); + ImmutableList.Builder selectAliasBuilder = ImmutableList.builder(); + int outputPosition = 1; for (SelectItem item : node.getSelect().getSelectItems()) { if (item instanceof AllColumns) { - analyzeSelectAllColumns( - (AllColumns) item, node, scope, outputExpressionBuilder, selectExpressionBuilder); + outputPosition += + analyzeSelectAllColumns( + (AllColumns) item, node, scope, outputExpressionBuilder, selectExpressionBuilder); } else if (item instanceof SingleColumn) { SingleColumn singleColumn = (SingleColumn) item; Expression selectExpression = singleColumn.getExpression(); @@ -1594,10 +1663,16 @@ private List analyzeSelect(QuerySpecification node, Scope scope) { for (Expression expression : expandedExpressions) { analyzeSelectSingleColumn( expression, node, scope, outputExpressionBuilder, selectExpressionBuilder); + outputPosition++; } } else { analyzeSelectSingleColumn( selectExpression, node, scope, outputExpressionBuilder, selectExpressionBuilder); + if (singleColumn.getAlias().isPresent()) { + Identifier alias = singleColumn.getAlias().get(); + selectAliasBuilder.add(new SelectAlias(alias.getCanonicalValue(), outputPosition)); + } + outputPosition++; } } else { throw new IllegalArgumentException( @@ -1610,7 +1685,7 @@ private List analyzeSelect(QuerySpecification node, Scope scope) { analysis.setContainsSelectDistinct(); } - return outputExpressionBuilder.build(); + return new SelectAnalysis(outputExpressionBuilder.build(), selectAliasBuilder.build()); } /** @@ -2412,7 +2487,7 @@ public List visitWhenClause(WhenClause node, Scope context) { } } - private void analyzeSelectAllColumns( + private int analyzeSelectAllColumns( AllColumns allColumns, QuerySpecification node, Scope scope, @@ -2458,7 +2533,7 @@ private void analyzeSelectAllColumns( () -> new NoSuchElementException( DataNodeQueryMessages.NO_VALUE_PRESENT))); - analyzeAllColumnsFromTable( + return analyzeAllColumnsFromTable( fields, allColumns, node, @@ -2467,7 +2542,6 @@ private void analyzeSelectAllColumns( selectExpressionBuilder, relationType, local); - return; } } // identifierChainBasis.get().getBasisType == FIELD or target expression isn't a @@ -2497,7 +2571,7 @@ private void analyzeSelectAllColumns( DataNodeQueryMessages.SELECT_NOT_ALLOWED_FROM_RELATION_THAT_HAS_NO); } - analyzeAllColumnsFromTable( + return analyzeAllColumnsFromTable( fields, allColumns, node, @@ -2550,7 +2624,7 @@ private List filterInaccessibleFields(List fields) { return fields.stream().filter(accessibleFields.build()::contains).collect(toImmutableList()); } - private void analyzeAllColumnsFromTable( + private int analyzeAllColumnsFromTable( List fields, AllColumns allColumns, QuerySpecification node, @@ -2611,6 +2685,7 @@ private void analyzeAllColumnsFromTable( } } analysis.setSelectAllResultFields(allColumns, itemOutputFieldBuilder.build()); + return fields.size(); } // private void analyzeAllFieldsFromRowTypeExpression( @@ -2681,7 +2756,10 @@ private void analyzeSelectSingleColumn( } private Analysis.GroupingSetAnalysis analyzeGroupBy( - QuerySpecification node, Scope scope, List outputExpressions) { + QuerySpecification node, + Scope scope, + List outputExpressions, + List selectAliases) { if (node.getGroupBy().isPresent()) { ImmutableList.Builder>> cubes = ImmutableList.builder(); ImmutableList.Builder>> rollups = ImmutableList.builder(); @@ -2706,6 +2784,7 @@ private Analysis.GroupingSetAnalysis analyzeGroupBy( column = outputExpressions.get(toIntExact(ordinal - 1)); verifyNoAggregateWindowOrGroupingFunctions(column, "GROUP BY clause"); } else { + column = resolveGroupBySelectAlias(column, scope, outputExpressions, selectAliases); verifyNoAggregateWindowOrGroupingFunctions(column, "GROUP BY clause"); analyzeExpression(column, scope); } @@ -2732,13 +2811,18 @@ private Analysis.GroupingSetAnalysis analyzeGroupBy( } } else if (groupingElement instanceof GroupingSets) { GroupingSets element = (GroupingSets) groupingElement; + Map, Expression> resolvedGroupingColumns = new HashMap<>(); for (Expression column : groupingElement.getExpressions()) { + Expression originalColumn = column; + column = resolveGroupBySelectAlias(column, scope, outputExpressions, selectAliases); + verifyNoAggregateWindowOrGroupingFunctions(column, "GROUP BY clause"); analyzeExpression(column, scope); if (!analysis.getColumnReferences().contains(NodeRef.of(column))) { throw new SemanticException( String.format("GROUP BY expression must be a column reference: %s", column)); } + resolvedGroupingColumns.put(NodeRef.of(originalColumn), column); groupingExpressions.add(column); } @@ -2747,6 +2831,11 @@ private Analysis.GroupingSetAnalysis analyzeGroupBy( .map( set -> set.stream() + .map( + expression -> + requireNonNull( + resolvedGroupingColumns.get(NodeRef.of(expression)), + "resolved grouping expression is null")) .map(NodeRef::of) .map(analysis.getColumnReferenceFields()::get) .map(ResolvedField::getFieldId) @@ -2807,6 +2896,25 @@ private Analysis.GroupingSetAnalysis analyzeGroupBy( return result; } + private Expression resolveGroupBySelectAlias( + Expression expression, + Scope scope, + List outputExpressions, + List selectAliases) { + if (!(expression instanceof Identifier)) { + return expression; + } + + Identifier identifier = (Identifier) expression; + if (resolvesToInputColumn(scope, identifier)) { + return expression; + } + + return resolveSelectAlias(identifier, selectAliases) + .map(alias -> outputExpressions.get(alias.getPosition() - 1)) + .orElse(expression); + } + private boolean isDateBinGapFill(Expression column) { return column instanceof FunctionCall && DATE_BIN @@ -4102,7 +4210,7 @@ private FieldReference getFieldReferenceForFillGroup( } private List analyzeOrderBy( - Node node, List sortItems, Scope orderByScope) { + Node node, List sortItems, Scope orderByScope, List selectAliases) { ImmutableList.Builder orderByFieldsBuilder = ImmutableList.builder(); for (SortItem item : sortItems) { @@ -4118,6 +4226,11 @@ private List analyzeOrderBy( } expression = new FieldReference(toIntExact(ordinal - 1)); + } else { + Optional selectAlias = resolveOrderBySelectAlias(expression, selectAliases); + if (selectAlias.isPresent()) { + expression = new FieldReference(selectAlias.get().getPosition() - 1); + } } ExpressionAnalysis expressionAnalysis = @@ -4148,6 +4261,15 @@ private List analyzeOrderBy( return orderByFieldsBuilder.build(); } + private Optional resolveOrderBySelectAlias( + Expression expression, List selectAliases) { + if (!(expression instanceof Identifier)) { + return Optional.empty(); + } + + return resolveSelectAlias((Identifier) expression, selectAliases); + } + private void analyzeOffset(Offset node, Scope scope) { long rowCount; if (node.getRowCount() instanceof LongLiteral) { @@ -5452,7 +5574,11 @@ private static boolean hasScopeAsLocalParent(Scope root, Scope parent) { } static void verifyNoAggregateWindowOrGroupingFunctions(Expression predicate, String clause) { - List aggregates = extractAggregateFunctions(ImmutableList.of(predicate)); + List aggregates = + ImmutableList.builder() + .addAll(extractAggregateFunctions(ImmutableList.of(predicate))) + .addAll(extractWindowFunctions(ImmutableList.of(predicate))) + .build(); if (!aggregates.isEmpty()) { throw new SemanticException( diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/SelectAliasReuseTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/SelectAliasReuseTest.java new file mode 100644 index 000000000000..bc14e65324d9 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/SelectAliasReuseTest.java @@ -0,0 +1,347 @@ +/* + * 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.iotdb.db.queryengine.plan.relational.analyzer; + +import org.apache.iotdb.commons.queryengine.common.SessionInfo; +import org.apache.iotdb.commons.queryengine.common.SqlDialect; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.ArithmeticBinaryExpression; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.ExistsPredicate; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Expression; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.FieldReference; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.FunctionCall; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Identifier; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.OrderBy; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Query; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.QuerySpecification; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.SubqueryExpression; +import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester; +import org.apache.iotdb.db.queryengine.plan.relational.sql.parser.SqlParser; + +import org.junit.Test; + +import java.time.ZoneId; +import java.util.List; + +import static org.apache.iotdb.db.queryengine.plan.relational.analyzer.AnalyzerTest.analyzeStatement; +import static org.apache.iotdb.db.queryengine.plan.relational.analyzer.TestUtils.QUERY_CONTEXT; +import static org.apache.iotdb.db.queryengine.plan.relational.analyzer.TestUtils.TEST_MATADATA; +import static org.apache.iotdb.db.queryengine.plan.relational.analyzer.TestUtils.assertAnalyzeSemanticException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class SelectAliasReuseTest { + + @Test + public void groupByAliasUsesExpressionAndOrderByAliasUsesOutputField() { + String sql = + "SELECT date_bin(1h, time) AS hour_time, AVG(s1) AS avg_s1 " + + "FROM table1 GROUP BY hour_time ORDER BY hour_time"; + + AnalyzedQuery analyzedQuery = analyze(sql); + assertDateBin( + analyzedQuery.analysis.getGroupingSets(analyzedQuery.query).getOriginalExpressions()); + assertFieldReference( + analyzedQuery.analysis.getOrderByExpressions(analyzedQuery.query).get(0), 0); + + new PlanTester().createPlan(sql); + } + + @Test + public void groupByInputColumnTakesPrecedenceOverAlias() { + String sql = "SELECT x + 1 AS x, COUNT(s1) FROM table_with_x GROUP BY x"; + + AnalyzedQuery analyzedQuery = analyze(sql); + assertIdentifier( + analyzedQuery.analysis.getGroupingSets(analyzedQuery.query).getOriginalExpressions().get(0), + "x"); + + new PlanTester().createPlan(sql); + } + + @Test + public void groupByAliasIsNotBlockedByOuterScopeColumn() { + String sql = + "SELECT x FROM table_with_x WHERE EXISTS (" + + "SELECT s1 AS x, COUNT(*) FROM table1 " + + "WHERE table_with_x.s1 = table1.s1 GROUP BY x)"; + + AnalyzedQuery analyzedQuery = analyze(sql); + QuerySpecification innerQuery = getExistsSubquery(analyzedQuery.query); + assertIdentifier( + analyzedQuery.analysis.getGroupingSets(innerQuery).getOriginalExpressions().get(0), "s1"); + + new PlanTester().createPlan(sql); + } + + @Test + public void groupingSetsAliasesUseResolvedExpressions() { + AnalyzedQuery rollup = analyze("SELECT s1 AS x, COUNT(*) FROM table1 GROUP BY ROLLUP(x)"); + Analysis.GroupingSetAnalysis rollupAnalysis = rollup.analysis.getGroupingSets(rollup.query); + assertSingleOriginalIdentifier(rollupAnalysis, "s1"); + assertEquals(1, rollupAnalysis.getRollups().size()); + + AnalyzedQuery cube = analyze("SELECT s1 AS x, COUNT(*) FROM table1 GROUP BY CUBE(x)"); + Analysis.GroupingSetAnalysis cubeAnalysis = cube.analysis.getGroupingSets(cube.query); + assertSingleOriginalIdentifier(cubeAnalysis, "s1"); + assertEquals(1, cubeAnalysis.getCubes().size()); + + AnalyzedQuery explicit = + analyze("SELECT s1 AS x, COUNT(*) FROM table1 GROUP BY GROUPING SETS ((x))"); + Analysis.GroupingSetAnalysis explicitAnalysis = + explicit.analysis.getGroupingSets(explicit.query); + assertSingleOriginalIdentifier(explicitAnalysis, "s1"); + assertEquals(1, explicitAnalysis.getOrdinarySets().size()); + } + + @Test + public void groupingSetsInputColumnTakesPrecedenceOverAlias() { + String sql = "SELECT x + 1 AS x, COUNT(s1) FROM table_with_x GROUP BY ROLLUP(x)"; + + AnalyzedQuery analyzedQuery = analyze(sql); + assertSingleOriginalIdentifier( + analyzedQuery.analysis.getGroupingSets(analyzedQuery.query), "x"); + } + + @Test + public void groupingSetsAliasIsNotBlockedByOuterScopeColumn() { + String sql = + "SELECT x FROM table_with_x WHERE EXISTS (" + + "SELECT s1 AS x, COUNT(*) FROM table1 " + + "WHERE table_with_x.s1 = table1.s1 GROUP BY GROUPING SETS ((x)))"; + + AnalyzedQuery analyzedQuery = analyze(sql); + QuerySpecification innerQuery = getExistsSubquery(analyzedQuery.query); + assertSingleOriginalIdentifier(analyzedQuery.analysis.getGroupingSets(innerQuery), "s1"); + } + + @Test + public void orderByOutputAliasTakesPrecedenceOverInputColumn() { + String sql = "SELECT s1 AS x FROM table_with_x ORDER BY x"; + + AnalyzedQuery analyzedQuery = analyze(sql); + assertFieldReference( + analyzedQuery.analysis.getOrderByExpressions(analyzedQuery.query).get(0), 0); + + new PlanTester().createPlan(sql); + } + + @Test + public void orderByAliasWithoutInputColumn() { + String sql = "SELECT s1 AS x FROM table1 ORDER BY x"; + + AnalyzedQuery analyzedQuery = analyze(sql); + assertFieldReference( + analyzedQuery.analysis.getOrderByExpressions(analyzedQuery.query).get(0), 0); + + new PlanTester().createPlan(sql); + } + + @Test + public void selectDistinctOrderByAliasUsesOutputField() { + String sql = "SELECT DISTINCT s1 AS x FROM table1 ORDER BY x"; + + AnalyzedQuery analyzedQuery = analyze(sql); + assertFieldReference( + analyzedQuery.analysis.getOrderByExpressions(analyzedQuery.query).get(0), 0); + + new PlanTester().createPlan(sql); + } + + @Test + public void orderByExpressionUsesOrderByScopeWithoutAliasRewrite() { + String sql = "SELECT s1 AS x FROM table1 ORDER BY x + 1"; + + AnalyzedQuery analyzedQuery = analyze(sql); + Expression orderByExpression = + analyzedQuery.analysis.getOrderByExpressions(analyzedQuery.query).get(0); + assertTrue(orderByExpression instanceof ArithmeticBinaryExpression); + + new PlanTester().createPlan(sql); + } + + @Test + public void orderByWindowFunctionAliasReusesSelectOutputField() { + String sql = "SELECT row_number() OVER (ORDER BY s1) AS rn FROM table1 ORDER BY rn"; + + AnalyzedQuery analyzedQuery = analyze(sql); + OrderBy orderBy = analyzedQuery.query.getOrderBy().get(); + List selectWindowFunctions = + analyzedQuery.analysis.getWindowFunctions(analyzedQuery.query); + + assertEquals(1, selectWindowFunctions.size()); + assertEquals("row_number", selectWindowFunctions.get(0).getName().getSuffix()); + assertTrue(analyzedQuery.analysis.getOrderByWindowFunctions(orderBy).isEmpty()); + assertFieldReference( + analyzedQuery.analysis.getOrderByExpressions(analyzedQuery.query).get(0), 0); + + new PlanTester().createPlan(sql); + } + + @Test + public void orderByWindowFunctionAliasExpressionUsesOrderByScope() { + String sql = "SELECT row_number() OVER (ORDER BY s1) AS rn FROM table1 ORDER BY rn + 1"; + + AnalyzedQuery analyzedQuery = analyze(sql); + OrderBy orderBy = analyzedQuery.query.getOrderBy().get(); + List selectWindowFunctions = + analyzedQuery.analysis.getWindowFunctions(analyzedQuery.query); + Expression orderByExpression = + analyzedQuery.analysis.getOrderByExpressions(analyzedQuery.query).get(0); + + assertEquals(1, selectWindowFunctions.size()); + assertEquals("row_number", selectWindowFunctions.get(0).getName().getSuffix()); + assertTrue(analyzedQuery.analysis.getOrderByWindowFunctions(orderBy).isEmpty()); + assertTrue(orderByExpression instanceof ArithmeticBinaryExpression); + + new PlanTester().createPlan(sql); + } + + @Test + public void duplicateAliasesAreAmbiguous() { + assertAnalyzeSemanticException( + "SELECT s1 AS x, s2 AS x FROM table1 ORDER BY x", "Column alias 'x' is ambiguous"); + + assertAnalyzeSemanticException( + "SELECT s1 AS x, s2 AS x, COUNT(*) FROM table1 GROUP BY x", + "Column alias 'x' is ambiguous"); + + assertAnalyzeSemanticException( + "SELECT s1 AS x, s2 AS x, COUNT(*) FROM table1 GROUP BY ROLLUP(x)", + "Column alias 'x' is ambiguous"); + + assertAnalyzeSemanticException( + "SELECT s1 AS x, s2 AS x, COUNT(*) FROM table1 GROUP BY CUBE(x)", + "Column alias 'x' is ambiguous"); + + assertAnalyzeSemanticException( + "SELECT s1 AS x, s2 AS x, COUNT(*) FROM table1 GROUP BY GROUPING SETS ((x))", + "Column alias 'x' is ambiguous"); + } + + @Test + public void invalidAliasReferencesStillFail() { + assertAnalyzeSemanticException( + "SELECT AVG(s1) AS avg_s1 FROM table1 GROUP BY avg_s1", + "GROUP BY clause cannot contain aggregations"); + + assertAnalyzeSemanticException( + "SELECT s1 AS x, table1.x + 1 FROM table1", "Column 'table1.x' cannot be resolved"); + + assertAnalyzeSemanticException( + "SELECT s1 AS x FROM table1 ORDER BY table1.x", "Column 'table1.x' cannot be resolved"); + + assertAnalyzeSemanticException( + "SELECT s1 AS x, COUNT(*) FROM table1 GROUP BY table1.x", + "Column 'table1.x' cannot be resolved"); + + assertAnalyzeSemanticException( + "SELECT s1 AS x FROM table1 WHERE x > 1", "Column 'x' cannot be resolved"); + + assertAnalyzeSemanticException( + "SELECT AVG(s1) AS avg_s1 FROM table1 HAVING avg_s1 > 1", + "Column 'avg_s1' cannot be resolved"); + + assertAnalyzeSemanticException( + "SELECT s1 + 1 AS x, x * 2 AS y FROM table1", "Column 'x' cannot be resolved"); + } + + @Test + public void selectAliasDoesNotLeakIntoSubquery() { + assertAnalyzeSemanticException( + "SELECT s1 AS x, (SELECT x FROM table1) FROM table1", "Column 'x' cannot be resolved"); + } + + @Test + public void ordinalAndFullExpressionsStillWork() { + new PlanTester() + .createPlan("SELECT date_bin(1h, time), AVG(s1) FROM table1 GROUP BY 1 ORDER BY 1"); + + new PlanTester() + .createPlan( + "SELECT date_bin(1h, time), AVG(s1) FROM table1 " + + "GROUP BY date_bin(1h, time) ORDER BY AVG(s1)"); + } + + @Test + public void dateBinGapFillAliasUsesRewrittenGroupingKey() { + String sql = + "SELECT date_bin_gapfill(1h, time) AS hour_time, AVG(s1) " + + "FROM table1 GROUP BY hour_time ORDER BY hour_time"; + + AnalyzedQuery analyzedQuery = analyze(sql); + assertNotNull(analyzedQuery.analysis.getGapFill(analyzedQuery.query)); + assertDateBin( + analyzedQuery.analysis.getGroupingSets(analyzedQuery.query).getOriginalExpressions()); + assertFieldReference( + analyzedQuery.analysis.getOrderByExpressions(analyzedQuery.query).get(0), 0); + } + + private static AnalyzedQuery analyze(String sql) { + SqlParser sqlParser = new SqlParser(); + Statement statement = sqlParser.createStatement(sql, ZoneId.systemDefault(), null); + SessionInfo session = + new SessionInfo(0, "test", ZoneId.systemDefault(), "testdb", SqlDialect.TABLE); + Analysis analysis = + analyzeStatement(statement, TEST_MATADATA, QUERY_CONTEXT, sqlParser, session); + Query query = (Query) statement; + return new AnalyzedQuery(analysis, (QuerySpecification) query.getQueryBody()); + } + + private static void assertDateBin(List expressions) { + assertEquals(1, expressions.size()); + assertTrue(expressions.get(0) instanceof FunctionCall); + assertEquals("date_bin", ((FunctionCall) expressions.get(0)).getName().getSuffix()); + } + + private static void assertSingleOriginalIdentifier( + Analysis.GroupingSetAnalysis analysis, String name) { + assertEquals(1, analysis.getOriginalExpressions().size()); + assertIdentifier(analysis.getOriginalExpressions().get(0), name); + } + + private static void assertIdentifier(Expression expression, String name) { + assertTrue(expression instanceof Identifier); + assertEquals(name, ((Identifier) expression).getValue()); + } + + private static void assertFieldReference(Expression expression, int index) { + assertTrue(expression instanceof FieldReference); + assertEquals(index, ((FieldReference) expression).getFieldIndex()); + } + + private static QuerySpecification getExistsSubquery(QuerySpecification query) { + assertTrue(query.getWhere().get() instanceof ExistsPredicate); + Expression subquery = ((ExistsPredicate) query.getWhere().get()).getSubquery(); + assertTrue(subquery instanceof SubqueryExpression); + return (QuerySpecification) ((SubqueryExpression) subquery).getQuery().getQueryBody(); + } + + private static class AnalyzedQuery { + private final Analysis analysis; + private final QuerySpecification query; + + private AnalyzedQuery(Analysis analysis, QuerySpecification query) { + this.analysis = analysis; + this.query = query; + } + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TestMetadata.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TestMetadata.java index de97615321f0..ac266f19398c 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TestMetadata.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TestMetadata.java @@ -112,6 +112,7 @@ public class TestMetadata implements Metadata { public static final String DB1 = "testdb"; public static final String TREE_DB1 = "root.test"; public static final String TABLE1 = "table1"; + public static final String TABLE_WITH_X = "table_with_x"; public static final String TIME = "time"; private static final String TAG1 = "tag1"; private static final String TAG2 = "tag2"; @@ -121,6 +122,7 @@ public class TestMetadata implements Metadata { private static final String S1 = "s1"; private static final String S2 = "s2"; private static final String S3 = "s3"; + private static final String X = "x"; private static final ColumnMetadata TIME_CM = new ColumnMetadata(TIME, TIMESTAMP); private static final ColumnMetadata TAG1_CM = new ColumnMetadata(TAG1, StringType.STRING); private static final ColumnMetadata TAG2_CM = new ColumnMetadata(TAG2, StringType.STRING); @@ -130,6 +132,7 @@ public class TestMetadata implements Metadata { private static final ColumnMetadata S1_CM = new ColumnMetadata(S1, INT64); private static final ColumnMetadata S2_CM = new ColumnMetadata(S2, INT64); private static final ColumnMetadata S3_CM = new ColumnMetadata(S3, DOUBLE); + private static final ColumnMetadata X_CM = new ColumnMetadata(X, INT64); public static final String DB2 = "db2"; public static final String TABLE2 = "table2"; @@ -144,6 +147,7 @@ public class TestMetadata implements Metadata { public boolean tableExists(final QualifiedObjectName name) { return name.getDatabaseName().equalsIgnoreCase(DB1) && (name.getObjectName().equalsIgnoreCase(TABLE1) + || name.getObjectName().equalsIgnoreCase(TABLE_WITH_X) || name.getObjectName().equalsIgnoreCase(TABLE2) || name.getObjectName().equalsIgnoreCase(TABLE3)); } @@ -214,6 +218,15 @@ public Optional getTableSchema(SessionInfo session, QualifiedObject ColumnSchema.builder(S3_CM).setColumnCategory(TsTableColumnCategory.FIELD).build()); return Optional.of(new TableSchema(TABLE1, columnSchemas)); + } else if (name.getObjectName().equalsIgnoreCase(TABLE_WITH_X)) { + final List columnSchemas = + Arrays.asList( + ColumnSchema.builder(TIME_CM).setColumnCategory(TsTableColumnCategory.TIME).build(), + ColumnSchema.builder(X_CM).setColumnCategory(TsTableColumnCategory.FIELD).build(), + ColumnSchema.builder(S1_CM).setColumnCategory(TsTableColumnCategory.FIELD).build(), + ColumnSchema.builder(S2_CM).setColumnCategory(TsTableColumnCategory.FIELD).build()); + + return Optional.of(new TableSchema(TABLE_WITH_X, columnSchemas)); } else { List columnSchemas = Arrays.asList(