diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBFromFirstQueryIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBFromFirstQueryIT.java new file mode 100644 index 0000000000000..6dd855f3b9b92 --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBFromFirstQueryIT.java @@ -0,0 +1,126 @@ +/* + * 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.relational.it.query.recent; + +import org.apache.iotdb.it.env.EnvFactory; +import org.apache.iotdb.it.framework.IoTDBTestRunner; +import org.apache.iotdb.itbase.category.TableClusterIT; +import org.apache.iotdb.itbase.category.TableLocalStandaloneIT; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import static org.apache.iotdb.db.it.utils.TestUtils.prepareTableData; +import static org.apache.iotdb.db.it.utils.TestUtils.tableResultSetEqualTest; + +@RunWith(IoTDBTestRunner.class) +@Category({TableLocalStandaloneIT.class, TableClusterIT.class}) +public class IoTDBFromFirstQueryIT { + private static final String DATABASE_NAME = "test"; + + private static final String[] createSqls = + new String[] { + "CREATE DATABASE " + DATABASE_NAME, + "USE " + DATABASE_NAME, + "CREATE TABLE table1(device STRING TAG, region STRING TAG, temperature FLOAT FIELD, humidity DOUBLE FIELD)", + "CREATE TABLE table2(device STRING TAG, location STRING TAG, pressure FLOAT FIELD)", + "INSERT INTO table1(time,device,region,temperature,humidity) values(1,'d1','north',25.5,60.3)", + "INSERT INTO table1(time,device,region,temperature,humidity) values(2,'d1','north',26.1,59.8)", + "INSERT INTO table1(time,device,region,temperature,humidity) values(3,'d2','south',24.8,65.2)", + "INSERT INTO table2(time,device,location,pressure) values(1,'d1','room1',1013.25)", + "INSERT INTO table2(time,device,location,pressure) values(2,'d2','room2',1012.5)", + "FLUSH" + }; + + @BeforeClass + public static void setUp() throws Exception { + EnvFactory.getEnv().initClusterEnvironment(); + prepareTableData(createSqls); + } + + @AfterClass + public static void tearDown() throws Exception { + EnvFactory.getEnv().cleanClusterEnvironment(); + } + + @Test + public void testBasicFromFirstQuery() { + String[] expectedHeader = new String[] {"time", "device", "region", "temperature", "humidity"}; + String[] retArray = + new String[] { + "1970-01-01T00:00:00.001Z,d1,north,25.5,60.3,", + "1970-01-01T00:00:00.002Z,d1,north,26.1,59.8,", + "1970-01-01T00:00:00.003Z,d2,south,24.8,65.2," + }; + + tableResultSetEqualTest( + "FROM table1 SELECT * order by time", expectedHeader, retArray, DATABASE_NAME); + } + + @Test + public void testFromFirstWithImplicitSelect() { + String[] expectedHeader = new String[] {"time", "device", "region", "temperature", "humidity"}; + String[] retArray = + new String[] { + "1970-01-01T00:00:00.001Z,d1,north,25.5,60.3,", + "1970-01-01T00:00:00.002Z,d1,north,26.1,59.8,", + "1970-01-01T00:00:00.003Z,d2,south,24.8,65.2," + }; + + tableResultSetEqualTest("FROM table1 order by time", expectedHeader, retArray, DATABASE_NAME); + } + + @Test + public void testFromFirstWithSimpleJoin() { + String[] expectedHeader = + new String[] { + "time", "device", "region", "temperature", "humidity", "location", "pressure" + }; + String[] retArray = + new String[] { + "1970-01-01T00:00:00.001Z,d1,north,25.5,60.3,room1,1013.25,", + "1970-01-01T00:00:00.002Z,d1,north,26.1,59.8,null,null,", + "1970-01-01T00:00:00.003Z,d2,south,24.8,65.2,null,null," + }; + + tableResultSetEqualTest( + "FROM table1 t1 LEFT JOIN table2 t2 ON t1.device = t2.device AND t1.time = t2.time " + + "SELECT t1.time, t1.device, t1.region, t1.temperature, t1.humidity, t2.location, t2.pressure " + + "ORDER BY t1.time", + expectedHeader, + retArray, + DATABASE_NAME); + } + + @Test + public void testFromFirstWithWhereAndAggregate() { + String[] expectedHeader = new String[] {"device", "avg_temp", "count_rows"}; + String[] retArray = new String[] {"d1,25.8,2,", "d2,24.8,1,"}; + + tableResultSetEqualTest( + "FROM table1 SELECT device, ROUND(AVG(temperature), 1) as avg_temp, COUNT(*) as count_rows WHERE temperature > 24.0 GROUP BY device order by device", + expectedHeader, + retArray, + DATABASE_NAME); + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java index 70dc79b6adb3a..8f21b94134560 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java @@ -2391,6 +2391,45 @@ public Node visitQuerySpecification(RelationalSqlParser.QuerySpecificationContex Optional.empty()); } + @Override + public Node visitFromFirstQuerySpecification( + RelationalSqlParser.FromFirstQuerySpecificationContext ctx) { + Optional from = Optional.empty(); + List selectItems = visit(ctx.selectItem(), SelectItem.class); + + List relations = visit(ctx.relation(), Relation.class); + if (!relations.isEmpty()) { + // synthesize implicit join nodes + Iterator iterator = relations.iterator(); + Relation relation = iterator.next(); + + while (iterator.hasNext()) { + relation = new Join(getLocation(ctx), Join.Type.IMPLICIT, relation, iterator.next()); + } + + from = Optional.of(relation); + } + if (selectItems.isEmpty()) { + selectItems = ImmutableList.of(new AllColumns(getLocation(ctx), ImmutableList.of())); + } + + NodeLocation selectLocation = + ctx.SELECT() != null ? getLocation(ctx.SELECT()) : getLocation(ctx); + + return new QuerySpecification( + getLocation(ctx), + new Select(selectLocation, isDistinct(ctx.setQuantifier()), selectItems), + from, + visitIfPresent(ctx.where, Expression.class), + visitIfPresent(ctx.groupBy(), GroupBy.class), + visitIfPresent(ctx.having, Expression.class), + Optional.empty(), + visit(ctx.windowDefinition(), WindowDefinition.class), + Optional.empty(), + Optional.empty(), + Optional.empty()); + } + @Override public Node visitSelectSingle(RelationalSqlParser.SelectSingleContext ctx) { if (ctx.identifier() != null) { diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java index 36bed6932a877..8c4bc256493a2 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java @@ -1252,6 +1252,120 @@ public void analyzeInsertRow() { assertEquals(1, distributedQueryPlan.getInstances().size()); } + @Test + public void fromFirstQueryTest() throws OperatorNotFoundException { + final String sqlSelectFirst = "SELECT * FROM table1"; + final String sqlFromFirst = "FROM table1 SELECT *"; + + final Analysis analysisSelectFirst = analyzeSQL(sqlSelectFirst, TEST_MATADATA, QUERY_CONTEXT); + final SymbolAllocator symbolAllocatorSelectFirst = new SymbolAllocator(); + final LogicalQueryPlan logicalQueryPlanSelectFirst = + new TableLogicalPlanner( + QUERY_CONTEXT, + TEST_MATADATA, + SESSION_INFO, + symbolAllocatorSelectFirst, + DEFAULT_WARNING) + .plan(analysisSelectFirst); + final PlanNode rootNodeSelectFirst = logicalQueryPlanSelectFirst.getRootNode(); + final DeviceTableScanNode deviceTableScanNodeSelectFirst = + (DeviceTableScanNode) ((OutputNode) rootNodeSelectFirst).getChild(); + + final Analysis analysisFromFirst = analyzeSQL(sqlFromFirst, TEST_MATADATA, QUERY_CONTEXT); + final SymbolAllocator symbolAllocatorFromFirst = new SymbolAllocator(); + final LogicalQueryPlan logicalQueryPlanFromFirst = + new TableLogicalPlanner( + QUERY_CONTEXT, + TEST_MATADATA, + SESSION_INFO, + symbolAllocatorFromFirst, + DEFAULT_WARNING) + .plan(analysisFromFirst); + final PlanNode rootNodeFromFirst = logicalQueryPlanFromFirst.getRootNode(); + final DeviceTableScanNode deviceTableScanNodeFromFirst = + (DeviceTableScanNode) ((OutputNode) rootNodeFromFirst).getChild(); + + assertEquals( + deviceTableScanNodeSelectFirst.getOutputColumnNames(), + deviceTableScanNodeFromFirst.getOutputColumnNames()); + + assertEquals( + deviceTableScanNodeSelectFirst.getQualifiedObjectName(), + deviceTableScanNodeFromFirst.getQualifiedObjectName()); + } + + @Test + public void fromFirstImplicitSelectTest() throws OperatorNotFoundException { + final String sqlFromFirst = "FROM table1"; + final String sqlSelectFirst = "SELECT * FROM table1"; + + final Analysis analysisFromFirst = analyzeSQL(sqlFromFirst, TEST_MATADATA, QUERY_CONTEXT); + final SymbolAllocator symbolAllocatorFromFirst = new SymbolAllocator(); + final LogicalQueryPlan logicalQueryPlanFromFirst = + new TableLogicalPlanner( + QUERY_CONTEXT, + TEST_MATADATA, + SESSION_INFO, + symbolAllocatorFromFirst, + DEFAULT_WARNING) + .plan(analysisFromFirst); + final PlanNode rootNodeFromFirst = logicalQueryPlanFromFirst.getRootNode(); + final DeviceTableScanNode deviceTableScanNodeFromFirst = + (DeviceTableScanNode) ((OutputNode) rootNodeFromFirst).getChild(); + + final Analysis analysisSelectFirst = analyzeSQL(sqlSelectFirst, TEST_MATADATA, QUERY_CONTEXT); + final SymbolAllocator symbolAllocatorSelectFirst = new SymbolAllocator(); + final LogicalQueryPlan logicalQueryPlanSelectFirst = + new TableLogicalPlanner( + QUERY_CONTEXT, + TEST_MATADATA, + SESSION_INFO, + symbolAllocatorSelectFirst, + DEFAULT_WARNING) + .plan(analysisSelectFirst); + final PlanNode rootNodeSelectFirst = logicalQueryPlanSelectFirst.getRootNode(); + final DeviceTableScanNode deviceTableScanNodeSelectFirst = + (DeviceTableScanNode) ((OutputNode) rootNodeSelectFirst).getChild(); + + assertEquals( + deviceTableScanNodeSelectFirst.getOutputColumnNames(), + deviceTableScanNodeFromFirst.getOutputColumnNames()); + + assertEquals( + Arrays.asList("time", "tag1", "tag2", "tag3", "attr1", "attr2", "s1", "s2", "s3"), + deviceTableScanNodeFromFirst.getOutputColumnNames()); + } + + @Test + public void fromFirstWithFilterTest() throws OperatorNotFoundException { + final String sql = "FROM table1 SELECT tag1, s1 WHERE s1 > 1"; + + final Analysis analysis = analyzeSQL(sql, TEST_MATADATA, QUERY_CONTEXT); + final SymbolAllocator symbolAllocator = new SymbolAllocator(); + final LogicalQueryPlan logicalQueryPlan = + new TableLogicalPlanner( + QUERY_CONTEXT, TEST_MATADATA, SESSION_INFO, symbolAllocator, DEFAULT_WARNING) + .plan(analysis); + + final PlanNode rootNode = logicalQueryPlan.getRootNode(); + assertTrue(rootNode instanceof OutputNode); + assertTrue(rootNode.getChildren().get(0) instanceof DeviceTableScanNode); + + final DeviceTableScanNode deviceTableScanNode = + (DeviceTableScanNode) rootNode.getChildren().get(0); + + assertEquals(Arrays.asList("tag1", "s1"), deviceTableScanNode.getOutputColumnNames()); + + assertNotNull(deviceTableScanNode.getPushDownPredicate()); + assertEquals("(\"s1\" > 1)", deviceTableScanNode.getPushDownPredicate().toString()); + + assertEquals( + ImmutableSet.of("tag1", "s1"), + deviceTableScanNode.getAssignments().keySet().stream() + .map(Symbol::toString) + .collect(Collectors.toSet())); + } + public static Analysis analyzeSQL(String sql, Metadata metadata, final MPPQueryContext context) { SqlParser sqlParser = new SqlParser(); Statement statement = diff --git a/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4 b/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4 index 291aa1ea1ae2b..aff01cb0d0ba1 100644 --- a/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4 +++ b/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4 @@ -996,6 +996,7 @@ queryPrimary | TABLE qualifiedName #table | VALUES expression (',' expression)* #inlineTable | '(' queryNoWith ')' #subquery + | fromFirstQuerySpecification #fromFirstQueryPrimary ; sortItem @@ -1011,6 +1012,15 @@ querySpecification (WINDOW windowDefinition (',' windowDefinition)*)? ; +fromFirstQuerySpecification + : FROM relation (',' relation)* + (SELECT setQuantifier? selectItem (',' selectItem)*)? + (WHERE where=booleanExpression)? + (GROUP BY groupBy)? + (HAVING having=booleanExpression)? + (WINDOW windowDefinition (',' windowDefinition)*)? + ; + groupBy : setQuantifier? groupingElement (',' groupingElement)* ; @@ -2010,4 +2020,4 @@ WS // when splitting statements with DelimiterLexer UNRECOGNIZED : . - ; \ No newline at end of file + ;