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 bf82d7a9950fb..2d6df6afe12d0 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.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 void selectWithNonExistMeasurementInWhereClause() { } } + @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 f292b162155f5..0f161037814ba 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 void testSeriesNotExist() { 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 018f171b13252..c6ae9a15a12d6 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 @@ private void analyzeDeviceToSource(Analysis analysis, QueryStatement queryStatem 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 05ce463d68452..b0b53d31de359 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.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 void testRawDataQueryAlignByDevice() { } } + @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 1e88743f2aba1..ab199047940c3 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.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 void testRawDataQueryAlignByDevice() { 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 =