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
+    ;

Reply via email to