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 <xxt...@gmail.com> 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") + ); } /**