This is an automated email from the ASF dual-hosted git repository.
ppa pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new b8e9c98465 IGNITE-21330 Sql. Support index scan for OR operator with
dynamic parameters (#3407)
b8e9c98465 is described below
commit b8e9c98465201c3449dd2aa25e3a07c389f01112
Author: Pavel Pereslegin <[email protected]>
AuthorDate: Thu Mar 28 13:21:55 2024 +0300
IGNITE-21330 Sql. Support index scan for OR operator with dynamic
parameters (#3407)
---
.../engine/ItSecondaryIndexMultiRangeScanTest.java | 369 +++++++++++++++++++++
.../internal/sql/engine/ItSecondaryIndexTest.java | 4 +-
.../sql/engine/datatypes/uuid/ItUuidIndexTest.java | 7 -
.../sql/engine/exec/exp/ExpressionFactoryImpl.java | 196 +++++++++--
.../ignite/internal/sql/engine/util/RexUtils.java | 146 ++++++--
.../engine/exec/exp/ExpressionFactoryImplTest.java | 259 ++++++++++++++-
.../planner/IndexSearchBoundsPlannerTest.java | 33 +-
7 files changed, 935 insertions(+), 79 deletions(-)
diff --git
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexMultiRangeScanTest.java
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexMultiRangeScanTest.java
new file mode 100644
index 0000000000..9d6a5537af
--- /dev/null
+++
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexMultiRangeScanTest.java
@@ -0,0 +1,369 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.ignite.internal.sql.BaseSqlIntegrationTest;
+import org.apache.ignite.internal.sql.engine.util.QueryChecker;
+import org.apache.ignite.internal.testframework.WithSystemProperty;
+import org.hamcrest.CoreMatchers;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests index multi-range scans (with SEARCH/SARG operator or with dynamic
parameters).
+ */
+@WithSystemProperty(key = "IMPLICIT_PK_ENABLED", value = "true")
+public class ItSecondaryIndexMultiRangeScanTest extends BaseSqlIntegrationTest
{
+ private static final String TEST_DISPLAY_NAME = "dynamicParams={0},
direction={1}";
+
+ @Override
+ protected int initialNodes() {
+ return 1;
+ }
+
+ /** {@inheritDoc} */
+ @BeforeAll
+ protected void prepare() {
+ sql("CREATE TABLE test_asc (c1 INTEGER, c2 VARCHAR, c3 INTEGER)");
+ sql("CREATE INDEX c1c2c3_asc ON test_asc(c1 ASC, c2 ASC, c3 ASC)");
+ sql("CREATE TABLE test_desc (c1 INTEGER, c2 VARCHAR, c3 INTEGER)");
+ sql("CREATE INDEX c1c2c3_desc ON test_desc(c1 DESC, c2 DESC, c3
DESC)");
+
+ for (String tbl : List.of("test_asc", "test_desc")) {
+ sql("INSERT INTO " + tbl + "(c1, c2, c3) VALUES (0, null, 0)");
+
+ for (int i = 0; i <= 5; i++) {
+ for (int j = 1; j <= 5; j++) {
+ sql("INSERT INTO " + tbl + "(c1, c2, c3) VALUES (?, ?,
?)", i == 0 ? null : i, Integer.toString(j), i * j);
+ }
+ }
+ }
+ }
+
+ @ParameterizedTest(name = TEST_DISPLAY_NAME)
+ @MethodSource("testParameters")
+ public void testIn(boolean useDynamicParameters, String direction) {
+ assertCondition(direction, useDynamicParameters,
+ "c1 IN (%s, %s) AND c2 IN (%s, %s) AND c3 IN (%s, %s)",
+ 2, 3, "2", "3", 6, 9)
+ .returns(2, "3", 6)
+ .returns(3, "2", 6)
+ .returns(3, "3", 9)
+ .check();
+
+ assertCondition(direction, useDynamicParameters,
+ "(c1 = %s OR c1 IS NULL) AND c2 IN (%s, %s) AND c3 IN (%s,
%s)",
+ 2, "2", "3", 0, 6)
+ .returns(null, "2", 0)
+ .returns(null, "3", 0)
+ .returns(2, "3", 6)
+ .check();
+ }
+
+ @ParameterizedTest(name = TEST_DISPLAY_NAME)
+ @MethodSource("testParameters")
+ public void testRange(boolean useDynamicParameters, String direction) {
+ assertCondition(direction, useDynamicParameters,
+ "((c1 > %s AND c1 < %s) OR (c1 > %s AND c1 < %s)) AND c2 > %s
AND c2 < %s",
+ 1, 3, 3, 5, "2", "5")
+ .returns(2, "3", 6)
+ .returns(2, "4", 8)
+ .returns(4, "3", 12)
+ .returns(4, "4", 16)
+ .check();
+
+ assertCondition(direction, useDynamicParameters,
+ "c1 IN (%s, %s) AND ((c2 >= %s AND c2 < %s) OR (c2 > %s AND c1
<= %s))",
+ 1, 2, "2", "3", "4", 5)
+ .returns(1, "2", 2)
+ .returns(1, "5", 5)
+ .returns(2, "2", 4)
+ .returns(2, "5", 10)
+ .check();
+
+ assertCondition(direction, useDynamicParameters,
+ "c1 IN (%s, %s) AND (c2 < %s OR (c2 >= %s AND c2 < %s)
OR (c2 > %s AND c2 < %s) OR c2 > %s)",
+ 1, 2, "1", "2", "3", "2", "4", "5")
+ .returns(1, "2", 2)
+ .returns(1, "3", 3)
+ .returns(2, "2", 4)
+ .returns(2, "3", 6)
+ .check();
+
+ assertCondition(direction, useDynamicParameters,
+ "c1 = %s AND c2 > %s AND c3 IN (%s, %s)",
+ 4, "3", 16, 20)
+ .returns(4, "4", 16)
+ .returns(4, "5", 20)
+ .check();
+
+ assertCondition(direction, useDynamicParameters,
+ "(c1 < %s OR c1 >= %s) AND c2 = c1",
+ 2, 4)
+ .returns(1, "1", 1)
+ .returns(4, "4", 16)
+ .returns(5, "5", 25)
+ .check();
+ }
+
+ @ParameterizedTest(name = TEST_DISPLAY_NAME)
+ @MethodSource("testParameters")
+ public void testNulls(boolean useDynamicParameters, String direction) {
+ assertCondition(direction, useDynamicParameters, "c1 IS NULL AND c2 <=
%s", "1")
+ .returns(null, "1", 0)
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "(c1 IS NULL OR c1 =
%s) AND c2 = %s AND c3 in (%s, %s)",
+ 3, "1", 0, 3)
+ .returns(null, "1", 0)
+ .returns(3, "1", 3)
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "(c1 IS NULL OR c1 <
%s) AND c2 = %s AND c3 in (%s, %s)",
+ 3, "1", 0, 1)
+ .returns(null, "1", 0)
+ .returns(1, "1", 1)
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "(c1 IS NULL OR c1 >
%s) AND c2 = %s AND c3 in (%s, %s)",
+ 3, "5", 0, 25)
+ .returns(null, "5", 0)
+ .returns(5, "5", 25)
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "c1 IS NOT NULL AND
c2 IS NULL")
+ .returns(0, null, 0)
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "c1 IN(NULL, %s) AND
c2 = %s", 1, "1")
+ .returns(1, "1", 1)
+ .check();
+ }
+
+ /** Tests not supported range index scan conditions. */
+ @ParameterizedTest(name = "dynamicParams={0}")
+ @ValueSource(booleans = {true, false})
+ public void testNot(boolean useDynamicParameters) {
+ assertQuery(useDynamicParameters, "SELECT * FROM test_asc WHERE c1 <>
%s AND c3 = %s", 1, 6)
+ .matches(QueryChecker.containsTableScan("PUBLIC", "TEST_ASC"))
// Can't use index scan.
+ .returns(2, "3", 6)
+ .returns(3, "2", 6)
+ .check();
+
+ assertQuery(useDynamicParameters, "SELECT * FROM test_asc WHERE c1 NOT
IN (%s, %s, %s) AND c2 NOT IN (%s, %s, %s)",
+ 1, 2, 5, 1, 2, 5)
+ .matches(QueryChecker.containsTableScan("PUBLIC", "TEST_ASC"))
// Can't use index scan.
+ .returns(3, "3", 9)
+ .returns(3, "4", 12)
+ .returns(4, "3", 12)
+ .returns(4, "4", 16)
+ .check();
+ }
+
+ /** Test correct index ordering without additional sorting. */
+ @ParameterizedTest(name = "dynamicParams={0}")
+ @ValueSource(booleans = {true, false})
+ public void testOrdering(boolean useDynamicParameters) {
+ assertQuery(useDynamicParameters,
+ "SELECT /*+ FORCE_INDEX(c1c2c3_ASC) */ * FROM test_asc WHERE
c1 IN (%s, %s) AND c2 IN (%s, %s) ORDER BY c1, c2, c3",
+ 3, 2, "3", "2")
+
.matches(CoreMatchers.not(QueryChecker.containsSubPlan("Sort"))) // Don't
require additional sorting.
+ .ordered()
+ .returns(2, "2", 4)
+ .returns(2, "3", 6)
+ .returns(3, "2", 6)
+ .returns(3, "3", 9)
+ .check();
+
+ assertQuery(useDynamicParameters,
+ "SELECT /*+ FORCE_INDEX(c1c2c3_ASC) */ * FROM test_asc WHERE
c1 IN (%s, %s) AND c2 < %s ORDER BY c1, c2, c3", 2, 3, "3")
+
.matches(CoreMatchers.not(QueryChecker.containsSubPlan("Sort"))) // Don't
require additional sorting.
+ .ordered()
+ .returns(2, "1", 2)
+ .returns(2, "2", 4)
+ .returns(3, "1", 3)
+ .returns(3, "2", 6)
+ .check();
+
+ assertQuery(useDynamicParameters,
+ "SELECT /*+ FORCE_INDEX(c1c2c3_ASC) */ * FROM test_asc"
+ + " WHERE c1 IN (%s, %s) AND c2 IN (%s, %s) AND c3
BETWEEN %s AND %s ORDER BY c1, c2, c3",
+ 2, 3, "2", "3", 5, 7)
+
.matches(CoreMatchers.not(QueryChecker.containsSubPlan("Sort"))) // Don't
require additional sorting.
+ .ordered()
+ .returns(2, "3", 6)
+ .returns(3, "2", 6)
+ .check();
+
+ // Check order for table with DESC ordering.
+ assertQuery(useDynamicParameters,
+ "SELECT /*+ FORCE_INDEX(c1c2c3_DESC) */ * FROM test_desc"
+ + " WHERE c1 IN (%s, %s) AND c2 IN (%s, %s) ORDER BY
c1 DESC, c2 DESC, c3 DESC",
+ 3, 2, "3", "2")
+
.matches(CoreMatchers.not(QueryChecker.containsSubPlan("Sort"))) // Don't
require additional sorting.
+ .ordered()
+ .returns(3, "3", 9)
+ .returns(3, "2", 6)
+ .returns(2, "3", 6)
+ .returns(2, "2", 4)
+ .check();
+
+ assertQuery(useDynamicParameters,
+ "SELECT /*+ FORCE_INDEX(c1c2c3_DESC) */ * FROM test_desc"
+ + " WHERE c1 IN (%s, %s) AND c2 < %s ORDER BY c1 DESC,
c2 DESC, c3 DESC",
+ 2, 3, "3")
+
.matches(CoreMatchers.not(QueryChecker.containsSubPlan("Sort"))) // Don't
require additional sorting.
+ .ordered()
+ .returns(3, "2", 6)
+ .returns(3, "1", 3)
+ .returns(2, "2", 4)
+ .returns(2, "1", 2)
+ .check();
+
+ assertQuery(useDynamicParameters,
+ "SELECT /*+ FORCE_INDEX(c1c2c3_DESC) */ * FROM test_desc"
+ + " WHERE c1 IN (%s, %s) AND c2 IN (%s, %s) AND c3
BETWEEN %s AND %s"
+ + " ORDER BY c1 DESC, c2 DESC, c3 DESC",
+ 2, 3, "2", "3", 5, 7)
+
.matches(CoreMatchers.not(QueryChecker.containsSubPlan("Sort"))) // Don't
require additional sorting.
+ .ordered()
+ .returns(3, "2", 6)
+ .returns(2, "3", 6)
+ .check();
+ }
+
+ /** Tests not supported range index scan conditions. */
+ @ParameterizedTest(name = TEST_DISPLAY_NAME)
+ @MethodSource("testParameters")
+ public void testRangeIntersection(boolean useDynamicParameters, String
direction) {
+ assertCondition(direction, useDynamicParameters, "c1 IN (%s, %s, %s,
%s) and c3 in (%s, %s, %s, %s)",
+ 3, 4, 4, 3, 12, 9, 16, 12)
+ .returns(3, "3", 9)
+ .returns(3, "4", 12)
+ .returns(4, "3", 12)
+ .returns(4, "4", 16)
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "c1 IN (%s, %s) "
+ + "AND ((c2 > %s AND c2 < %s) OR (c2 > %s AND c2 <
%s))",
+ 3, 4, "1", "4", "3", "5")
+ .returns(3, "2", 6)
+ .returns(3, "3", 9)
+ .returns(3, "4", 12)
+ .returns(4, "2", 8)
+ .returns(4, "3", 12)
+ .returns(4, "4", 16)
+ .check();
+
+ // Different combinations of LESS_THAN and LESS_THAN_OR_EQUAL.
+ assertCondition(direction, useDynamicParameters, "c1 IN (%s, %s) AND
(c2 < %s OR c2 < %s)",
+ 3, 4, "1", "2")
+ .returns(3, "1", 3)
+ .returns(4, "1", 4)
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "c1 IN (%s, %s) AND
(c2 <= %s OR c2 < %s)",
+ 3, 4, "1", "2")
+ .returns(3, "1", 3)
+ .returns(4, "1", 4)
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "c1 IN (%s, %s) AND
(c2 <= %s OR c2 <= %s)",
+ 3, 4, "1", "2")
+ .returns(3, "1", 3)
+ .returns(3, "2", 6)
+ .returns(4, "1", 4)
+ .returns(4, "2", 8)
+ .check();
+
+ // Different combinations of LESS_THAN, LESS_THAN_OR_EQUAL,
GREATER_THAN, GREATER_THAN_OR_EQUAL.
+ assertCondition(direction, useDynamicParameters,
+ "c1 IN (%s, %s) AND ((c2 > %s AND c2 <= %s) OR (c2 > %s AND c2
<= %s) OR (c2 >= %s AND c2 < %s))",
+ 1, 2, "1", "2", "2", "3", "3", "4")
+ .returns(1, "2", 2)
+ .returns(1, "3", 3)
+ .returns(2, "2", 4)
+ .returns(2, "3", 6)
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "c1 IN (%s, %s) AND
c2 BETWEEN %s AND %s",
+ 3, 3, "2", "4")
+ .returns(3, "2", 6)
+ .returns(3, "3", 9)
+ .returns(3, "4", 12)
+ .check();
+ }
+
+ @ParameterizedTest(name = TEST_DISPLAY_NAME)
+ @MethodSource("testParameters")
+ public void testInvalidRange(boolean useDynamicParameters, String
direction) {
+ assertCondition(direction, useDynamicParameters, "c1 BETWEEN %s AND
%s", 4, 3)
+ .returnNothing()
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "c1 = %s AND c2 > %s
AND c2 < %s", 1, "2", "2")
+ .returnNothing()
+ .check();
+
+ assertCondition(direction, useDynamicParameters, "c1 = %s OR c1
BETWEEN %s AND %s", 1, 3, 2)
+ .returns(1, "1", 1)
+ .returns(1, "2", 2)
+ .returns(1, "3", 3)
+ .returns(1, "4", 4)
+ .returns(1, "5", 5)
+ .check();
+ }
+
+ private static QueryChecker assertCondition(String direction, boolean
dynamicParams, String condition, Object... params) {
+ String sqlPattern = format("SELECT /*+ FORCE_INDEX(c1c2c3_{}) */ *
FROM test_{} WHERE {}", direction, direction, condition);
+
+ return assertQuery(dynamicParams, sqlPattern, params);
+ }
+
+ private static QueryChecker assertQuery(boolean dynamicParams, String
sqlPattern, Object... params) {
+ Object[] args = new Object[params.length];
+
+ if (dynamicParams) {
+ Arrays.fill(args, "?");
+
+ return assertQuery(String.format(sqlPattern,
args)).withParams(params);
+ }
+
+ for (int i = 0; i < params.length; i++) {
+ args[i] = params[i] instanceof String ? '\'' +
params[i].toString() + '\'' : params[i].toString();
+ }
+
+ return assertQuery(String.format(sqlPattern, args));
+ }
+
+ private static List<Arguments> testParameters() {
+ return List.of(
+ Arguments.of(true, "ASC"),
+ Arguments.of(true, "DESC"),
+ Arguments.of(false, "ASC"),
+ Arguments.of(false, "DESC")
+ );
+ }
+}
diff --git
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
index d33ace2634..b0378fdeff 100644
---
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
+++
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
@@ -560,11 +560,9 @@ public class ItSecondaryIndexTest extends
BaseSqlIntegrationTest {
}
@Test
- @Disabled("https://issues.apache.org/jira/browse/IGNITE-21287")
public void testOrCondition4() {
assertQuery("SELECT * FROM Developer WHERE depId=1 OR (name='Mozart'
AND depId=3)")
- .matches(containsUnion(true))
- .matches(containsIndexScan("PUBLIC", "DEVELOPER",
NAME_DEPID_CITY_IDX))
+ .matches(containsIndexScan("PUBLIC", "DEVELOPER", DEPID_IDX))
.returns(1, "Mozart", 3, "Vienna", 33)
.returns(3, "Bach", 1, "Leipzig", 55)
.check();
diff --git
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidIndexTest.java
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidIndexTest.java
index e1b878f415..54a7904146 100644
---
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidIndexTest.java
+++
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidIndexTest.java
@@ -22,7 +22,6 @@ import
org.apache.ignite.internal.sql.engine.datatypes.DataTypeTestSpecs;
import
org.apache.ignite.internal.sql.engine.datatypes.tests.BaseIndexDataTypeTest;
import org.apache.ignite.internal.sql.engine.datatypes.tests.DataTypeTestSpec;
import org.apache.ignite.internal.sql.engine.type.UuidType;
-import org.junit.jupiter.api.Disabled;
/**
* Tests for queries that use indexes with {@link UuidType UUID data type}.
@@ -34,10 +33,4 @@ public class ItUuidIndexTest extends
BaseIndexDataTypeTest<UUID> {
protected DataTypeTestSpec<UUID> getTypeSpec() {
return DataTypeTestSpecs.UUID_TYPE;
}
-
- @Override
- @Disabled("https://issues.apache.org/jira/browse/IGNITE-21330")
- public void testInLookUp() {
- super.testInLookUp();
- }
}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java
index 6e4e604ca1..75737052cf 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java
@@ -50,6 +50,7 @@ import org.apache.calcite.linq4j.tree.ParameterExpression;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelFieldCollation;
+import org.apache.calcite.rel.RelFieldCollation.Direction;
import org.apache.calcite.rel.core.AggregateCall;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
@@ -145,28 +146,38 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
return (o1, o2) -> {
RowHandler<RowT> hnd = ctx.rowHandler();
+ List<RelFieldCollation> collations =
collation.getFieldCollations();
- for (RelFieldCollation field : collation.getFieldCollations()) {
- int fieldIdx = field.getFieldIndex();
- int nullComparison = field.nullDirection.nullComparison;
+ int colsCountRow1 = hnd.columnCount(o1);
+ int colsCountRow2 = hnd.columnCount(o2);
- if (o1 == null || o2 == null) {
- if (o1 == o2) {
- return 0;
- } else if (o1 == null) {
- return nullComparison;
- } else {
- return -nullComparison;
- }
+ // The index range condition can contain the prefix of the index
columns (not all index columns).
+ int maxCols = Math.min(Math.max(colsCountRow1, colsCountRow2),
collations.size());
+
+ for (int i = 0; i < maxCols; i++) {
+ RelFieldCollation field = collations.get(i);
+ boolean ascending = field.direction == Direction.ASCENDING;
+
+ if (i == colsCountRow1) {
+ // There is no more values in first row.
+ return ascending ? -1 : 1;
}
+ if (i == colsCountRow2) {
+ // There is no more values in second row.
+ return ascending ? 1 : -1;
+ }
+
+ int fieldIdx = field.getFieldIndex();
+
Object c1 = hnd.get(fieldIdx, o1);
Object c2 = hnd.get(fieldIdx, o2);
- int res = (field.direction ==
RelFieldCollation.Direction.ASCENDING)
- ?
- compare(c1, c2, nullComparison) :
- compare(c2, c1, -nullComparison);
+ int nullComparison = field.nullDirection.nullComparison;
+
+ int res = ascending
+ ? compare(c1, c2, nullComparison)
+ : compare(c2, c1, -nullComparison);
if (res != 0) {
return res;
@@ -332,7 +343,6 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
return rows;
}
-
/** {@inheritDoc} */
@Override
public RangeIterable<RowT> ranges(
@@ -434,14 +444,12 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
.factory(TypeUtils.rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(upperType)));
ranges.add(new RangeConditionImpl(
- curLower,
- curUpper,
+ nullOrEmpty(curLower) ? null : scalar(curLower, lowerType),
+ nullOrEmpty(curUpper) ? null : scalar(curUpper, upperType),
lowerInclude,
upperInclude,
- lowerType,
- upperType,
- lowerFactory,
- upperFactory
+ lowerFactory.rowBuilder(),
+ upperFactory.rowBuilder()
));
return;
@@ -810,22 +818,20 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
private final RowBuilder<RowT> upperRowBuilder;
private RangeConditionImpl(
- List<RexNode> lower,
- List<RexNode> upper,
+ @Nullable SingleScalar lowerScalar,
+ @Nullable SingleScalar upperScalar,
boolean lowerInclude,
boolean upperInclude,
- RelDataType rowTypeLower,
- RelDataType rowTypeUpper,
- RowFactory<RowT> lowerFactory,
- RowFactory<RowT> upperFactory
+ RowBuilder<RowT> lowerRowBuilder,
+ RowBuilder<RowT> upperRowBuilder
) {
- this.lowerBound = nullOrEmpty(lower) ? null : scalar(lower,
rowTypeLower);
- this.upperBound = nullOrEmpty(upper) ? null : scalar(upper,
rowTypeUpper);
+ this.lowerBound = lowerScalar;
+ this.upperBound = upperScalar;
this.lowerInclude = lowerInclude;
this.upperInclude = upperInclude;
- this.lowerRowBuilder = lowerFactory.rowBuilder();
- this.upperRowBuilder = upperFactory.rowBuilder();
+ this.lowerRowBuilder = lowerRowBuilder;
+ this.upperRowBuilder = upperRowBuilder;
}
/** {@inheritDoc} */
@@ -875,7 +881,7 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
}
private class RangeIterableImpl implements RangeIterable<RowT> {
- private final List<RangeCondition<RowT>> ranges;
+ private List<RangeCondition<RowT>> ranges;
private final @Nullable Comparator<RowT> comparator;
@@ -908,12 +914,134 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
// Do not sort again if ranges already were sorted before,
different values of correlated variables
// should not affect ordering.
if (!sorted && comparator != null) {
- ranges.sort((o1, o2) -> comparator.compare(o1.lower(),
o2.lower()));
+ ranges.sort(this::compareRanges);
+
+ List<RangeCondition<RowT>> ranges0 = new
ArrayList<>(ranges.size());
+
+ RangeConditionImpl prevRange = null;
+
+ for (RangeCondition<RowT> range0 : ranges) {
+ RangeConditionImpl range = (RangeConditionImpl) range0;
+
+ if (compareLowerAndUpperBounds(range.lower(),
range.upper()) > 0) {
+ // Invalid range (low > up).
+ continue;
+ }
+
+ if (prevRange != null) {
+ RangeConditionImpl merged = tryMerge(prevRange, range);
+
+ if (merged == null) {
+ ranges0.add(prevRange);
+ } else {
+ range = merged;
+ }
+ }
+
+ prevRange = range;
+ }
+
+ if (prevRange != null) {
+ ranges0.add(prevRange);
+ }
+
+ ranges = ranges0;
sorted = true;
}
return ranges.iterator();
}
+
+ private int compareRanges(RangeCondition<RowT> first,
RangeCondition<RowT> second) {
+ int cmp = compareBounds(first.lower(), second.lower(), true);
+
+ if (cmp != 0) {
+ return cmp;
+ }
+
+ return compareBounds(first.upper(), second.upper(), false);
+ }
+
+ private int compareBounds(@Nullable RowT row1, @Nullable RowT row2,
boolean lower) {
+ assert comparator != null;
+
+ if (row1 == null || row2 == null) {
+ if (row1 == row2) {
+ return 0;
+ }
+
+ RowT row = lower ? row2 : row1;
+
+ return row == null ? 1 : -1;
+ }
+
+ return comparator.compare(row1, row2);
+ }
+
+ private int compareLowerAndUpperBounds(@Nullable RowT lower, @Nullable
RowT upper) {
+ assert comparator != null;
+
+ if (lower == null || upper == null) {
+ if (lower == upper) {
+ return 0;
+ } else {
+ // lower = null -> lower < any upper
+ // upper = null -> upper > any lower
+ return -1;
+ }
+ }
+
+ return comparator.compare(lower, upper);
+ }
+
+ /** Returns combined range if the provided ranges intersect, {@code
null} otherwise. */
+ @Nullable RangeConditionImpl tryMerge(RangeConditionImpl first,
RangeConditionImpl second) {
+ if (compareLowerAndUpperBounds(first.lower(), second.upper()) > 0
+ || compareLowerAndUpperBounds(second.lower(),
first.upper()) > 0) {
+ // Ranges are not intersect.
+ return null;
+ }
+
+ SingleScalar newLowerBound;
+ RowT newLowerRow;
+ boolean newLowerInclude;
+
+ int cmp = compareBounds(first.lower(), second.lower(), true);
+
+ if (cmp < 0 || (cmp == 0 && first.lowerInclude())) {
+ newLowerBound = first.lowerBound;
+ newLowerRow = first.lower();
+ newLowerInclude = first.lowerInclude();
+ } else {
+ newLowerBound = second.lowerBound;
+ newLowerRow = second.lower();
+ newLowerInclude = second.lowerInclude();
+ }
+
+ SingleScalar newUpperBound;
+ RowT newUpperRow;
+ boolean newUpperInclude;
+
+ cmp = compareBounds(first.upper(), second.upper(), false);
+
+ if (cmp > 0 || (cmp == 0 && first.upperInclude())) {
+ newUpperBound = first.upperBound;
+ newUpperRow = first.upper();
+ newUpperInclude = first.upperInclude();
+ } else {
+ newUpperBound = second.upperBound;
+ newUpperRow = second.upper();
+ newUpperInclude = second.upperInclude();
+ }
+
+ RangeConditionImpl newRangeCondition = new
RangeConditionImpl(newLowerBound, newUpperBound,
+ newLowerInclude, newUpperInclude, first.lowerRowBuilder,
first.upperRowBuilder);
+
+ newRangeCondition.lowerRow = newLowerRow;
+ newRangeCondition.upperRow = newUpperRow;
+
+ return newRangeCondition;
+ }
}
private class BiFieldGetter extends CommonFieldGetter {
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/RexUtils.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/RexUtils.java
index 6dffd47ec0..a237de02f5 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/RexUtils.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/RexUtils.java
@@ -29,6 +29,7 @@ import static org.apache.calcite.sql.SqlKind.IS_NULL;
import static org.apache.calcite.sql.SqlKind.LESS_THAN;
import static org.apache.calcite.sql.SqlKind.LESS_THAN_OR_EQUAL;
import static org.apache.calcite.sql.SqlKind.NOT;
+import static org.apache.calcite.sql.SqlKind.OR;
import static org.apache.calcite.sql.SqlKind.SEARCH;
import static org.apache.ignite.internal.util.CollectionUtils.nullOrEmpty;
@@ -44,6 +45,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
@@ -82,6 +84,7 @@ import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.util.ControlFlowException;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.Litmus;
+import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Sarg;
import org.apache.calcite.util.Util;
import org.apache.calcite.util.mapping.MappingType;
@@ -413,6 +416,18 @@ public class RexUtils {
boolean upperInclude = true;
boolean lowerInclude = true;
+ // Give priority to equality operators.
+ collFldPreds.sort(Comparator.comparingInt(pred -> {
+ switch (pred.getOperator().getKind()) {
+ case EQUALS:
+ case IS_NOT_DISTINCT_FROM:
+ case IS_NULL:
+ return 0;
+ default:
+ return 1;
+ }
+ }));
+
for (RexCall pred : collFldPreds) {
RexNode val = null;
RexNode ref = pred.getOperands().get(0);
@@ -429,6 +444,33 @@ public class RexUtils {
return new ExactBounds(pred, val);
} else if (op.kind == IS_NULL) {
return new ExactBounds(pred, nullValue);
+ } else if (op.kind == OR) {
+ List<SearchBounds> orBounds = new ArrayList<>();
+ int curComplexity = 0;
+
+ for (RexNode operand : pred.getOperands()) {
+ SearchBounds opBounds = createBounds(fc,
Collections.singletonList((RexCall) operand),
+ cluster, fldType, prevComplexity);
+
+ if (opBounds instanceof MultiBounds) {
+ curComplexity += ((MultiBounds)
opBounds).bounds().size();
+ orBounds.addAll(((MultiBounds) opBounds).bounds());
+ } else if (opBounds != null) {
+ curComplexity++;
+ orBounds.add(opBounds);
+ }
+
+ if (opBounds == null || curComplexity >
MAX_SEARCH_BOUNDS_COMPLEXITY) {
+ orBounds = null;
+ break;
+ }
+ }
+
+ if (orBounds == null) {
+ continue;
+ }
+
+ return new MultiBounds(pred, orBounds);
} else if (op.kind == SEARCH) {
Sarg<?> sarg = ((RexLiteral)
pred.operands.get(1)).getValueAs(Sarg.class);
@@ -614,40 +656,104 @@ public class RexUtils {
Int2ObjectMap<List<RexCall>> res = new
Int2ObjectOpenHashMap<>(conjunctions.size());
for (RexNode rexNode : conjunctions) {
- rexNode = expandBooleanFieldComparison(rexNode, builder(cluster));
+ Pair<Integer, RexCall> refPredicate = null;
- if (!isSupportedTreeComparison(rexNode)) {
- continue;
- }
+ if (rexNode instanceof RexCall && rexNode.getKind() == OR) {
+ List<RexNode> operands = ((RexCall) rexNode).getOperands();
- RexCall predCall = (RexCall) rexNode;
- RexSlot ref;
+ Integer ref = null;
+ List<RexCall> preds = new ArrayList<>(operands.size());
- if (isBinaryComparison(rexNode)) {
- ref = extractRefFromBinary(predCall, cluster);
+ for (RexNode operand : operands) {
+ Pair<Integer, RexCall> operandRefPredicate =
extractRefPredicate(operand, cluster);
- if (ref == null) {
- continue;
+ // Skip the whole OR condition if any operand does not
support tree comparison or not on reference.
+ if (operandRefPredicate == null) {
+ ref = null;
+ break;
+ }
+
+ // Ensure that we have the same field reference in all
operands.
+ if (ref == null) {
+ ref = operandRefPredicate.getKey();
+ } else if (!ref.equals(operandRefPredicate.getKey())) {
+ ref = null;
+ break;
+ }
+
+ // For correlated variables it's required to resort and
merge ranges on each nested loop,
+ // don't support it now.
+ if (containsFieldAccess(operandRefPredicate.getValue())) {
+ ref = null;
+ break;
+ }
+
+ preds.add(operandRefPredicate.getValue());
}
- // Let RexLocalRef be on the left side.
- if (refOnTheRight(predCall)) {
- predCall = (RexCall) invert(builder(cluster), predCall);
+ if (ref != null) {
+ refPredicate = Pair.of(ref, (RexCall)
builder(cluster).makeCall(((RexCall) rexNode).getOperator(), preds));
}
} else {
- ref = extractRefFromOperand(predCall, cluster, 0);
+ refPredicate = extractRefPredicate(rexNode, cluster);
+ }
- if (ref == null) {
- continue;
- }
+ if (refPredicate != null) {
+ List<RexCall> fldPreds =
res.computeIfAbsent(refPredicate.getKey(), k -> new
ArrayList<>(conjunctions.size()));
+
+ fldPreds.add(refPredicate.getValue());
}
+ }
+ return res;
+ }
- List<RexCall> fldPreds = res.computeIfAbsent(ref.getIndex(), k ->
new ArrayList<>(conjunctions.size()));
+ private static @Nullable Pair<Integer, RexCall>
extractRefPredicate(RexNode rexNode, RelOptCluster cluster) {
+ rexNode = expandBooleanFieldComparison(rexNode, builder(cluster));
- fldPreds.add(predCall);
+ if (!isSupportedTreeComparison(rexNode)) {
+ return null;
}
- return res;
+ RexCall predCall = (RexCall) rexNode;
+ RexSlot ref;
+
+ if (isBinaryComparison(rexNode)) {
+ ref = extractRefFromBinary(predCall, cluster);
+
+ if (ref == null) {
+ return null;
+ }
+
+ // Let RexLocalRef be on the left side.
+ if (refOnTheRight(predCall)) {
+ predCall = (RexCall) invert(builder(cluster), predCall);
+ }
+ } else {
+ ref = extractRefFromOperand(predCall, cluster, 0);
+
+ if (ref == null) {
+ return null;
+ }
+ }
+
+ return Pair.of(ref.getIndex(), predCall);
+ }
+
+ private static Boolean containsFieldAccess(RexNode node) {
+ RexVisitor<Void> v = new RexVisitorImpl<Void>(true) {
+ @Override
+ public Void visitFieldAccess(RexFieldAccess fieldAccess) {
+ throw Util.FoundOne.NULL;
+ }
+ };
+
+ try {
+ node.accept(v);
+
+ return false;
+ } catch (Util.FoundOne e) {
+ return true;
+ }
}
/** Extended version of {@link RexUtil#invert(RexBuilder, RexCall)} with
additional operators support. */
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImplTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImplTest.java
index e51b16ed61..bf27f49150 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImplTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImplTest.java
@@ -32,6 +32,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Objects;
import java.util.UUID;
import java.util.function.BiPredicate;
import java.util.function.Function;
@@ -239,7 +240,7 @@ public class ExpressionFactoryImplTest extends
BaseIgniteAbstractTest {
List<SearchBounds> boundsList = List.of(
new MultiBounds(condition, List.of(
new ExactBounds(condition, intValue1),
- new RangeBounds(condition, intValue5, null, true,
true)
+ new RangeBounds(condition, nullValue, intValue5,
false, false)
))
);
@@ -250,14 +251,14 @@ public class ExpressionFactoryImplTest extends
BaseIgniteAbstractTest {
ranges.forEach(r -> list.add(new TestRange(r.lower(), r.upper())));
- assertEquals(List.of(new TestRange(new Object[]{5}, null), new
TestRange(new Object[]{1})), list);
+ assertEquals(List.of(new TestRange(new Object[]{null}, new
Object[]{5}), new TestRange(new Object[]{1})), list);
}
{ // range condition with null value as lower bound should respect
collation (NULLS FIRST)
List<SearchBounds> boundsList = List.of(
new MultiBounds(condition, List.of(
new ExactBounds(condition, intValue1),
- new RangeBounds(condition, nullValue, null, true,
true)
+ new RangeBounds(condition, null, nullValue, true,
true)
))
);
@@ -269,19 +270,19 @@ public class ExpressionFactoryImplTest extends
BaseIgniteAbstractTest {
ranges.forEach(r -> list.add(new TestRange(r.lower(), r.upper())));
- assertEquals(List.of(new TestRange(new Object[]{null}, null), new
TestRange(new Object[]{1})), list);
+ assertEquals(List.of(new TestRange(null, new Object[]{null}), new
TestRange(new Object[]{1})), list);
}
{ // range condition with null value as lower bound should respect
collation (NULLS LAST)
List<SearchBounds> boundsList = List.of(
new MultiBounds(condition, List.of(
- new ExactBounds(condition, intValue1),
- new RangeBounds(condition, nullValue, null, true,
true)
+ new RangeBounds(condition, nullValue, null, true,
true),
+ new ExactBounds(condition, intValue1)
))
);
Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
- new RelFieldCollation(0, Direction.DESCENDING,
NullDirection.LAST)));
+ new RelFieldCollation(0, Direction.ASCENDING,
NullDirection.LAST)));
RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
List<TestRange> list = new ArrayList<>();
@@ -311,6 +312,232 @@ public class ExpressionFactoryImplTest extends
BaseIgniteAbstractTest {
}
}
+ @Test
+ void multiBoundConditionsAreMergedCorrectly() {
+ RexBuilder rexBuilder = Commons.rexBuilder();
+
+ // condition expression is not used
+ RexLiteral condition = rexBuilder.makeLiteral(true);
+
+ RelDataType rowType = new Builder(typeFactory)
+ .add("C1", SqlTypeName.INTEGER)
+ .build();
+
+ RexNode intValue1 = rexBuilder.makeExactLiteral(new BigDecimal("1"));
+ RexNode intValue2 = rexBuilder.makeExactLiteral(new BigDecimal("2"));
+ RexNode intValue3 = rexBuilder.makeExactLiteral(new BigDecimal("3"));
+ RexNode intValue5 = rexBuilder.makeExactLiteral(new BigDecimal("5"));
+
+ { // conditions 'val < 1 or val = 1 or val = 5' can be combined to
'val <= 1 or val = 5' (ASCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new ExactBounds(condition, intValue5),
+ new ExactBounds(condition, intValue1),
+ new RangeBounds(condition, null, intValue1, true,
false)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.ASCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(null, new Object[]{1}), new
TestRange(new Object[]{5})), list);
+ }
+
+ { // conditions 'val < 1 or val = 1 or val = 5' can be combined to
'val <= 1 or val = 5' (DESCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new RangeBounds(condition, intValue1, null, false,
true),
+ new ExactBounds(condition, intValue1),
+ new ExactBounds(condition, intValue5)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.DESCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(new Object[]{5}), new
TestRange(new Object[]{1}, null)), list);
+ }
+
+ { // conditions 'val >= 1 and val <= 5 or val > 1 and val < 5' must be
combined to single 'val >= 1 and val <= 5' (ASCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new RangeBounds(condition, intValue1, intValue5,
true, true),
+ new RangeBounds(condition, intValue1, intValue5,
false, false)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.ASCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(new Object[]{1}, new
Object[]{5})), list);
+ }
+
+ { // conditions 'val >= 1 and val <= 5 or val > 1 and val < 5' must be
combined to single 'val >= 1 and val <= 5' (DESCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new RangeBounds(condition, intValue5, intValue1,
true, true),
+ new RangeBounds(condition, intValue5, intValue1,
false, false)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.DESCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(new Object[]{5}, new
Object[]{1})), list);
+ }
+
+ { // conditions 'val >= 1 and val <= 2 or val >= 2 and val < 5' must
be combined to single 'val >= 1 and val < 5' (ASCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new RangeBounds(condition, intValue1, intValue2,
true, true),
+ new RangeBounds(condition, intValue2, intValue5,
true, false)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.ASCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(new Object[]{1}, true, new
Object[]{5}, false)), list);
+ }
+
+ { // conditions 'val >= 1 and val <= 2 or val >= 2 and val < 5' must
be combined to single 'val >= 1 and val < 5' (DESCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new RangeBounds(condition, intValue2, intValue1,
true, true),
+ new RangeBounds(condition, intValue5, intValue2,
false, true)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.DESCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(new Object[]{5}, false, new
Object[]{1}, true)), list);
+ }
+
+ { // conditions 'val >= 1 and val < 3 or val > 2 and val < 5' must be
combined into single 'val >= 1 and val <= 5' (ASCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new RangeBounds(condition, intValue1, intValue3,
true, false),
+ new RangeBounds(condition, intValue2, intValue5,
false, false)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.ASCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(new Object[]{1}, true, new
Object[]{5}, false)), list);
+ }
+
+ { // conditions 'val >= 1 and val < 3 or val > 2 and val < 5' must be
combined into single 'val >= 1 and val <= 5' (DESCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new RangeBounds(condition, intValue3, intValue1,
false, true),
+ new RangeBounds(condition, intValue5, intValue2,
false, false)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.DESCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(new Object[]{5}, false, new
Object[]{1}, true)), list);
+ }
+ }
+
+ @Test
+ public void testInvalidConditions() {
+ // At the moment, such conditions are impossible to obtain, but we
should be aware of them,
+ // since they can break the merge procedure.
+ RexBuilder rexBuilder = Commons.rexBuilder();
+
+ // condition expression is not used
+ RexLiteral condition = rexBuilder.makeLiteral(true);
+
+ RelDataType rowType = new Builder(typeFactory)
+ .add("C1", SqlTypeName.INTEGER)
+ .build();
+
+ RexNode intValue1 = rexBuilder.makeExactLiteral(new BigDecimal("1"));
+ RexNode intValue2 = rexBuilder.makeExactLiteral(new BigDecimal("2"));
+
+ { // conditions 'val between 2 and 1 or val = 2' should lead to single
'val = 2' (ASCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new RangeBounds(condition, intValue2, intValue1,
true, true),
+ new ExactBounds(condition, intValue2)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.ASCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(new Object[]{2})), list);
+ }
+
+ { // conditions 'val between 2 and 1 or val = 2' should lead to single
'val = 2' (DESCENDING)
+ List<SearchBounds> boundsList = List.of(
+ new MultiBounds(condition, List.of(
+ new RangeBounds(condition, intValue1, intValue2,
true, true),
+ new ExactBounds(condition, intValue2)
+ ))
+ );
+
+ Comparator<Object[]> comparator =
expFactory.comparator(RelCollations.of(
+ new RelFieldCollation(0, Direction.DESCENDING)));
+
+ RangeIterable<Object[]> ranges = expFactory.ranges(boundsList,
rowType, comparator);
+ List<TestRange> list = new ArrayList<>();
+
+ ranges.forEach(r -> list.add(new TestRange(r.lower(),
r.lowerInclude(), r.upper(), r.upperInclude())));
+
+ assertEquals(List.of(new TestRange(new Object[]{2})), list);
+ }
+ }
+
@ParameterizedTest(name = "condition satisfies the index: [{0}]")
@ValueSource(booleans = {true, false})
public void testConditionsNotContainsNulls(boolean conditionSatisfyIdx) {
@@ -498,13 +725,23 @@ public class ExpressionFactoryImplTest extends
BaseIgniteAbstractTest {
final Object[] upper;
+ final boolean lowerInclude;
+
+ final boolean upperInclude;
+
TestRange(Object[] lower) {
this(lower, lower);
}
TestRange(Object @Nullable [] lower, Object @Nullable [] upper) {
+ this(lower, true, upper, true);
+ }
+
+ TestRange(Object @Nullable [] lower, boolean lowerInclude, Object
@Nullable [] upper, boolean upperInclude) {
this.lower = lower;
this.upper = upper;
+ this.lowerInclude = lowerInclude;
+ this.upperInclude = upperInclude;
}
@Override
@@ -516,12 +753,14 @@ public class ExpressionFactoryImplTest extends
BaseIgniteAbstractTest {
return false;
}
TestRange testRange = (TestRange) o;
- return Arrays.equals(lower, testRange.lower) &&
Arrays.equals(upper, testRange.upper);
+ return lowerInclude == testRange.lowerInclude && upperInclude ==
testRange.upperInclude && Arrays.equals(lower,
+ testRange.lower) && Arrays.equals(upper, testRange.upper);
}
@Override
public int hashCode() {
- int result = Arrays.hashCode(lower);
+ int result = Objects.hash(lowerInclude, upperInclude);
+ result = 31 * result + Arrays.hashCode(lower);
result = 31 * result + Arrays.hashCode(upper);
return result;
}
@@ -530,6 +769,8 @@ public class ExpressionFactoryImplTest extends
BaseIgniteAbstractTest {
public String toString() {
return "{lower=" + Arrays.toString(lower)
+ ", upper=" + Arrays.toString(upper)
+ + ", lowerInclude=" + lowerInclude
+ + ", upperInclude=" + upperInclude
+ '}';
}
}
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/IndexSearchBoundsPlannerTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/IndexSearchBoundsPlannerTest.java
index ed3349185b..b805c56f4e 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/IndexSearchBoundsPlannerTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/IndexSearchBoundsPlannerTest.java
@@ -351,12 +351,8 @@ public class IndexSearchBoundsPlannerTest extends
AbstractPlannerTest {
/** Tests bounds with dynamic parameters. */
@Test
public void testBoundsDynamicParams() throws Exception {
- // Cannot optimize dynamic parameters to SEARCH/SARG, query is
splitted by or-to-union rule.
- // TODO: https://issues.apache.org/jira/browse/IGNITE-21287
- // assertPlan("SELECT * FROM TEST WHERE C1 IN (?, ?)", publicSchema,
isInstanceOf(IgniteUnionAll.class)
- // .and(input(0, isIndexScan("TEST", "C1C2C3")))
- // .and(input(1, isIndexScan("TEST", "C1C2C3"))), List.of(1, 1)
- // );
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (?, ?)",
+ multi(exact("?0"), exact("?1")));
assertBounds("SELECT * FROM TEST WHERE C1 = ? AND C2 IN ('a', 'b')",
List.of(1), publicSchema,
exact("?0"),
@@ -445,6 +441,31 @@ public class IndexSearchBoundsPlannerTest extends
AbstractPlannerTest {
assertPlan("SELECT (SELECT C1 FROM TEST t2 WHERE t2.C1 < t1.C1 +
t2.C1) FROM TEST t1", publicSchema,
nodeOrAnyChild(isIndexScan("TEST", "C1C2C3")).negate());
+
+ // Here we have two OR sets in CNF, second set can't be used, since it
contains condition on C1 and C2 columns,
+ // so use only first OR set as bounds.
+ assertBounds("SELECT * FROM TEST WHERE C1 in (?, 1, 2) or (C1 = ? and
C2 > 'asd')",
+ multi(exact("?0"), exact(1), exact(2), exact("?1"))
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 in (?, ? + 1, ? * 2)",
+ multi(exact("?0"), exact("+(?1, 1)"), exact("*(?2, 2)"))
+ );
+
+ // Don't support expanding OR with correlate to bounds.
+ assertPlan("SELECT (SELECT C1 FROM TEST t2 WHERE C1 in (t1.C1, 1, ?))
FROM TEST t1", publicSchema,
+ nodeOrAnyChild(isIndexScan("TEST", "C1C2C3")).negate());
+
+ // Here "BETWEEN" generates AND condition, and we have two OR sets in
CNF, so we can't correctly use range
+ // with both upper and lower bounds. So, we use only first OR set as
bounds.
+ assertBounds("SELECT * FROM TEST WHERE C1 in (?, 1, 2) or C1 between ?
and ?",
+ multi(exact("?0"), exact(1), exact(2), range("?1", "null",
true, false))
+ );
+
+ // Check equality condition priority over SEARCH/SARG.
+ assertBounds("SELECT * FROM TEST WHERE (C1 BETWEEN 1 AND 10 OR C1 IN
(20, 30)) AND C1 = ?",
+ exact("?0")
+ );
}
/**