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 321bba77b39 Fix NPE in ALIGN BY DEVICE queries when a device's 
predicate is always true (#19009)
321bba77b39 is described below

commit 321bba77b39c30dd6cffdb154e761da85910c3ec
Author: Jackie Tien <[email protected]>
AuthorDate: Sat Oct 24 08:13:17 2026 +0800

    Fix NPE in ALIGN BY DEVICE queries when a device's predicate is always true 
(#19009)
---
 .../db/it/alignbydevice/IoTDBAlignByDeviceIT.java  | 17 +++++++++
 .../iotdb/db/it/query/IoTDBNullOperandIT.java      | 31 +++++++++++++++
 .../queryengine/plan/analyze/AnalyzeVisitor.java   |  9 +++--
 .../db/queryengine/plan/analyze/AnalyzeTest.java   | 44 ++++++++++++++++++++++
 .../logical/DataQueryLogicalPlannerTest.java       | 27 +++++++++++++
 5 files changed, 125 insertions(+), 3 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/db/it/alignbydevice/IoTDBAlignByDeviceIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/db/it/alignbydevice/IoTDBAlignByDeviceIT.java
index bf82d7a9950..2d6df6afe12 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/db/it/alignbydevice/IoTDBAlignByDeviceIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/db/it/alignbydevice/IoTDBAlignByDeviceIT.java
@@ -41,6 +41,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import static org.apache.iotdb.db.it.utils.TestUtils.resultSetEqualTest;
 import static org.junit.Assert.fail;
 
 @RunWith(IoTDBTestRunner.class)
@@ -1127,6 +1128,22 @@ public class IoTDBAlignByDeviceIT {
     }
   }
 
+  @Test
+  public void selectWithNonExistMeasurementInAlwaysTrueWhereClause() {
+    // root.vehicle.d1 has no s1, so its predicate is always true, while 
root.vehicle.d0 is still
+    // filtered by its own s1
+    resultSetEqualTest(
+        "select s0 from root.vehicle.* where s1 is null align by device",
+        new String[] {"Time", "Device", "s0"},
+        new String[] {
+          "106,root.vehicle.d0,99,", "1,root.vehicle.d1,999,", 
"1000,root.vehicle.d1,888,",
+        });
+    resultSetEqualTest(
+        "select count(s0) from root.vehicle.* where s1 is null align by 
device",
+        new String[] {"Device", "count(s0)"},
+        new String[] {"root.vehicle.d0,1,", "root.vehicle.d1,2,"});
+  }
+
   /**
    * data structure D, time, s1, s2; d1: 1, 10, 20; d2: 1, 12, 22; d3: 1, 
null, 33
    *
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/db/it/query/IoTDBNullOperandIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/db/it/query/IoTDBNullOperandIT.java
index f292b162155..0f161037814 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/db/it/query/IoTDBNullOperandIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/db/it/query/IoTDBNullOperandIT.java
@@ -279,4 +279,35 @@ public class IoTDBNullOperandIT {
         expectedHeader,
         retArray);
   }
+
+  @Test
+  public void testSeriesNotExistInWhereAlignByDevice() {
+    // a predicate on a series that the device does not have is always true 
for that device
+    String[] expectedHeader = new String[] {TIMESTAMP_STR, "Device", "s1", 
"s3", "s4"};
+    String[] retArray =
+        new String[] {
+          "1,root.test.sg1,1,true,false,",
+          "2,root.test.sg1,2,true,null,",
+          "3,root.test.sg1,3,null,false,",
+        };
+    resultSetEqualTest(
+        "select s1, s3, s4 from root.** where notExist is null align by 
device",
+        expectedHeader,
+        retArray);
+    resultSetEqualTest(
+        "select s1, s3, s4 from root.** where not(notExist > 0) align by 
device",
+        expectedHeader,
+        retArray);
+    resultSetEqualTest(
+        "select s1, s3, s4 from root.** where notExist is null and time > 1 
align by device",
+        expectedHeader,
+        new String[] {"2,root.test.sg1,2,true,null,", 
"3,root.test.sg1,3,null,false,"});
+
+    expectedHeader = new String[] {"Device", "count(s1)"};
+    retArray = new String[] {"root.test.sg1,3,"};
+    resultSetEqualTest(
+        "select count(s1) from root.** where notExist is null align by device",
+        expectedHeader,
+        retArray);
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java
index 6781cc019c7..c9a422dec98 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeVisitor.java
@@ -1485,9 +1485,12 @@ public class AnalyzeVisitor extends 
StatementVisitor<Analysis, MPPQueryContext>
           deviceToWhereExpression.entrySet()) {
         IDeviceID deviceName = deviceWhereExpressionEntry.getKey();
         Expression whereExpression = deviceWhereExpressionEntry.getValue();
-        deviceToSourceExpressions
-            .computeIfAbsent(deviceName, key -> new LinkedHashSet<>())
-            .addAll(searchSourceExpressions(whereExpression));
+        // null means the predicate of this device is always true, so it needs 
no value filter
+        if (whereExpression != null) {
+          deviceToSourceExpressions
+              .computeIfAbsent(deviceName, key -> new LinkedHashSet<>())
+              .addAll(searchSourceExpressions(whereExpression));
+        }
       }
     }
 
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeTest.java
index 05ce463d684..b0b53d31de3 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/AnalyzeTest.java
@@ -45,6 +45,7 @@ import 
org.apache.iotdb.db.queryengine.plan.expression.leaf.ConstantOperand;
 import org.apache.iotdb.db.queryengine.plan.expression.leaf.TimeSeriesOperand;
 import org.apache.iotdb.db.queryengine.plan.expression.leaf.TimestampOperand;
 import 
org.apache.iotdb.db.queryengine.plan.expression.multi.FunctionExpression;
+import org.apache.iotdb.db.queryengine.plan.expression.unary.IsNullExpression;
 import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator;
 import org.apache.iotdb.db.queryengine.plan.statement.Statement;
 import org.apache.iotdb.db.queryengine.plan.statement.crud.QueryStatement;
@@ -387,6 +388,49 @@ public class AnalyzeTest {
     }
   }
 
+  @Test
+  public void testAlignByDeviceWithAlwaysTruePredicate() throws 
IllegalPathException {
+    // root.sg.d2 has no s3, so its predicate is simplified to TRUE and needs 
no value filter
+    String[] sqls =
+        new String[] {
+          "select s1 from root.sg.d2 where s3 is null align by device",
+          "select s1 from root.sg.d2 where not(s3 > 1) align by device"
+        };
+    for (String sql : sqls) {
+      Analysis analysis = analyzeSQL(sql);
+
+      Assert.assertNull(sql, 
analysis.getDeviceToWhereExpression().get(DEVICE2));
+      Assert.assertFalse(sql, analysis.hasValueFilter());
+      assertEquals(
+          sql,
+          ImmutableMap.of(
+              DEVICE2, Sets.newHashSet(new TimeSeriesOperand(new 
PartialPath("root.sg.d2.s1")))),
+          analysis.getDeviceToSourceExpressions());
+    }
+  }
+
+  @Test
+  public void testAlignByDeviceWithAlwaysTruePredicateOnPartOfDevices()
+      throws IllegalPathException {
+    // root.sg.d1 is still filtered by its own s3, while the predicate of 
root.sg.d2 is TRUE
+    Analysis analysis = analyzeSQL("select s1 from root.sg.* where s3 is null 
align by device");
+
+    assertEquals(
+        new IsNullExpression(new TimeSeriesOperand(new 
PartialPath("root.sg.d1.s3")), false),
+        analysis.getDeviceToWhereExpression().get(DEVICE1));
+    Assert.assertNull(analysis.getDeviceToWhereExpression().get(DEVICE2));
+    Assert.assertTrue(analysis.hasValueFilter());
+    assertEquals(
+        ImmutableMap.of(
+            DEVICE1,
+            Sets.newHashSet(
+                new TimeSeriesOperand(new PartialPath("root.sg.d1.s1")),
+                new TimeSeriesOperand(new PartialPath("root.sg.d1.s3"))),
+            DEVICE2,
+            Sets.newHashSet(new TimeSeriesOperand(new 
PartialPath("root.sg.d2.s1")))),
+        analysis.getDeviceToSourceExpressions());
+  }
+
   @Test
   public void testAggregationQueryAlignByDevice() {
     String sql =
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/logical/DataQueryLogicalPlannerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/logical/DataQueryLogicalPlannerTest.java
index 1e88743f2ab..ab199047940 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/logical/DataQueryLogicalPlannerTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/logical/DataQueryLogicalPlannerTest.java
@@ -30,6 +30,7 @@ import 
org.apache.iotdb.db.queryengine.plan.expression.binary.GreaterThanExpress
 import 
org.apache.iotdb.db.queryengine.plan.expression.binary.LogicAndExpression;
 import org.apache.iotdb.db.queryengine.plan.expression.leaf.ConstantOperand;
 import org.apache.iotdb.db.queryengine.plan.expression.leaf.TimeSeriesOperand;
+import org.apache.iotdb.db.queryengine.plan.expression.unary.IsNullExpression;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.process.DeviceViewNode;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.process.FilterNode;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.process.GroupByLevelNode;
@@ -370,6 +371,32 @@ public class DataQueryLogicalPlannerTest {
     Assert.assertEquals(actualPlan, limitNode);
   }
 
+  @Test
+  public void testRawDataQueryAlignByDeviceWithAlwaysTruePredicate() {
+    // root.sg.d2 has no s3, so its predicate is always true and only 
root.sg.d1 needs a FilterNode
+    String sql = "SELECT s1 FROM root.sg.* WHERE s3 IS NULL ALIGN BY DEVICE";
+
+    PlanNode actualPlan = parseSQLToPlanNode(sql);
+    Assert.assertTrue(actualPlan instanceof DeviceViewNode);
+    DeviceViewNode deviceViewNode = (DeviceViewNode) actualPlan;
+    Assert.assertEquals(
+        Arrays.asList(
+            IDeviceID.Factory.DEFAULT_FACTORY.create("root.sg.d1"),
+            IDeviceID.Factory.DEFAULT_FACTORY.create("root.sg.d2")),
+        deviceViewNode.getDevices());
+
+    PlanNode d1SubPlan = deviceViewNode.getChildren().get(0);
+    Assert.assertTrue(d1SubPlan instanceof FilterNode);
+    Assert.assertEquals(
+        new IsNullExpression(new 
TimeSeriesOperand(schemaMap.get("root.sg.d1.s3")), false),
+        ((FilterNode) d1SubPlan).getPredicate());
+
+    PlanNode d2SubPlan = deviceViewNode.getChildren().get(1);
+    Assert.assertTrue(d2SubPlan instanceof SeriesScanNode);
+    Assert.assertEquals(
+        schemaMap.get("root.sg.d2.s1"), ((SeriesScanNode) 
d2SubPlan).getSeriesPath());
+  }
+
   @Test
   public void testSimpleAggregationQuery() {
     String sql =

Reply via email to