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 afe09efb48103..4ca331702aefc 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.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 void testDoubleLiteralOverflow() { 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 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 d3282b7895321..6df33e5105f53 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 6a50a255cf60b..2f9ab1f0fc42c 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 018f171b13252..6781cc019c78a 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 @@ private void analyzeDeviceToWhere( 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 @@ private void analyzeWhere( 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 @@ private void analyzeWhere( } private Expression analyzeWhereSplitByDevice( + final Analysis analysis, final QueryStatement queryStatement, final PartialPath devicePath, final ISchemaTree schemaTree, @@ -1606,16 +1607,25 @@ private Expression analyzeWhereSplitByDevice( schemaTree, true, queryContext); - return convertConJunctionsToWhereExpression(conJunctions); + return convertConJunctionsToWhereExpression(analysis, conJunctions); } - private Expression convertConJunctionsToWhereExpression(List conJunctions) { + private Expression convertConJunctionsToWhereExpression( + Analysis analysis, List 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 edf52063588a5..c97528739bbb7 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.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 static GlobalTimePredicateExtractionResult extractGlobalTimePredicate( } 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 static GlobalTimePredicateExtractionResult extractGlobalTimePredicate( // 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 @@ private static Expression combineConjuncts( 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 static boolean checkIfTimeFilterExist(Expression predicate) { * *

eg: not(and(eq(), not(eq(y))) -> or(notEq(), eq(y)) * - *

The returned predicate should have the same meaning as the original, but without the use of - * the not() operator. + *

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. * *

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 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. * *

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 static Expression predicateRemoveNot(Expression predicate) { * @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 typeProvider) { + return new ReversePredicateVisitor(typeProvider).process(predicate, null); } /** @@ -418,7 +484,8 @@ private static void extractConjuncts(Expression predicate, Set conju /** * 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 static PartialPath extractPredicateSourceSymbol(Expression predicate) { sourceExpressions.stream() .map(expression -> ((TimeSeriesOperand) expression).getPath()) .collect(Collectors.toSet()); + if (sourcePaths.isEmpty()) { + return null; + } Iterator 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 350cc5d090ea5..c3a3b421d9314 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 @@ static void analyzeDeviceToWhere(Analysis analysis, QueryStatement queryStatemen 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 ae60ec381df24..b2c1ff24c00e7 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.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.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 List visitBinaryExpression( binaryExpression, leftExpressions, rightExpressions, context.getQueryContext()); } + @Override + public List visitUnaryExpression(UnaryExpression unaryExpression, Context context) { + List childExpressions = + process(unaryExpression.getExpression(), context.notRootClone()); + return reconstructUnaryExpressionsWithMemoryCheck( + unaryExpression, childExpressions, context.getQueryContext()); + } + @Override public List 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 3a7c1ddbb37fe..dac1e59b5d943 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 @@ 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 { @@ -37,4 +38,11 @@ public Boolean visitIsNullExpression(IsNullExpression isNullExpression, Void con 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 d082486355787..3659c13d86bf2 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.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.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 { + private final Function typeProvider; + + public ReversePredicateVisitor() { + this(null); + } + + public ReversePredicateVisitor(Function 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 Expression visitRegularExpression(RegularExpression regularExpression, Vo @Override public Expression visitLogicNotExpression(LogicNotExpression logicNotExpression, Void context) { - return logicNotExpression.getExpression(); + return PredicateUtils.predicateRemoveNot(logicNotExpression.getExpression(), typeProvider); } @Override @@ -108,6 +125,9 @@ public Expression visitNonEqualExpression(NonEqualExpression nonEqualExpression, @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 Expression visitGreaterThanExpression( @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 Expression visitLessThanExpression(LessThanExpression lessThanExpression, @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 Expression visitBetweenExpression(BetweenExpression betweenExpression, Vo @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 6171cc4829473..e20420411fa0a 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.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 typeProvider(TSDataType seriesType) { + return expression -> + expression instanceof TimeSeriesOperand + ? seriesType + : ((ConstantOperand) expression).getDataType(); + } + + private static final Function DOUBLE_TYPE_PROVIDER = + typeProvider(TSDataType.DOUBLE); + @Test public void testReverse() { // test comparison @@ -92,4 +111,59 @@ public void testRemoveNot() { 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 8cd0d106047d5..de47e754689f6 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.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 bc9d252d30e32..1c7451e06c3ee 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 void testPureTimeNotCanBeExtracted() { 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 void testSimplifierReusesUnchangedExpression() throws IllegalPathExceptio 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)); + } }