This is an automated email from the ASF dual-hosted git repository.
jackietien pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 2e77bf44c37 Add FROM-first query syntax support (#17410)
2e77bf44c37 is described below
commit 2e77bf44c3754e615aadd9042d03866a046fd818
Author: Abdelrahman Mostafa <[email protected]>
AuthorDate: Wed Apr 8 10:57:52 2026 +0200
Add FROM-first query syntax support (#17410)
---
.../it/query/recent/IoTDBFromFirstQueryIT.java | 126 +++++++++++++++++++++
.../plan/relational/sql/parser/AstBuilder.java | 66 +++++++++--
.../plan/relational/analyzer/AnalyzerTest.java | 114 +++++++++++++++++++
.../db/relational/grammar/sql/RelationalSql.g4 | 12 +-
4 files changed, 308 insertions(+), 10 deletions(-)
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 00000000000..6dd855f3b9b
--- /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 c2dec8dce4c..5c481196714 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
@@ -2438,31 +2438,79 @@ public class AstBuilder extends
RelationalSqlBaseVisitor<Node> {
@Override
public Node
visitQuerySpecification(RelationalSqlParser.QuerySpecificationContext ctx) {
+ return buildQuerySpecification(
+ ctx,
+ ctx.selectItem(),
+ ctx.relation(),
+ ctx.SELECT(),
+ ctx.setQuantifier(),
+ ctx.where,
+ ctx.groupBy(),
+ ctx.having,
+ ctx.windowDefinition());
+ }
+
+ @Override
+ public Node visitFromFirstQuerySpecification(
+ RelationalSqlParser.FromFirstQuerySpecificationContext ctx) {
+ return buildQuerySpecification(
+ ctx,
+ ctx.selectItem(),
+ ctx.relation(),
+ ctx.SELECT(),
+ ctx.setQuantifier(),
+ ctx.where,
+ ctx.groupBy(),
+ ctx.having,
+ ctx.windowDefinition());
+ }
+
+ private Node buildQuerySpecification(
+ ParserRuleContext parserRuleContext,
+ List<RelationalSqlParser.SelectItemContext> selectItemContexts,
+ List<RelationalSqlParser.RelationContext> relationContexts,
+ TerminalNode selectNode,
+ RelationalSqlParser.SetQuantifierContext setQuantifier,
+ RelationalSqlParser.BooleanExpressionContext where,
+ RelationalSqlParser.GroupByContext groupBy,
+ RelationalSqlParser.BooleanExpressionContext having,
+ List<RelationalSqlParser.WindowDefinitionContext> windowDefinitions) {
+
Optional<Relation> from = Optional.empty();
- List<SelectItem> selectItems = visit(ctx.selectItem(), SelectItem.class);
+ List<SelectItem> selectItems = visit(selectItemContexts, SelectItem.class);
- List<Relation> relations = visit(ctx.relation(), Relation.class);
+ List<Relation> relations = visit(relationContexts, Relation.class);
if (!relations.isEmpty()) {
// synthesize implicit join nodes
Iterator<Relation> iterator = relations.iterator();
Relation relation = iterator.next();
while (iterator.hasNext()) {
- relation = new Join(getLocation(ctx), Join.Type.IMPLICIT, relation,
iterator.next());
+ relation =
+ new Join(getLocation(parserRuleContext), Join.Type.IMPLICIT,
relation, iterator.next());
}
from = Optional.of(relation);
}
+ // If no SELECT items provided (FROM-first without SELECT clause), default
to SELECT *
+ if (selectItems.isEmpty()) {
+ selectItems =
+ ImmutableList.of(new AllColumns(getLocation(parserRuleContext),
ImmutableList.of()));
+ }
+
+ NodeLocation selectLocation =
+ selectNode != null ? getLocation(selectNode) :
getLocation(parserRuleContext);
+
return new QuerySpecification(
- getLocation(ctx),
- new Select(getLocation(ctx.SELECT()), isDistinct(ctx.setQuantifier()),
selectItems),
+ getLocation(parserRuleContext),
+ new Select(selectLocation, isDistinct(setQuantifier), selectItems),
from,
- visitIfPresent(ctx.where, Expression.class),
- visitIfPresent(ctx.groupBy(), GroupBy.class),
- visitIfPresent(ctx.having, Expression.class),
+ visitIfPresent(where, Expression.class),
+ visitIfPresent(groupBy, GroupBy.class),
+ visitIfPresent(having, Expression.class),
Optional.empty(),
- visit(ctx.windowDefinition(), WindowDefinition.class),
+ visit(windowDefinitions, WindowDefinition.class),
Optional.empty(),
Optional.empty(),
Optional.empty());
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 36bed6932a8..8c4bc256493 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 class AnalyzerTest {
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 f30dbba697b..ca766464686 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
@@ -1018,6 +1018,7 @@ queryPrimary
| TABLE qualifiedName #table
| VALUES expression (',' expression)* #inlineTable
| '(' queryNoWith ')' #subquery
+ | fromFirstQuerySpecification #fromFirstQueryPrimary
;
sortItem
@@ -1033,6 +1034,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)*
;
@@ -2036,4 +2046,4 @@ WS
// when splitting statements with DelimiterLexer
UNRECOGNIZED
: .
- ;
\ No newline at end of file
+ ;