This is an automated email from the ASF dual-hosted git repository.
JackieTien97 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 594fb879c4d Support table-less SELECT queries without FROM clause
(#17437)
594fb879c4d is described below
commit 594fb879c4d21398449291ccdcbe0d0e0ec26631
Author: FearfulTomcat27 <[email protected]>
AuthorDate: Thu Jun 4 20:24:51 2026 +0800
Support table-less SELECT queries without FROM clause (#17437)
---
.../it/query/recent/IoTDBComplexQueryIT.java | 28 ++++++++++++++++++++++
.../calc/plan/planner/TableOperatorGenerator.java | 21 ++++++++++++++--
.../plan/relational/planner/QueryPlanner.java | 12 +++++++++-
.../optimizations/UnaliasSymbolReferences.java | 14 +++++++++++
.../plan/relational/planner/node/ValuesNode.java | 5 ++++
5 files changed, 77 insertions(+), 3 deletions(-)
diff --git
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBComplexQueryIT.java
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBComplexQueryIT.java
index ac93e29302e..55b42142249 100644
---
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBComplexQueryIT.java
+++
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBComplexQueryIT.java
@@ -81,4 +81,32 @@ public class IoTDBComplexQueryIT {
retArray,
DATABASE_NAME);
}
+
+ @Test
+ public void testTableLessQuery() {
+ String[] expectedHeader;
+ String[] retArray;
+
+ expectedHeader = new String[] {"_col0", "_col1", "_col2", "_col3"};
+ retArray = new String[] {"2,0,1,1,"};
+ tableResultSetEqualTest("SELECT 1+1, 1-1, 1*1, 1/1", expectedHeader,
retArray, DATABASE_NAME);
+
+ expectedHeader = new String[] {"_col0", "_col1", "_col2"};
+ retArray = new String[] {"0.841471,0.540302,1.557408,"};
+ tableResultSetEqualTest(
+ "SELECT round(sin(1),6), round(cos(1),6), round(tan(1),6)",
+ expectedHeader,
+ retArray,
+ DATABASE_NAME);
+
+ expectedHeader = new String[] {"_col0"};
+ retArray = new String[] {"Hello world,"};
+ tableResultSetEqualTest(
+ "SELECT FORMAT('Hello %s','world')", expectedHeader, retArray,
DATABASE_NAME);
+
+ // SELECT COUNT(*) without FROM returns 1 (implicit single-row semantics)
+ expectedHeader = new String[] {"_col0"};
+ retArray = new String[] {"1,"};
+ tableResultSetEqualTest("SELECT COUNT(*)", expectedHeader, retArray,
DATABASE_NAME);
+ }
}
diff --git
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/TableOperatorGenerator.java
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/TableOperatorGenerator.java
index ed4fc101b1b..f65ba4dbc5d 100644
---
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/TableOperatorGenerator.java
+++
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/plan/planner/TableOperatorGenerator.java
@@ -176,12 +176,14 @@ import org.apache.tsfile.block.column.Column;
import org.apache.tsfile.common.conf.TSFileConfig;
import org.apache.tsfile.common.conf.TSFileDescriptor;
import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.common.block.TsBlock;
import org.apache.tsfile.read.common.block.column.BinaryColumn;
import org.apache.tsfile.read.common.block.column.BooleanColumn;
import org.apache.tsfile.read.common.block.column.DoubleColumn;
import org.apache.tsfile.read.common.block.column.FloatColumn;
import org.apache.tsfile.read.common.block.column.IntColumn;
import org.apache.tsfile.read.common.block.column.LongColumn;
+import org.apache.tsfile.read.common.block.column.RunLengthEncodedColumn;
import org.apache.tsfile.read.common.type.Type;
import org.apache.tsfile.utils.Binary;
@@ -213,6 +215,7 @@ import static
org.apache.iotdb.calc.execution.operator.source.relational.aggrega
import static
org.apache.iotdb.calc.execution.operator.source.relational.aggregation.AccumulatorFactory.createBuiltinAccumulator;
import static
org.apache.iotdb.calc.execution.operator.source.relational.aggregation.AccumulatorFactory.createGroupedAccumulator;
import static
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.IDENTITY_FILL;
+import static
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.TIME_COLUMN_TEMPLATE;
import static
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.UNKNOWN_DATATYPE;
import static
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.getLinearFill;
import static
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.getPreviousFill;
@@ -2179,8 +2182,22 @@ public abstract class TableOperatorGenerator<
context, node.getPlanNodeId(),
MappingCollectOperator.class.getSimpleName());
// Currently we only support empty values operator
- assert node.getRowCount() == 0;
- return new ValuesOperator(operatorContext, ImmutableList.of());
+ if (node.getRowCount() == 0) {
+ return new ValuesOperator(operatorContext, ImmutableList.of());
+ }
+
+ // No-FROM query (e.g. SELECT 1+1): produce rowCount rows with no value
columns so that the
+ // upstream ProjectNode can evaluate expressions once per row.
+ if (node.getRowCount() == 1) {
+ TsBlock oneRowWithoutColumnsBlock =
+ new TsBlock(
+ node.getRowCount(),
+ new RunLengthEncodedColumn(TIME_COLUMN_TEMPLATE,
node.getRowCount()),
+ new Column[0]);
+ return new ValuesOperator(operatorContext,
ImmutableList.of(oneRowWithoutColumnsBlock));
+ } else {
+ throw new IllegalArgumentException("Row count must be 0 or 1");
+ }
}
@Override
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
index 10ea1c1e25a..0232406c4e5 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
@@ -40,6 +40,7 @@ import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.Previou
import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ProjectNode;
import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.SortNode;
import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ValueFillNode;
+import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ValuesNode;
import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.WindowNode;
import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Cast;
import
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.ComparisonExpression;
@@ -61,6 +62,7 @@ import
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.QuerySpecifi
import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.SortItem;
import
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.VariableDefinition;
import
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.WindowFrame;
+import
org.apache.iotdb.commons.queryengine.plan.relational.type.InternalTypeManager;
import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
import org.apache.iotdb.db.queryengine.common.QueryId;
@@ -68,6 +70,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.analyzer.Analysis;
import
org.apache.iotdb.db.queryengine.plan.relational.analyzer.Analysis.GroupingSetAnalysis;
import org.apache.iotdb.db.queryengine.plan.relational.analyzer.FieldId;
import org.apache.iotdb.db.queryengine.plan.relational.analyzer.RelationType;
+import
org.apache.iotdb.db.queryengine.plan.relational.metadata.TableMetadataImpl;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.ir.GapFillStartAndEndTimeExtractVisitor;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.ir.PredicateWithUncorrelatedScalarSubqueryReconstructor;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Delete;
@@ -792,7 +795,14 @@ public class QueryPlanner {
.process(node.getFrom().orElse(null), null);
return newPlanBuilder(relationPlan, analysis);
} else {
- throw new
SemanticException(DataNodeQueryMessages.FROM_CLAUSE_MUST_NOT_BE_EMPTY);
+ return new PlanBuilder(
+ new TranslationMap(
+ outerContext,
+ analysis.getImplicitFromScope(node),
+ analysis,
+ ImmutableList.of(),
+ new PlannerContext(new TableMetadataImpl(), new
InternalTypeManager())),
+ new ValuesNode(queryIdAllocator.genPlanNodeId(), 1));
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
index 6dda2d17503..b944715fb8f 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
@@ -53,6 +53,7 @@ import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKNod
import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKRankingNode;
import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ValueFillNode;
+import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ValuesNode;
import
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.WindowNode;
import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Expression;
import
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.NullLiteral;
@@ -510,6 +511,19 @@ public class UnaliasSymbolReferences implements
PlanOptimizer {
mapping);
}
+ @Override
+ public PlanAndMappings visitValuesNode(ValuesNode node, UnaliasContext
context) {
+ Map<Symbol, Symbol> mapping = new
HashMap<>(context.getCorrelationMapping());
+ SymbolMapper mapper = symbolMapper(mapping);
+
+ List<Symbol> newOutputs = mapper.map(node.getOutputSymbols());
+ Optional<List<Expression>> newRows =
+ node.getRows().map(rows ->
rows.stream().map(mapper::map).collect(toImmutableList()));
+
+ return new PlanAndMappings(
+ new ValuesNode(node.getPlanNodeId(), newOutputs, node.getRowCount(),
newRows), mapping);
+ }
+
@Override
public PlanAndMappings visitFilter(FilterNode node, UnaliasContext
context) {
PlanAndMappings rewrittenSource = node.getChild().accept(this, context);
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/ValuesNode.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/ValuesNode.java
index 473282d41f0..3dd6071e475 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/ValuesNode.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/relational/planner/node/ValuesNode.java
@@ -205,6 +205,11 @@ public class ValuesNode extends SourceNode {
planNodeId, outputSymbols, rowCount, flag ? Optional.of(rows) :
Optional.empty());
}
+ @Override
+ public List<Symbol> getOutputSymbols() {
+ return outputSymbols;
+ }
+
public int getRowCount() {
return rowCount;
}