This is an automated email from the ASF dual-hosted git repository.
ColinLeeo 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 a4202b47f99 Fix tree-model NOT predicate pushdown (#18982)
a4202b47f99 is described below
commit a4202b47f990dd0e3160e33902dcea30b145c11d
Author: Colin Lee <[email protected]>
AuthorDate: Fri Oct 23 16:13:43 2026 +0800
Fix tree-model NOT predicate pushdown (#18982)
* Fix tree-model NOT predicate pushdown
Remove NOT from tree-model WHERE and HAVING predicates before binding and
pushdown, including the templated device path. Prevent non-invertible NOT
predicates from reaching TsFile filters.
* Address NOT predicate review feedback
Make NOT rewriting total and type-aware, fix root predicate context
propagation through unary expressions, and add regressions for NaN, nested
negation, unsupported operands, composite HAVING, and aligned templates.
* Extract time predicate from a NOT over a disjunction
NOT (A OR B) is NOT(A) AND NOT(B), so every disjunct that is a pure time
predicate still bounds the time range of the negated predicate. Hence
WHERE NOT (time > 2 OR s1 > 5) now yields time <= 2 for partition pruning
instead of scanning every time partition.
* Cover HAVING NOT over boolean aggregations
Add regressions for HAVING NOT last_value(bool) on the raw, ALIGN BY DEVICE
and aligned-template paths, which is the non-invertible HAVING NOT shape
that must stay in FilterOperator.
* Guard time-only predicates during source extraction
* Return the original predicate when NOT cannot be inverted
predicateRemoveNot() rebuilt the tree even when the operand could not be
inverted, so callers comparing instances to detect a rewrite always saw a
change and re-simplified, re-normalized and re-analyzed the predicate once
per
device. Return the original node instead so the existing short-circuit
works.
Also remove DataNodeQueryMessages.GROUP_BY_TIME_CANNOT_BE_REVERSED (en and
zh),
which is no longer referenced since ReversePredicateVisitor retains the NOT
instead of throwing.
---
.../it/query/IoTDBPredicateConversionTreeIT.java | 231 ++++++++++++++++++++-
.../iotdb/db/i18n/DataNodeQueryMessages.java | 2 -
.../iotdb/db/i18n/DataNodeQueryMessages.java | 2 -
.../queryengine/plan/analyze/AnalyzeVisitor.java | 20 +-
.../queryengine/plan/analyze/PredicateUtils.java | 98 +++++++--
.../queryengine/plan/analyze/TemplatedAnalyze.java | 9 +-
.../cartesian/BindSchemaForPredicateVisitor.java | 10 +
.../PredicateCanPushDownToSourceChecker.java | 8 +
.../visitor/predicate/ReversePredicateVisitor.java | 82 +++++++-
.../predicate/PredicateRemoveNotTest.java | 74 +++++++
.../expression/predicate/PredicateUtilsTest.java | 3 +
.../plan/analyze/PredicateUtilsTest.java | 70 +++++++
12 files changed, 579 insertions(+), 30 deletions(-)
diff --git
a/integration-test/src/test/java/org/apache/iotdb/db/it/query/IoTDBPredicateConversionTreeIT.java
b/integration-test/src/test/java/org/apache/iotdb/db/it/query/IoTDBPredicateConversionTreeIT.java
index afe09efb481..4ca331702ae 100644
---
a/integration-test/src/test/java/org/apache/iotdb/db/it/query/IoTDBPredicateConversionTreeIT.java
+++
b/integration-test/src/test/java/org/apache/iotdb/db/it/query/IoTDBPredicateConversionTreeIT.java
@@ -31,9 +31,18 @@ import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import java.sql.Connection;
+import java.sql.ResultSet;
import java.sql.Statement;
+import java.util.HashSet;
+import java.util.Set;
import static org.apache.iotdb.db.it.utils.TestUtils.resultSetEqualTest;
+import static org.apache.iotdb.itbase.constant.TestConstant.DEVICE;
+import static org.apache.iotdb.itbase.constant.TestConstant.TIMESTAMP_STR;
+import static org.apache.iotdb.itbase.constant.TestConstant.count;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(IoTDBTestRunner.class)
@@ -42,6 +51,8 @@ public class IoTDBPredicateConversionTreeIT {
private static final String DATABASE_NAME = "root.test_pred";
private static final String DEVICE_ID = DATABASE_NAME + ".d1";
+ private static final String NOT_DATABASE_NAME = "root.test_not_regression";
+ private static final String TEMPLATE_DATABASE_NAME =
"root.test_not_template";
private static final String[] SQLs =
new String[] {
@@ -57,7 +68,31 @@ public class IoTDBPredicateConversionTreeIT {
"INSERT INTO " + DEVICE_ID + "(time, int32_col, int64_col) VALUES(7,
null, null)",
"INSERT INTO " + DEVICE_ID + "(time, int32_col, int64_col) VALUES(8,
1000, 1000)",
"INSERT INTO " + DEVICE_ID + "(time, int32_col, int64_col) VALUES(9,
-29, -29)",
- "INSERT INTO " + DEVICE_ID + "(time, int32_col, int64_col) VALUES(10,
-30, -30)"
+ "INSERT INTO " + DEVICE_ID + "(time, int32_col, int64_col) VALUES(10,
-30, -30)",
+ "CREATE DATABASE " + NOT_DATABASE_NAME,
+ "CREATE TIMESERIES " + NOT_DATABASE_NAME + ".d1.s1 WITH
DATATYPE=INT32, ENCODING=PLAIN",
+ "CREATE TIMESERIES " + NOT_DATABASE_NAME + ".d1.s2 WITH
DATATYPE=INT32, ENCODING=PLAIN",
+ "CREATE TIMESERIES " + NOT_DATABASE_NAME + ".d1.b WITH
DATATYPE=BOOLEAN, ENCODING=PLAIN",
+ "INSERT INTO " + NOT_DATABASE_NAME + ".d1(time, s1, s2, b) VALUES(1,
1, 10, true)",
+ "INSERT INTO " + NOT_DATABASE_NAME + ".d1(time, s1, s2, b) VALUES(2,
6, 20, false)",
+ "INSERT INTO " + NOT_DATABASE_NAME + ".d1(time, s1, s2, b) VALUES(3,
3, 30, true)",
+ "INSERT INTO " + NOT_DATABASE_NAME + ".d1(time, s1, s2, b) VALUES(4,
8, 40, false)",
+ "CREATE TIMESERIES " + NOT_DATABASE_NAME + ".d2.s1 WITH
DATATYPE=INT32, ENCODING=PLAIN",
+ "INSERT INTO " + NOT_DATABASE_NAME + ".d2(time, s1) VALUES(1, 2)",
+ "INSERT INTO " + NOT_DATABASE_NAME + ".d2(time, s1) VALUES(2, 7)",
+ "CREATE TIMESERIES " + NOT_DATABASE_NAME + ".d3.x WITH
DATATYPE=DOUBLE, ENCODING=PLAIN",
+ "CREATE TIMESERIES " + NOT_DATABASE_NAME + ".d3.y WITH DATATYPE=INT32,
ENCODING=PLAIN",
+ "INSERT INTO " + NOT_DATABASE_NAME + ".d3(time, x, y) VALUES(1, NaN,
1)",
+ "INSERT INTO " + NOT_DATABASE_NAME + ".d3(time, x, y) VALUES(2, 3.0,
2)",
+ "INSERT INTO " + NOT_DATABASE_NAME + ".d3(time, x, y) VALUES(3, 10.0,
3)",
+ "CREATE DATABASE " + TEMPLATE_DATABASE_NAME,
+ "CREATE DEVICE TEMPLATE not_predicate_template ALIGNED (s1 INT64, b
BOOLEAN)",
+ "SET DEVICE TEMPLATE not_predicate_template TO " +
TEMPLATE_DATABASE_NAME + ".d1",
+ "SET DEVICE TEMPLATE not_predicate_template TO " +
TEMPLATE_DATABASE_NAME + ".d2",
+ "INSERT INTO " + TEMPLATE_DATABASE_NAME + ".d1(time, s1, b) ALIGNED
VALUES(1, 3, true)",
+ "INSERT INTO " + TEMPLATE_DATABASE_NAME + ".d1(time, s1, b) ALIGNED
VALUES(2, 8, false)",
+ "INSERT INTO " + TEMPLATE_DATABASE_NAME + ".d2(time, s1, b) ALIGNED
VALUES(1, 4, false)",
+ "INSERT INTO " + TEMPLATE_DATABASE_NAME + ".d2(time, s1, b) ALIGNED
VALUES(2, 9, true)"
};
@BeforeClass
@@ -290,4 +325,198 @@ public class IoTDBPredicateConversionTreeIT {
expectedHeaderInt64,
allInt64Rows);
}
+
+ @Test
+ public void testWhereAndHavingWithNot() {
+ String[] expectedWhereHeader = new String[] {TIMESTAMP_STR, DEVICE_ID +
".int32_col"};
+ resultSetEqualTest(
+ "SELECT int32_col FROM " + DEVICE_ID + " WHERE NOT (int32_col > 20)",
+ expectedWhereHeader,
+ new String[] {"1,20,", "9,-29,", "10,-30,"});
+ resultSetEqualTest(
+ "SELECT int32_col FROM " + DEVICE_ID + " WHERE NOT (int32_col > 20 AND
int32_col < 40)",
+ expectedWhereHeader,
+ new String[] {"1,20,", "8,1000,", "9,-29,", "10,-30,"});
+
+ String[] expectedHavingHeader = new String[] {TIMESTAMP_STR,
count(DEVICE_ID + ".int32_col")};
+ resultSetEqualTest(
+ "SELECT count(int32_col) FROM "
+ + DEVICE_ID
+ + " GROUP BY ([1,11),2ms) HAVING NOT (count(int32_col) > 1)",
+ expectedHavingHeader,
+ new String[] {"5,1,", "7,1,"});
+
+ String[] expectedWhereAlignByDeviceHeader = new String[] {TIMESTAMP_STR,
DEVICE, "int32_col"};
+ resultSetEqualTest(
+ "SELECT int32_col FROM root.test_pred.* WHERE NOT (int32_col > 20 AND
int32_col < 40) ALIGN BY DEVICE",
+ expectedWhereAlignByDeviceHeader,
+ new String[] {
+ "1,root.test_pred.d1,20,",
+ "8,root.test_pred.d1,1000,",
+ "9,root.test_pred.d1,-29,",
+ "10,root.test_pred.d1,-30,",
+ });
+
+ String[] expectedHavingAlignByDeviceHeader =
+ new String[] {TIMESTAMP_STR, DEVICE, count("int32_col")};
+ resultSetEqualTest(
+ "SELECT count(int32_col) FROM root.test_pred.* GROUP BY ([1,11),2ms)
HAVING NOT (count(int32_col) > 1) ALIGN BY DEVICE",
+ expectedHavingAlignByDeviceHeader,
+ new String[] {"5,root.test_pred.d1,1,", "7,root.test_pred.d1,1,"});
+ }
+
+ @Test
+ public void testTemplatedWhereWithNot() {
+ String[] expectedHeader = new String[] {TIMESTAMP_STR, DEVICE, "s1"};
+ resultSetEqualTest(
+ "SELECT s1 FROM " + TEMPLATE_DATABASE_NAME + ".* WHERE NOT (s1 > 5)
ALIGN BY DEVICE",
+ expectedHeader,
+ new String[] {
+ "1,root.test_not_template.d1,3,", "1,root.test_not_template.d2,4,",
+ });
+
+ String[] expectedBooleanHeader = new String[] {TIMESTAMP_STR, DEVICE,
"s1", "b"};
+ resultSetEqualTest(
+ "SELECT s1, b FROM " + TEMPLATE_DATABASE_NAME + ".* WHERE NOT b ALIGN
BY DEVICE",
+ expectedBooleanHeader,
+ new String[] {
+ "2,root.test_not_template.d1,8,false,",
"1,root.test_not_template.d2,4,false,",
+ });
+ }
+
+ @Test
+ public void testUnsupportedNotPredicate() {
+ String[] expectedHeader =
+ new String[] {TIMESTAMP_STR, NOT_DATABASE_NAME + ".d1.s1",
NOT_DATABASE_NAME + ".d1.b"};
+ resultSetEqualTest(
+ "SELECT s1, b FROM " + NOT_DATABASE_NAME + ".d1 WHERE NOT b OR s1 > 5",
+ expectedHeader,
+ new String[] {"2,6,false,", "4,8,false,"});
+ resultSetEqualTest(
+ "SELECT s1, b FROM " + NOT_DATABASE_NAME + ".d1 WHERE NOT b",
+ expectedHeader,
+ new String[] {"2,6,false,", "4,8,false,"});
+ resultSetEqualTest(
+ "SELECT s1, b FROM "
+ + NOT_DATABASE_NAME
+ + ".d1 WHERE NOT (CASE WHEN s1 > 5 THEN true ELSE false END) OR s2
> 35",
+ expectedHeader,
+ new String[] {"1,1,true,", "3,3,true,", "4,8,false,"});
+ }
+
+ @Test
+ public void testNotPredicateWithTimeAndMultipleSeries() {
+ resultSetEqualTest(
+ "SELECT s1, s2 FROM " + NOT_DATABASE_NAME + ".d1 WHERE NOT (time > 2
OR s1 > 5)",
+ new String[] {TIMESTAMP_STR, NOT_DATABASE_NAME + ".d1.s1",
NOT_DATABASE_NAME + ".d1.s2"},
+ new String[] {"1,1,10,"});
+
+ resultSetEqualTest(
+ "SELECT s1, s2 FROM "
+ + NOT_DATABASE_NAME
+ + ".d1 WHERE NOT (time > 2 OR s1 > 5) ALIGN BY DEVICE",
+ new String[] {TIMESTAMP_STR, DEVICE, "s1", "s2"},
+ new String[] {"1," + NOT_DATABASE_NAME + ".d1,1,10,"});
+ }
+
+ @Test
+ public void testNestedNotPredicate() {
+ String[] expectedHeader =
+ new String[] {TIMESTAMP_STR, NOT_DATABASE_NAME + ".d1.s1",
NOT_DATABASE_NAME + ".d1.s2"};
+ resultSetEqualTest(
+ "SELECT s1, s2 FROM "
+ + NOT_DATABASE_NAME
+ + ".d1 WHERE NOT (NOT (NOT (s1 > 5 AND s2 > 25)))",
+ expectedHeader,
+ new String[] {"1,1,10,", "2,6,20,", "3,3,30,"});
+ }
+
+ @Test
+ public void testNotPredicateWithNaN() throws Exception {
+ try (Connection connection = EnvFactory.getEnv().getConnection();
+ Statement statement = connection.createStatement()) {
+ try (ResultSet resultSet =
+ statement.executeQuery(
+ "SELECT x, y FROM " + NOT_DATABASE_NAME + ".d3 WHERE NOT (x > 5
OR y > 2)")) {
+ assertTrue(resultSet.next());
+ assertEquals(1, resultSet.getLong(1));
+ assertTrue(Double.isNaN(resultSet.getDouble(2)));
+ assertEquals(1, resultSet.getInt(3));
+ assertTrue(resultSet.next());
+ assertEquals(2, resultSet.getLong(1));
+ assertEquals(3.0, resultSet.getDouble(2), 0.0);
+ assertEquals(2, resultSet.getInt(3));
+ assertFalse(resultSet.next());
+ }
+
+ try (ResultSet resultSet =
+ statement.executeQuery(
+ "SELECT x, y FROM " + NOT_DATABASE_NAME + ".d3 WHERE NOT (x + y
> 5)")) {
+ assertTrue(resultSet.next());
+ assertEquals(1, resultSet.getLong(1));
+ assertTrue(Double.isNaN(resultSet.getDouble(2)));
+ assertTrue(resultSet.next());
+ assertEquals(2, resultSet.getLong(1));
+ assertFalse(resultSet.next());
+ }
+ }
+ }
+
+ @Test
+ public void testCompositeHavingNot() throws Exception {
+ try (Connection connection = EnvFactory.getEnv().getConnection();
+ Statement statement = connection.createStatement()) {
+ try (ResultSet resultSet =
+ statement.executeQuery(
+ "SELECT count(s1), sum(s2) FROM "
+ + NOT_DATABASE_NAME
+ + ".d1 HAVING NOT (count(s1) > 5 AND sum(s2) > 50)")) {
+ assertTrue(resultSet.next());
+ assertEquals(4, resultSet.getLong(1));
+ assertEquals(100.0, resultSet.getDouble(2), 0.0);
+ assertFalse(resultSet.next());
+ }
+
+ try (ResultSet resultSet =
+ statement.executeQuery(
+ "SELECT count(s1) FROM "
+ + NOT_DATABASE_NAME
+ + ".* GROUP BY ([1,5),10ms), LEVEL=1 "
+ + "HAVING NOT (count(s1) > 0 AND sum(s2) > 150)")) {
+ assertTrue(resultSet.next());
+ assertEquals(1, resultSet.getLong(1));
+ assertFalse(resultSet.next());
+ }
+ }
+ }
+
+ @Test
+ public void testHavingNotWithBooleanAggregation() {
+ resultSetEqualTest(
+ "SELECT count(s1) FROM "
+ + NOT_DATABASE_NAME
+ + ".d1 GROUP BY ([1,5),1ms) HAVING NOT last_value(b)",
+ new String[] {TIMESTAMP_STR, count(NOT_DATABASE_NAME + ".d1.s1")},
+ new String[] {"2,1,", "4,1,"});
+
+ resultSetEqualTest(
+ "SELECT count(s1) FROM "
+ + NOT_DATABASE_NAME
+ + ".d1 GROUP BY ([1,5),1ms) HAVING NOT last_value(b) ALIGN BY
DEVICE",
+ new String[] {TIMESTAMP_STR, DEVICE, count("s1")},
+ new String[] {"2," + NOT_DATABASE_NAME + ".d1,1,", "4," +
NOT_DATABASE_NAME + ".d1,1,"});
+ }
+
+ @Test
+ public void testTemplatedHavingNotWithBooleanAggregation() {
+ Set<String> expectedRows = new HashSet<>();
+ expectedRows.add("1," + TEMPLATE_DATABASE_NAME + ".d2,1,");
+ expectedRows.add("2," + TEMPLATE_DATABASE_NAME + ".d1,1,");
+ resultSetEqualTest(
+ "SELECT count(s1) FROM "
+ + TEMPLATE_DATABASE_NAME
+ + ".* GROUP BY ([1,3),1ms) HAVING NOT last_value(b) ALIGN BY
DEVICE",
+ TIMESTAMP_STR + "," + DEVICE + "," + count("s1") + ",",
+ expectedRows);
+ }
}
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index dabf70dd732..b8bd2a24eb2 100644
---
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -642,8 +642,6 @@ public final class DataNodeQueryMessages {
"TIMESTAMP IN-list literal can only be LongLiteral, DoubleLiteral, or
GenericLiteral; actual type: %s";
public static final String IS_NULL_CAN_BE_PUSHED_DOWN =
"IS NULL can be pushed down";
- public static final String GROUP_BY_TIME_CANNOT_BE_REVERSED =
- "GROUP BY TIME cannot be reversed";
// --- Plan / Optimization ---
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index 9addf5f954a..d24c94b9b5a 100644
---
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -622,8 +622,6 @@ public final class DataNodeQueryMessages {
"TIMESTAMP 的 IN 列表字面量只能是 LongLiteral、DoubleLiteral 或
GenericLiteral,实际类型为 %s";
public static final String IS_NULL_CAN_BE_PUSHED_DOWN =
"IS NULL 可以下推";
- public static final String GROUP_BY_TIME_CANNOT_BE_REVERSED =
- "GROUP BY TIME 不能反转";
// --- Plan / Optimization ---
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 018f171b132..6781cc019c7 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
@@ -1547,7 +1547,7 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
while (deviceIterator.hasNext()) {
PartialPath devicePath = deviceIterator.next();
Expression whereExpression =
- analyzeWhereSplitByDevice(queryStatement, devicePath, schemaTree,
queryContext);
+ analyzeWhereSplitByDevice(analysis, queryStatement, devicePath,
schemaTree, queryContext);
if (whereExpression.equals(ConstantOperand.FALSE)) {
deviceIterator.remove();
} else if (whereExpression.equals(ConstantOperand.TRUE)) {
@@ -1580,7 +1580,7 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
schemaTree,
true,
queryContext);
- Expression whereExpression =
convertConJunctionsToWhereExpression(conJunctions);
+ Expression whereExpression =
convertConJunctionsToWhereExpression(analysis, conJunctions);
if (whereExpression.equals(ConstantOperand.TRUE)) {
analysis.setWhereExpression(null);
analysis.setHasValueFilter(false);
@@ -1595,6 +1595,7 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
}
private Expression analyzeWhereSplitByDevice(
+ final Analysis analysis,
final QueryStatement queryStatement,
final PartialPath devicePath,
final ISchemaTree schemaTree,
@@ -1606,16 +1607,25 @@ public class AnalyzeVisitor extends
StatementVisitor<Analysis, MPPQueryContext>
schemaTree,
true,
queryContext);
- return convertConJunctionsToWhereExpression(conJunctions);
+ return convertConJunctionsToWhereExpression(analysis, conJunctions);
}
- private Expression convertConJunctionsToWhereExpression(List<Expression>
conJunctions) {
+ private Expression convertConJunctionsToWhereExpression(
+ Analysis analysis, List<Expression> conJunctions) {
Expression predicate =
PredicateUtils.combineConjuncts(
conJunctions.stream().distinct().collect(Collectors.toList()));
predicate = PredicateUtils.simplifyPredicate(predicate);
predicate = normalizeExpression(predicate);
- return predicate;
+ analyzeExpressionType(analysis, predicate);
+ Expression predicateWithoutNot =
+ PredicateUtils.predicateRemoveNot(predicate, analysis::getType);
+ if (predicateWithoutNot != predicate) {
+ predicateWithoutNot =
PredicateUtils.simplifyPredicate(predicateWithoutNot);
+ predicateWithoutNot = normalizeExpression(predicateWithoutNot);
+ analyzeExpressionType(analysis, predicateWithoutNot);
+ }
+ return predicateWithoutNot;
}
private void analyzeDeviceViewOutput(Analysis analysis, QueryStatement
queryStatement) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
index edf52063588..c97528739bb 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtils.java
@@ -56,6 +56,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
import java.util.stream.Collectors;
public class PredicateUtils {
@@ -120,8 +121,9 @@ public class PredicateUtils {
}
return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (predicate.getExpressionType().equals(ExpressionType.LOGIC_NOT))
{
+ Expression negatedExpression = ((UnaryExpression)
predicate).getExpression();
GlobalTimePredicateExtractionResult childResult =
- extractGlobalTimePredicate(((UnaryExpression)
predicate).getExpression());
+ extractGlobalTimePredicate(negatedExpression);
if (childResult.getGlobalTimePredicate() != null &&
!childResult.hasValueFilter()) {
return new GlobalTimePredicateExtractionResult(
ExpressionFactory.not(childResult.getGlobalTimePredicate()),
@@ -131,6 +133,14 @@ public class PredicateUtils {
// NOT(G AND R) cannot in general be represented as NOT(G) AND NOT(R). Do
// not extract a time predicate when the negated subtree contains a value
// filter.
+ //
+ // NOT(A OR B) is NOT(A) AND NOT(B) though, so every disjunct that is a
pure
+ // time predicate still bounds the time range of the whole negation, e.g.
+ // NOT (time > 2 OR s1 > 5) implies time <= 2.
+ Expression negatedTimePredicate =
extractNegatedTimePredicateFromDisjuncts(negatedExpression);
+ if (negatedTimePredicate != null) {
+ return new GlobalTimePredicateExtractionResult(negatedTimePredicate,
predicate, true);
+ }
return new GlobalTimePredicateExtractionResult(null, predicate, true);
} else if (predicate.isCompareBinaryExpression()) {
Expression leftExpression = ((BinaryExpression)
predicate).getLeftExpression();
@@ -194,6 +204,27 @@ public class PredicateUtils {
return ExpressionFactory.and(leftExpression, rightExpression);
}
+ /**
+ * Collects the negation of every disjunct that is a pure time predicate. It
is only called for
+ * the subtree negated by a {@code not()}, where De Morgan's law turns the
disjunction into a
+ * conjunction, so each collected term is a sound time bound for the whole
predicate.
+ */
+ private static Expression
extractNegatedTimePredicateFromDisjuncts(Expression predicate) {
+ if (predicate.getExpressionType().equals(ExpressionType.LOGIC_OR)) {
+ return combineConjuncts(
+ extractNegatedTimePredicateFromDisjuncts(
+ ((BinaryExpression) predicate).getLeftExpression()),
+ extractNegatedTimePredicateFromDisjuncts(
+ ((BinaryExpression) predicate).getRightExpression()),
+ null);
+ }
+ GlobalTimePredicateExtractionResult result =
extractGlobalTimePredicate(predicate);
+ if (result.getGlobalTimePredicate() != null && !result.hasValueFilter()) {
+ return ExpressionFactory.not(result.getGlobalTimePredicate());
+ }
+ return null;
+ }
+
public static final class GlobalTimePredicateExtractionResult {
private final Expression globalTimePredicate;
@@ -251,33 +282,63 @@ public class PredicateUtils {
*
* <p>eg: not(and(eq(), not(eq(y))) -> or(notEq(), eq(y))
*
- * <p>The returned predicate should have the same meaning as the original,
but without the use of
- * the not() operator.
+ * <p>The returned predicate has the same meaning as the original. A {@code
not()} is retained
+ * when the operand cannot be inverted safely, for example for
non-comparison expressions or
+ * ordered comparisons on FLOAT/DOUBLE values where NaN semantics differ.
*
* <p>See also {@link PredicateUtils#reversePredicate(Expression)}, which is
used to do the
* inversion.
*
* @param predicate the predicate to remove not() from
- * @return the predicate with all not() operators removed
+ * @return the predicate with safely invertible not() operators removed
*/
public static Expression predicateRemoveNot(Expression predicate) {
+ return predicateRemoveNot(predicate, null);
+ }
+
+ /**
+ * @param predicate the predicate to remove not() from
+ * @return the predicate with safely invertible not() operators removed. The
original instance is
+ * returned when no not() could be removed, so callers can detect that
nothing changed.
+ */
+ public static Expression predicateRemoveNot(
+ Expression predicate, Function<Expression, TSDataType> typeProvider) {
if (predicate.getExpressionType().equals(ExpressionType.LOGIC_AND)) {
- return ExpressionFactory.and(
- predicateRemoveNot(((BinaryExpression)
predicate).getLeftExpression()),
- predicateRemoveNot(((BinaryExpression)
predicate).getRightExpression()));
+ Expression leftExpression =
+ predicateRemoveNot(((BinaryExpression)
predicate).getLeftExpression(), typeProvider);
+ Expression rightExpression =
+ predicateRemoveNot(((BinaryExpression)
predicate).getRightExpression(), typeProvider);
+ if (leftExpression == ((BinaryExpression) predicate).getLeftExpression()
+ && rightExpression == ((BinaryExpression)
predicate).getRightExpression()) {
+ return predicate;
+ }
+ return ExpressionFactory.and(leftExpression, rightExpression);
} else if (predicate.getExpressionType().equals(ExpressionType.LOGIC_OR)) {
- return ExpressionFactory.or(
- predicateRemoveNot(((BinaryExpression)
predicate).getLeftExpression()),
- predicateRemoveNot(((BinaryExpression)
predicate).getRightExpression()));
+ Expression leftExpression =
+ predicateRemoveNot(((BinaryExpression)
predicate).getLeftExpression(), typeProvider);
+ Expression rightExpression =
+ predicateRemoveNot(((BinaryExpression)
predicate).getRightExpression(), typeProvider);
+ if (leftExpression == ((BinaryExpression) predicate).getLeftExpression()
+ && rightExpression == ((BinaryExpression)
predicate).getRightExpression()) {
+ return predicate;
+ }
+ return ExpressionFactory.or(leftExpression, rightExpression);
} else if (predicate.getExpressionType().equals(ExpressionType.LOGIC_NOT))
{
- return reversePredicate(((LogicNotExpression)
predicate).getExpression());
+ Expression innerExpression = ((LogicNotExpression)
predicate).getExpression();
+ Expression reversedExpression = reversePredicate(innerExpression,
typeProvider);
+ if
(reversedExpression.getExpressionType().equals(ExpressionType.LOGIC_NOT)
+ && ((LogicNotExpression) reversedExpression).getExpression() ==
innerExpression) {
+ // The operand cannot be inverted safely, so the not() is kept as is.
+ return predicate;
+ }
+ return reversedExpression;
}
return predicate;
}
/**
* Converts a predicate to its logical inverse. The returned predicate
should be equivalent to
- * not(p), but without the use of a not() operator.
+ * not(p). A {@code not()} is retained when the inverse cannot be
represented safely.
*
* <p>See also {@link PredicateUtils#predicateRemoveNot(Expression)}, which
can remove the use of
* all not() operators without inverting the overall predicate.
@@ -286,7 +347,12 @@ public class PredicateUtils {
* @return the predicate after reversing
*/
public static Expression reversePredicate(Expression predicate) {
- return new ReversePredicateVisitor().process(predicate, null);
+ return reversePredicate(predicate, null);
+ }
+
+ public static Expression reversePredicate(
+ Expression predicate, Function<Expression, TSDataType> typeProvider) {
+ return new ReversePredicateVisitor(typeProvider).process(predicate, null);
}
/**
@@ -418,7 +484,8 @@ public class PredicateUtils {
/**
* Extract the source symbol (full path for non-aligned path, device path
for aligned path) from
- * the given predicate. If the predicate contains multiple source symbols,
return null.
+ * the given predicate. If the predicate contains no source symbols or
multiple source symbols,
+ * return null.
*
* @param predicate given predicate
* @return the source symbol extracted from the given predicate
@@ -429,6 +496,9 @@ public class PredicateUtils {
sourceExpressions.stream()
.map(expression -> ((TimeSeriesOperand) expression).getPath())
.collect(Collectors.toSet());
+ if (sourcePaths.isEmpty()) {
+ return null;
+ }
Iterator<PartialPath> pathIterator = sourcePaths.iterator();
MeasurementPath firstPath = (MeasurementPath) pathIterator.next();
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java
index 350cc5d090e..c3a3b421d93 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/TemplatedAnalyze.java
@@ -272,12 +272,17 @@ public class TemplatedAnalyze {
analysis.getDeviceTemplate().getSchemaMap());
wherePredicate = PredicateUtils.simplifyPredicate(wherePredicate);
if (!wherePredicate.equals(ConstantOperand.TRUE)) {
- analysis.setWhereExpression(wherePredicate);
-
TSDataType outputType = analyzeExpressionForTemplatedQuery(analysis,
wherePredicate);
if (outputType != TSDataType.BOOLEAN) {
throw new SemanticException(String.format(WHERE_WRONG_TYPE_ERROR_MSG,
outputType));
}
+ Expression predicateWithoutNot =
+ PredicateUtils.predicateRemoveNot(wherePredicate, analysis::getType);
+ if (predicateWithoutNot != wherePredicate) {
+ predicateWithoutNot =
PredicateUtils.simplifyPredicate(predicateWithoutNot);
+ analyzeExpressionForTemplatedQuery(analysis, predicateWithoutNot);
+ }
+ analysis.setWhereExpression(predicateWithoutNot);
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/cartesian/BindSchemaForPredicateVisitor.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/cartesian/BindSchemaForPredicateVisitor.java
index ae60ec381df..b2c1ff24c00 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/cartesian/BindSchemaForPredicateVisitor.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/cartesian/BindSchemaForPredicateVisitor.java
@@ -33,6 +33,7 @@ import
org.apache.iotdb.db.queryengine.plan.expression.leaf.NullOperand;
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.UnaryExpression;
import org.apache.tsfile.external.commons.lang3.Validate;
@@ -47,6 +48,7 @@ import static
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.carte
import static
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructBinaryExpressionsWithMemoryCheck;
import static
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructFunctionExpressionsWithMemoryCheck;
import static
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructTimeSeriesOperandsWithMemoryCheck;
+import static
org.apache.iotdb.db.queryengine.plan.analyze.ExpressionUtils.reconstructUnaryExpressionsWithMemoryCheck;
import static
org.apache.iotdb.db.queryengine.plan.expression.visitor.cartesian.BindSchemaForExpressionVisitor.transformViewPath;
import static
org.apache.iotdb.db.utils.TypeInferenceUtils.bindTypeForBuiltinAggregationNonSeriesInputExpressions;
@@ -69,6 +71,14 @@ public class BindSchemaForPredicateVisitor
binaryExpression, leftExpressions, rightExpressions,
context.getQueryContext());
}
+ @Override
+ public List<Expression> visitUnaryExpression(UnaryExpression
unaryExpression, Context context) {
+ List<Expression> childExpressions =
+ process(unaryExpression.getExpression(), context.notRootClone());
+ return reconstructUnaryExpressionsWithMemoryCheck(
+ unaryExpression, childExpressions, context.getQueryContext());
+ }
+
@Override
public List<Expression> visitFunctionExpression(FunctionExpression
predicate, Context context) {
if (COUNT_TIME.equalsIgnoreCase(predicate.getFunctionName())) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/logical/PredicateCanPushDownToSourceChecker.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/logical/PredicateCanPushDownToSourceChecker.java
index 3a7c1ddbb37..dac1e59b5d9 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/logical/PredicateCanPushDownToSourceChecker.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/logical/PredicateCanPushDownToSourceChecker.java
@@ -21,6 +21,7 @@ package
org.apache.iotdb.db.queryengine.plan.expression.visitor.logical;
import org.apache.iotdb.db.queryengine.plan.expression.leaf.LeafOperand;
import org.apache.iotdb.db.queryengine.plan.expression.unary.IsNullExpression;
+import
org.apache.iotdb.db.queryengine.plan.expression.unary.LogicNotExpression;
public class PredicateCanPushDownToSourceChecker extends
LogicalAndVisitor<Void> {
@@ -37,4 +38,11 @@ public class PredicateCanPushDownToSourceChecker extends
LogicalAndVisitor<Void>
public Boolean visitLeafOperand(LeafOperand leafOperand, Void context) {
return Boolean.TRUE;
}
+
+ @Override
+ public Boolean visitLogicNotExpression(LogicNotExpression
logicNotExpression, Void context) {
+ // TsFile filters cannot represent NOT. Predicates that cannot be inverted
during analysis must
+ // be evaluated by FilterOperator instead of being pushed into a scan.
+ return Boolean.FALSE;
+ }
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/ReversePredicateVisitor.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/ReversePredicateVisitor.java
index d0824863557..3659c13d86b 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/ReversePredicateVisitor.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/expression/visitor/predicate/ReversePredicateVisitor.java
@@ -19,9 +19,11 @@
package org.apache.iotdb.db.queryengine.plan.expression.visitor.predicate;
-import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
+import org.apache.iotdb.db.queryengine.plan.analyze.PredicateUtils;
import org.apache.iotdb.db.queryengine.plan.expression.Expression;
import org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory;
+import org.apache.iotdb.db.queryengine.plan.expression.ExpressionType;
+import org.apache.iotdb.db.queryengine.plan.expression.binary.BinaryExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.binary.EqualToExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.binary.GreaterEqualExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.binary.GreaterThanExpression;
@@ -30,6 +32,7 @@ import
org.apache.iotdb.db.queryengine.plan.expression.binary.LessThanExpression
import
org.apache.iotdb.db.queryengine.plan.expression.binary.LogicAndExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.binary.LogicOrExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.binary.NonEqualExpression;
+import org.apache.iotdb.db.queryengine.plan.expression.leaf.ConstantOperand;
import
org.apache.iotdb.db.queryengine.plan.expression.other.GroupByTimeExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.ternary.BetweenExpression;
import org.apache.iotdb.db.queryengine.plan.expression.unary.InExpression;
@@ -38,11 +41,25 @@ import
org.apache.iotdb.db.queryengine.plan.expression.unary.LikeExpression;
import
org.apache.iotdb.db.queryengine.plan.expression.unary.LogicNotExpression;
import org.apache.iotdb.db.queryengine.plan.expression.unary.RegularExpression;
+import org.apache.tsfile.enums.TSDataType;
+
+import java.util.function.Function;
+
public class ReversePredicateVisitor extends PredicateVisitor<Expression,
Void> {
+ private final Function<Expression, TSDataType> typeProvider;
+
+ public ReversePredicateVisitor() {
+ this(null);
+ }
+
+ public ReversePredicateVisitor(Function<Expression, TSDataType>
typeProvider) {
+ this.typeProvider = typeProvider;
+ }
+
@Override
public Expression visitExpression(Expression expression, Void context) {
- throw new UnsupportedOperationException();
+ return new LogicNotExpression(expression);
}
@Override
@@ -76,7 +93,7 @@ public class ReversePredicateVisitor extends
PredicateVisitor<Expression, Void>
@Override
public Expression visitLogicNotExpression(LogicNotExpression
logicNotExpression, Void context) {
- return logicNotExpression.getExpression();
+ return
PredicateUtils.predicateRemoveNot(logicNotExpression.getExpression(),
typeProvider);
}
@Override
@@ -108,6 +125,9 @@ public class ReversePredicateVisitor extends
PredicateVisitor<Expression, Void>
@Override
public Expression visitGreaterThanExpression(
GreaterThanExpression greaterThanExpression, Void context) {
+ if (!canSafelyInvertOrderedComparison(greaterThanExpression)) {
+ return new LogicNotExpression(greaterThanExpression);
+ }
return new LessEqualExpression(
greaterThanExpression.getLeftExpression(),
greaterThanExpression.getRightExpression());
}
@@ -115,12 +135,18 @@ public class ReversePredicateVisitor extends
PredicateVisitor<Expression, Void>
@Override
public Expression visitGreaterEqualExpression(
GreaterEqualExpression greaterEqualExpression, Void context) {
+ if (!canSafelyInvertOrderedComparison(greaterEqualExpression)) {
+ return new LogicNotExpression(greaterEqualExpression);
+ }
return new LessThanExpression(
greaterEqualExpression.getLeftExpression(),
greaterEqualExpression.getRightExpression());
}
@Override
public Expression visitLessThanExpression(LessThanExpression
lessThanExpression, Void context) {
+ if (!canSafelyInvertOrderedComparison(lessThanExpression)) {
+ return new LogicNotExpression(lessThanExpression);
+ }
return new GreaterEqualExpression(
lessThanExpression.getLeftExpression(),
lessThanExpression.getRightExpression());
}
@@ -128,12 +154,18 @@ public class ReversePredicateVisitor extends
PredicateVisitor<Expression, Void>
@Override
public Expression visitLessEqualExpression(
LessEqualExpression lessEqualExpression, Void context) {
+ if (!canSafelyInvertOrderedComparison(lessEqualExpression)) {
+ return new LogicNotExpression(lessEqualExpression);
+ }
return new GreaterThanExpression(
lessEqualExpression.getLeftExpression(),
lessEqualExpression.getRightExpression());
}
@Override
public Expression visitBetweenExpression(BetweenExpression
betweenExpression, Void context) {
+ if (!canSafelyInvertBetween(betweenExpression)) {
+ return new LogicNotExpression(betweenExpression);
+ }
return new BetweenExpression(
betweenExpression.getFirstExpression(),
betweenExpression.getSecondExpression(),
@@ -144,6 +176,48 @@ public class ReversePredicateVisitor extends
PredicateVisitor<Expression, Void>
@Override
public Expression visitGroupByTimeExpression(
GroupByTimeExpression groupByTimeExpression, Void context) {
- throw new
UnsupportedOperationException(DataNodeQueryMessages.GROUP_BY_TIME_CANNOT_BE_REVERSED);
+ return new LogicNotExpression(groupByTimeExpression);
+ }
+
+ private boolean canSafelyInvertOrderedComparison(Expression
comparisonExpression) {
+ BinaryExpression binaryExpression = (BinaryExpression)
comparisonExpression;
+ TSDataType leftType = getType(binaryExpression.getLeftExpression());
+ TSDataType rightType = getType(binaryExpression.getRightExpression());
+ return leftType != null
+ && rightType != null
+ && !isFloatingPoint(leftType)
+ && !isFloatingPoint(rightType);
+ }
+
+ private boolean canSafelyInvertBetween(BetweenExpression betweenExpression) {
+ TSDataType firstType = getType(betweenExpression.getFirstExpression());
+ TSDataType secondType = getType(betweenExpression.getSecondExpression());
+ TSDataType thirdType = getType(betweenExpression.getThirdExpression());
+ return firstType != null
+ && secondType != null
+ && thirdType != null
+ && !isFloatingPoint(firstType)
+ && !isFloatingPoint(secondType)
+ && !isFloatingPoint(thirdType);
+ }
+
+ private boolean isFloatingPoint(TSDataType type) {
+ return type == TSDataType.FLOAT || type == TSDataType.DOUBLE;
+ }
+
+ private TSDataType getType(Expression expression) {
+ if (typeProvider != null) {
+ TSDataType type = typeProvider.apply(expression);
+ if (type != null) {
+ return type;
+ }
+ }
+ if (expression.getExpressionType().equals(ExpressionType.TIMESTAMP)) {
+ return TSDataType.INT64;
+ }
+ if (expression.getExpressionType().equals(ExpressionType.CONSTANT)) {
+ return ((ConstantOperand) expression).getDataType();
+ }
+ return null;
}
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/expression/predicate/PredicateRemoveNotTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/expression/predicate/PredicateRemoveNotTest.java
index 6171cc48294..e20420411fa 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/expression/predicate/PredicateRemoveNotTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/expression/predicate/PredicateRemoveNotTest.java
@@ -19,17 +19,25 @@
package org.apache.iotdb.db.queryengine.expression.predicate;
+import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.db.queryengine.plan.expression.Expression;
+import org.apache.iotdb.db.queryengine.plan.expression.leaf.ConstantOperand;
+import org.apache.iotdb.db.queryengine.plan.expression.leaf.TimeSeriesOperand;
+
+import org.apache.tsfile.enums.TSDataType;
import org.junit.Assert;
import org.junit.Test;
import java.util.Arrays;
import java.util.LinkedHashSet;
+import java.util.function.Function;
import static
org.apache.iotdb.db.queryengine.plan.analyze.PredicateUtils.predicateRemoveNot;
import static
org.apache.iotdb.db.queryengine.plan.analyze.PredicateUtils.reversePredicate;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.and;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.between;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.eq;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.function;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.gt;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.gte;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.in;
@@ -42,9 +50,20 @@ import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.notIn;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.or;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.time;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.timeSeries;
public class PredicateRemoveNotTest {
+ private static Function<Expression, TSDataType> typeProvider(TSDataType
seriesType) {
+ return expression ->
+ expression instanceof TimeSeriesOperand
+ ? seriesType
+ : ((ConstantOperand) expression).getDataType();
+ }
+
+ private static final Function<Expression, TSDataType> DOUBLE_TYPE_PROVIDER =
+ typeProvider(TSDataType.DOUBLE);
+
@Test
public void testReverse() {
// test comparison
@@ -92,4 +111,59 @@ public class PredicateRemoveNotTest {
and(gt(time(), longValue(1)), lte(time(), longValue(1))),
predicateRemoveNot(and(not(lte(time(), longValue(1))), not(gt(time(),
longValue(1))))));
}
+
+ @Test
+ public void testRemoveNotKeepsUnsupportedNot() throws IllegalPathException {
+ Expression booleanSeries = timeSeries("root.sg.d.s1");
+ Expression predicate = not(booleanSeries);
+
+ // The kept not() must be the original instance so callers can detect that
nothing changed.
+ Assert.assertSame(predicate, predicateRemoveNot(predicate));
+ }
+
+ @Test
+ public void testRemoveNotKeepsFunctionPredicate() throws
IllegalPathException {
+ Expression predicate = not(function("udf", timeSeries("root.sg.d.s1")));
+
+ Assert.assertSame(predicate, predicateRemoveNot(predicate));
+ }
+
+ @Test
+ public void testRemoveNotKeepsBetweenOnDouble() throws IllegalPathException {
+ Expression series = timeSeries("root.sg.d.s1");
+ Expression predicate =
+ not(
+ between(
+ series,
+ new ConstantOperand(TSDataType.DOUBLE, "1.0"),
+ new ConstantOperand(TSDataType.DOUBLE, "5.0")));
+
+ Assert.assertSame(predicate, predicateRemoveNot(predicate,
DOUBLE_TYPE_PROVIDER));
+ }
+
+ @Test
+ public void testRemoveNotKeepsComparisonWithUnknownType() throws
IllegalPathException {
+ Expression predicate = not(gt(timeSeries("root.sg.d.s1"), longValue(1)));
+
+ // An operand of unknown type cannot be inverted safely either.
+ Assert.assertSame(predicate, predicateRemoveNot(predicate, expression ->
null));
+ }
+
+ @Test
+ public void testRemoveNotHandlesNestedNot() {
+ Expression predicate = not(not(not(and(gt(time(), longValue(1)),
lt(time(), longValue(10))))));
+ Expression expected = or(lte(time(), longValue(1)), gte(time(),
longValue(10)));
+
+ Assert.assertEquals(expected, predicateRemoveNot(predicate));
+ }
+
+ @Test
+ public void testRemoveNotUsesTypeToPreserveNaNSemantics() throws
IllegalPathException {
+ Expression series = timeSeries("root.sg.d.s1");
+ Expression predicate = not(gt(series, new
ConstantOperand(TSDataType.DOUBLE, "5.0")));
+
+ Expression result = predicateRemoveNot(predicate, DOUBLE_TYPE_PROVIDER);
+
+ Assert.assertSame(predicate, result);
+ }
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/expression/predicate/PredicateUtilsTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/expression/predicate/PredicateUtilsTest.java
index 8cd0d106047..de47e754689 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/expression/predicate/PredicateUtilsTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/expression/predicate/PredicateUtilsTest.java
@@ -28,6 +28,8 @@ import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.and;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.gt;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.intValue;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.longValue;
+import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.time;
import static
org.apache.iotdb.db.queryengine.plan.expression.ExpressionFactory.timeSeries;
import static
org.apache.iotdb.db.queryengine.plan.optimization.OptimizationTestUtil.schemaMap;
@@ -35,6 +37,7 @@ public class PredicateUtilsTest {
@Test
public void testExtractPredicateSourceSymbol() {
+ Assert.assertNull(PredicateUtils.extractPredicateSourceSymbol(gt(time(),
longValue(10))));
Assert.assertEquals(
schemaMap.get("root.sg.d1.s1"),
PredicateUtils.extractPredicateSourceSymbol(
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
index bc9d252d30e..1c7451e06c3 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/PredicateUtilsTest.java
@@ -132,6 +132,69 @@ public class PredicateUtilsTest {
assertSame(timePredicate, predicate.getExpression());
}
+ @Test
+ public void testMixedNotOrExtractsNegatedTimePredicate() throws
IllegalPathException {
+ Expression timePredicate = gt(time(), longValue(1));
+ Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+ BinaryExpression child = or(timePredicate, valuePredicate);
+ LogicNotExpression predicate = not(child);
+
+ GlobalTimePredicateExtractionResult result =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+
+ // NOT(time > 1 OR s1 > 1) implies time <= 1
+ assertEquals(not(timePredicate), result.getGlobalTimePredicate());
+ assertSame(predicate, result.getResidualPredicate());
+ assertTrue(result.hasValueFilter());
+ assertSame(child, predicate.getExpression());
+ }
+
+ @Test
+ public void testNestedMixedNotOrExtractsAllNegatedTimePredicates() throws
IllegalPathException {
+ Expression firstTimePredicate = gt(time(), longValue(1));
+ Expression secondTimePredicate = lt(time(), longValue(10));
+ Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+ Expression predicate = not(or(or(firstTimePredicate, valuePredicate),
secondTimePredicate));
+
+ GlobalTimePredicateExtractionResult result =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+
+ assertEquals(
+ and(not(firstTimePredicate), not(secondTimePredicate)),
result.getGlobalTimePredicate());
+ assertSame(predicate, result.getResidualPredicate());
+ assertTrue(result.hasValueFilter());
+ }
+
+ @Test
+ public void testNegatedOrWithoutPureTimeDisjunctDoesNotExtractTimePredicate()
+ throws IllegalPathException {
+ Expression firstValuePredicate = gt(timeSeries("root.sg.d.s1"),
intValue("1"));
+ Expression secondValuePredicate = gt(timeSeries("root.sg.d.s2"),
intValue("2"));
+ LogicNotExpression predicate = not(or(firstValuePredicate,
secondValuePredicate));
+
+ GlobalTimePredicateExtractionResult result =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+
+ assertNull(result.getGlobalTimePredicate());
+ assertSame(predicate, result.getResidualPredicate());
+ assertTrue(result.hasValueFilter());
+ }
+
+ @Test
+ public void testDoubleNegatedOrDoesNotExtractTimePredicate() throws
IllegalPathException {
+ Expression timePredicate = gt(time(), longValue(1));
+ Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
+ LogicNotExpression predicate = not(not(or(timePredicate, valuePredicate)));
+
+ GlobalTimePredicateExtractionResult result =
+ PredicateUtils.extractGlobalTimePredicate(predicate);
+
+ // NOT NOT (time > 1 OR s1 > 1) is a disjunction, which does not bound the
time range
+ assertNull(result.getGlobalTimePredicate());
+ assertSame(predicate, result.getResidualPredicate());
+ assertTrue(result.hasValueFilter());
+ }
+
@Test
public void testSimplifierUsesCopyOnWriteForUnaryExpression() throws
IllegalPathException {
Expression valuePredicate = gt(timeSeries("root.sg.d.s1"), intValue("1"));
@@ -169,4 +232,11 @@ public class PredicateUtilsTest {
assertSame(predicate, PredicateUtils.simplifyPredicate(predicate));
}
+
+ @Test
+ public void testLogicNotCannotPushDownToSource() throws IllegalPathException
{
+ Expression predicate = not(gt(timeSeries("root.sg.d.s1"), intValue("1")));
+
+ assertFalse(PredicateUtils.predicateCanPushDownToSource(predicate));
+ }
}