This is an automated email from the ASF dual-hosted git repository.
zstan 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 d53663e52c IGNITE-18746 Sql. Sargs implementation alignment - Fixes
#1721.
d53663e52c is described below
commit d53663e52cc2b69af92abce6afad9ee3d81ffdf2
Author: zstan <[email protected]>
AuthorDate: Tue Mar 7 10:03:44 2023 +0300
IGNITE-18746 Sql. Sargs implementation alignment - Fixes #1721.
Signed-off-by: zstan <[email protected]>
---
modules/rest-api/openapi/openapi.yaml | 3 +
.../sql/engine/ItHashSpoolIntegrationTest.java | 4 +-
.../ignite/internal/sql/engine/ItJoinTest.java | 11 +-
.../internal/sql/engine/ItSecondaryIndexTest.java | 265 ++++++++++-
.../sql/engine/exec/AbstractIndexScan.java | 14 +-
.../ignite/internal/sql/engine/exec/TreeIndex.java | 3 +-
.../sql/engine/exec/exp/ExpressionFactoryImpl.java | 48 +-
.../sql/engine/exec/exp/RangeIterable.java | 8 +-
.../sql/engine/exec/rel/IndexScanNode.java | 1 -
.../internal/sql/engine/rel/AbstractIndexScan.java | 2 +-
.../exec/rel/IndexScanNodeExecutionTest.java | 2 +-
.../exec/rel/SortedIndexSpoolExecutionTest.java | 5 +-
.../sql/engine/planner/AbstractPlannerTest.java | 46 +-
.../planner/IndexSearchBoundsPlannerTest.java | 510 +++++++++++++++++++++
.../internal/sql/engine/planner/PlannerTest.java | 118 -----
15 files changed, 849 insertions(+), 191 deletions(-)
diff --git a/modules/rest-api/openapi/openapi.yaml
b/modules/rest-api/openapi/openapi.yaml
index e7b2305b49..10d9f4e941 100644
--- a/modules/rest-api/openapi/openapi.yaml
+++ b/modules/rest-api/openapi/openapi.yaml
@@ -852,12 +852,15 @@ components:
properties:
id:
type: string
+ description: Returns unit identifier.
versionToConsistentIds:
type: object
additionalProperties:
type: array
items:
type: string
+ description: Returns map from existing unit version to list of nodes
consistent
+ ids where unit deployed.
description: Unit status.
Void:
type: object
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItHashSpoolIntegrationTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItHashSpoolIntegrationTest.java
index 3101f099f2..cef1e09ad1 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItHashSpoolIntegrationTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItHashSpoolIntegrationTest.java
@@ -50,11 +50,11 @@ public class ItHashSpoolIntegrationTest extends
ClusterPerClassIntegrationTest {
sql("INSERT INTO t0(i1, i2) VALUES (null, 0), (1, null), (null, 2),
(3, null), (1, 1)");
sql("INSERT INTO t1(i1, i2) VALUES (null, 0), (null, 1), (2, null),
(3, null), (1, 1)");
- String sql = "SELECT /*+ DISABLE_RULE ('MergeJoinConverter',
'NestedLoopJoinConverter', "
- + "'FilterSpoolMergeToSortedIndexSpoolRule')*/ t0.i1, t0.i2,
t1.i1, t1.i2 "
+ String sql = "SELECT t0.i1, t0.i2, t1.i1, t1.i2 "
+ "FROM t0 JOIN t1 ON t0.i1=t1.i1 AND t0.i2=t1.i2";
assertQuery(sql)
+ .disableRules("MergeJoinConverter", "NestedLoopJoinConverter",
"FilterSpoolMergeToSortedIndexSpoolRule")
.returns(1, 1, 1, 1)
.check();
}
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItJoinTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItJoinTest.java
index eb2f787dd0..817646bbea 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItJoinTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItJoinTest.java
@@ -794,17 +794,14 @@ public class ItJoinTest extends
ClusterPerClassIntegrationTest {
@ParameterizedTest(name = "join algo : {0}, index present: {1}")
@MethodSource("joinTypes")
@WithSystemProperty(key = "IMPLICIT_PK_ENABLED", value = "true")
- public void testIsNotDistinctFrom(JoinType joinType, boolean indexScan) {
- if (indexScan) {
- // TODO: https://issues.apache.org/jira/browse/IGNITE-18468 Index
scan eventually return partial data.
- return;
- }
-
+ public void testIsNotDistinctFrom(JoinType joinType, boolean indexScan)
throws InterruptedException {
try {
sql("CREATE TABLE t11(i1 INTEGER, i2 INTEGER)");
if (indexScan) {
sql("CREATE INDEX t11_idx ON t11(i1)");
+ // FIXME: https://issues.apache.org/jira/browse/IGNITE-18203
+ waitForIndex("t11_idx");
}
sql("INSERT INTO t11 VALUES (1, null), (2, 2), (null, 3), (3,
null), (5, null)");
@@ -813,6 +810,8 @@ public class ItJoinTest extends
ClusterPerClassIntegrationTest {
if (indexScan) {
sql("CREATE INDEX t22_idx ON t22(i3)");
+ // FIXME: https://issues.apache.org/jira/browse/IGNITE-18203
+ waitForIndex("t22_idx");
}
sql("INSERT INTO t22 VALUES (1, 1), (2, 2), (null, 3), (4, null),
(5, null)");
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
index 6154b74b57..b27eff0186 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
@@ -25,11 +25,35 @@ import static
org.apache.ignite.internal.sql.engine.util.QueryChecker.containsTa
import static
org.apache.ignite.internal.sql.engine.util.QueryChecker.containsUnion;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.BitSet;
import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Flow.Publisher;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.internal.app.IgniteImpl;
+import org.apache.ignite.internal.hlc.HybridTimestamp;
+import org.apache.ignite.internal.index.Index;
+import org.apache.ignite.internal.index.SortedIndex;
+import org.apache.ignite.internal.index.SortedIndexDescriptor;
+import org.apache.ignite.internal.schema.BinaryRow;
+import org.apache.ignite.internal.schema.BinaryTuple;
+import org.apache.ignite.internal.schema.BinaryTuplePrefix;
+import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
+import org.apache.ignite.internal.sql.engine.schema.IgniteTableImpl;
+import org.apache.ignite.internal.sql.engine.schema.SqlSchemaManagerImpl;
+import org.apache.ignite.internal.sql.engine.util.QueryChecker;
+import org.apache.ignite.internal.testframework.IgniteTestUtils;
+import org.apache.ignite.internal.utils.PrimaryReplica;
+import org.apache.ignite.network.ClusterNode;
+import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
@@ -853,6 +877,11 @@ public class ItSecondaryIndexTest extends
ClusterPerClassIntegrationTest {
.returns(5, 5)
.returns(6, 6)
.check();
+
+ // Not nullable column, filter is always - false.
+ assertQuery("SELECT * FROM T1 WHERE id IS NULL")
+ .matches(QueryChecker.matches(".*filters=\\[false\\].*"))
+ .check();
}
/**
@@ -881,7 +910,6 @@ public class ItSecondaryIndexTest extends
ClusterPerClassIntegrationTest {
}
@Test
- @Disabled("https://issues.apache.org/jira/browse/IGNITE-18468")
public void testNullCondition1() {
assertQuery("SELECT * FROM T1 WHERE val is null")
.matches(containsIndexScan("PUBLIC", "T1", "T1_IDX"))
@@ -893,7 +921,6 @@ public class ItSecondaryIndexTest extends
ClusterPerClassIntegrationTest {
}
@Test
- @Disabled("https://issues.apache.org/jira/browse/IGNITE-18468")
public void testNullCondition2() {
assertQuery("SELECT * FROM T1 WHERE (val <= 5) or (val is null)")
.matches(containsIndexScan("PUBLIC", "T1", "T1_IDX"))
@@ -908,7 +935,6 @@ public class ItSecondaryIndexTest extends
ClusterPerClassIntegrationTest {
}
@Test
- @Disabled("https://issues.apache.org/jira/browse/IGNITE-18468")
public void testNullCondition3() {
assertQuery("SELECT * FROM T1 WHERE (val >= 5) or (val is null)")
.matches(containsIndexScan("PUBLIC", "T1", "T1_IDX"))
@@ -920,4 +946,235 @@ public class ItSecondaryIndexTest extends
ClusterPerClassIntegrationTest {
.returns(7, null)
.check();
}
+
+ @Test
+ public void testNullsInCorrNestedLoopJoinSearchRow() throws
InterruptedException {
+ try {
+ sql("CREATE TABLE t(i0 INTEGER PRIMARY KEY, i1 INTEGER, i2
INTEGER)");
+ sql("CREATE INDEX t_idx ON t(i1)");
+ // FIXME: https://issues.apache.org/jira/browse/IGNITE-18203
+ waitForIndex("t_idx");
+ sql("INSERT INTO t VALUES (1, 0, null), (2, 1, null), (3, 2, 2),
(4, 3, null), (5, 4, null), (6, null, 5)");
+
+ List<RowCountingIndex> idxs = injectRowCountingIndex("T", "T_IDX");
+
+ String sql = "SELECT t1.i1, t2.i1 FROM t t1 LEFT JOIN t t2 ON
t1.i2 = t2.i1";
+
+ assertQuery(sql)
+ .disableRules("NestedLoopJoinConverter",
"MergeJoinConverter")
+ .matches(containsSubPlan("IgniteCorrelatedNestedLoopJoin"))
+ .matches(containsIndexScan("PUBLIC", "T", "T_IDX"))
+ .returns(0, null)
+ .returns(1, null)
+ .returns(2, 2)
+ .returns(3, null)
+ .returns(4, null)
+ .returns(null, null)
+ .check();
+
+ // There shouldn't be full index scan in case of null values in
search row, only one value must be found by
+ // range scan and passed to predicate.
+ assertEquals(1,
idxs.stream().mapToInt(RowCountingIndex::touchCount).sum());
+ } finally {
+ sql("DROP TABLE IF EXISTS t");
+ }
+ }
+
+ @Test
+ public void testNullsInSearchRow() throws InterruptedException {
+ try {
+ sql("CREATE TABLE t(i0 INTEGER PRIMARY KEY, i1 INTEGER, i2
INTEGER)");
+ sql("CREATE INDEX t_idx ON t(i1, i2)");
+ // FIXME: https://issues.apache.org/jira/browse/IGNITE-18203
+ waitForIndex("t_idx");
+ sql("INSERT INTO t VALUES (1, null, 0), (2, 1, null), (3, 2, 2),
(4, 3, null)");
+
+ List<RowCountingIndex> idxs = injectRowCountingIndex("T", "T_IDX");
+
+ assertQuery("SELECT * FROM t WHERE i1 = ?")
+ .withParams(null)
+ .matches(containsIndexScan("PUBLIC", "T", "T_IDX"))
+ .check();
+
+ assertEquals(0,
idxs.stream().mapToInt(RowCountingIndex::touchCount).sum());
+
+ assertQuery("SELECT * FROM t WHERE i1 = 1 AND i2 = ?")
+ .withParams(new Object[] { null })
+ .matches(containsIndexScan("PUBLIC", "T", "T_IDX"))
+ .check();
+
+ // Multi ranges.
+ assertQuery("SELECT * FROM t WHERE i1 IN (1, 2, 3) AND i2 = ?")
+ .withParams(new Object[] { null })
+ .matches(containsIndexScan("PUBLIC", "T", "T_IDX"))
+ .check();
+
+ assertEquals(0,
idxs.stream().mapToInt(RowCountingIndex::touchCount).sum());
+
+ assertQuery("SELECT i1, i2 FROM t WHERE i1 IN (1, 2) AND i2 IS
NULL")
+ .matches(containsIndexScan("PUBLIC", "T", "T_IDX"))
+ .returns(1, null)
+ .check();
+
+ assertEquals(1,
idxs.stream().mapToInt(RowCountingIndex::touchCount).sum());
+ } finally {
+ sql("DROP TABLE IF EXISTS t");
+ }
+ }
+
+ private List<RowCountingIndex> injectRowCountingIndex(String tableName,
String idxName) {
+ List<RowCountingIndex> countingIdxs = new ArrayList<>();
+
+ for (Ignite ign : CLUSTER_NODES) {
+ IgniteImpl ignEx = (IgniteImpl) ign;
+
+ SqlQueryProcessor qp = (SqlQueryProcessor) ignEx.queryEngine();
+
+ SqlSchemaManagerImpl sqlSchemaManager = (SqlSchemaManagerImpl)
IgniteTestUtils.getFieldValue(qp,
+ SqlQueryProcessor.class, "sqlSchemaManager");
+
+ IgniteTableImpl tbl = (IgniteTableImpl)
sqlSchemaManager.schema("PUBLIC").getTable(tableName);
+
+ IgniteIndex idx = tbl.getIndex(idxName);
+
+ Index<?> internalIdx = idx.index();
+
+ RowCountingIndex countingIdx = new RowCountingIndex((SortedIndex)
internalIdx);
+
+ IgniteTestUtils.setFieldValue(idx, "index", countingIdx);
+
+ countingIdxs.add(countingIdx);
+ }
+
+ return countingIdxs;
+ }
+
+ private static class RowCountingIndex implements SortedIndex {
+ private SortedIndex delegate;
+
+ private WrappedPublisher wrpPublisher;
+
+ RowCountingIndex(SortedIndex delegate) {
+ this.delegate = delegate;
+ }
+
+ List<WrappedPublisher> wrpPublishers = new ArrayList<>();
+
+ int touchCount() {
+ return
wrpPublishers.stream().mapToInt(WrappedPublisher::touchCount).sum();
+ }
+
+ @Override
+ public UUID id() {
+ return delegate.id();
+ }
+
+ @Override
+ public String name() {
+ return delegate.name();
+ }
+
+ @Override
+ public UUID tableId() {
+ return delegate.tableId();
+ }
+
+ @Override
+ public SortedIndexDescriptor descriptor() {
+ return delegate.descriptor();
+ }
+
+ @Override
+ public Publisher<BinaryRow> lookup(int partId, UUID txId,
PrimaryReplica recipient, BinaryTuple key, @Nullable BitSet columns) {
+ return delegate.lookup(partId, txId, recipient, key, columns);
+ }
+
+ @Override
+ public Publisher<BinaryRow> lookup(int partId, HybridTimestamp
readTimestamp, ClusterNode recipientNode, BinaryTuple key,
+ @Nullable BitSet columns) {
+ return delegate.lookup(partId, readTimestamp, recipientNode, key,
columns);
+ }
+
+ @Override
+ public Publisher<BinaryRow> scan(int partId, UUID txId, PrimaryReplica
recipient, @Nullable BinaryTuplePrefix leftBound,
+ @Nullable BinaryTuplePrefix rightBound, int flags, @Nullable
BitSet columnsToInclude) {
+ Publisher<BinaryRow> pub = delegate.scan(partId, txId, recipient,
leftBound, rightBound, flags, columnsToInclude);
+
+ return wrap(pub);
+ }
+
+ @Override
+ public Publisher<BinaryRow> scan(int partId, HybridTimestamp
readTimestamp, ClusterNode recipientNode,
+ @Nullable BinaryTuplePrefix leftBound, @Nullable
BinaryTuplePrefix rightBound, int flags,
+ @Nullable BitSet columnsToInclude) {
+ Publisher<BinaryRow> pub = delegate.scan(partId, readTimestamp,
recipientNode, leftBound, rightBound, flags, columnsToInclude);
+
+ return wrap(pub);
+ }
+
+ private WrappedPublisher wrap(Publisher<BinaryRow> pub) {
+ wrpPublisher = new WrappedPublisher(pub);
+
+ wrpPublishers.add(wrpPublisher);
+
+ return wrpPublisher;
+ }
+ }
+
+ private static class WrappedPublisher implements Publisher<BinaryRow> {
+ Publisher<BinaryRow> pub;
+
+ WrappedSubscriber subs;
+
+ WrappedPublisher(Publisher<BinaryRow> pub) {
+ this.pub = pub;
+ }
+
+ @Override
+ public void subscribe(Subscriber subscriber) {
+ subs = new WrappedSubscriber(subscriber);
+
+ pub.subscribe(subs);
+ }
+
+ int touchCount() {
+ return subs.touchCount();
+ }
+ }
+
+ private static class WrappedSubscriber implements Subscriber {
+ private Subscriber subs;
+
+ private AtomicInteger touched = new AtomicInteger();
+
+ WrappedSubscriber(Subscriber subs) {
+ this.subs = subs;
+ }
+
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ subs.onSubscribe(subscription);
+ }
+
+ @Override
+ public void onNext(Object item) {
+ touched.incrementAndGet();
+
+ subs.onNext(item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ subs.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ subs.onComplete();
+ }
+
+ int touchCount() {
+ return touched.get();
+ }
+ }
}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/AbstractIndexScan.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/AbstractIndexScan.java
index f4baba4c21..a9f22d3677 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/AbstractIndexScan.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/AbstractIndexScan.java
@@ -18,10 +18,12 @@
package org.apache.ignite.internal.sql.engine.exec;
import com.google.common.collect.Streams;
+import java.util.Collections;
import java.util.Iterator;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.calcite.rel.type.RelDataType;
+import org.apache.ignite.internal.sql.engine.exec.exp.RangeCondition;
import org.apache.ignite.internal.sql.engine.exec.exp.RangeIterable;
import org.apache.ignite.internal.util.CollectionUtils;
import org.apache.ignite.internal.util.Cursor;
@@ -57,7 +59,7 @@ public abstract class AbstractIndexScan<RowT, IdxRowT>
implements Iterable<RowT>
* @param ranges Index scan bounds.
* @param rowTransformer Row transformer.
*/
- protected AbstractIndexScan(
+ AbstractIndexScan(
ExecutionContext<RowT> ectx,
RelDataType rowType,
TreeIndex<IdxRowT> idx,
@@ -79,13 +81,21 @@ public abstract class AbstractIndexScan<RowT, IdxRowT>
implements Iterable<RowT>
if (ranges == null) { // Full index scan.
Cursor<IdxRowT> cursor = idx.find(null, null, true, true);
- Iterator<RowT> it = new TransformingIterator<>(cursor,
AbstractIndexScan.this::indexRow2Row);
+ Iterator<RowT> it = new TransformingIterator<>(cursor,
this::indexRow2Row);
it = (filters != null) ? new FilteringIterator<>(it, filters) : it;
return (rowTransformer != null) ? new TransformingIterator<>(it,
rowTransformer) : it;
}
+ if (!ranges.multiBounds()) {
+ Iterator<RangeCondition<RowT>> it = ranges.iterator();
+
+ if (!it.hasNext()) {
+ return Collections.emptyIterator();
+ }
+ }
+
Iterable<RowT>[] iterables = Streams.stream(ranges)
.map(range -> new Iterable<RowT>() {
@Override
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/TreeIndex.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/TreeIndex.java
index 5d40c5d5a7..aaf11c6da5 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/TreeIndex.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/TreeIndex.java
@@ -18,6 +18,7 @@
package org.apache.ignite.internal.sql.engine.exec;
import org.apache.ignite.internal.util.Cursor;
+import org.jetbrains.annotations.Nullable;
/**
* Tree index interface.
@@ -34,5 +35,5 @@ public interface TreeIndex<R> {
* @param upperInclude Inclusive upper bound.
* @return Cursor over the rows within bounds.
*/
- Cursor<R> find(R lower, R upper, boolean lowerInclude, boolean
upperInclude);
+ Cursor<R> find(@Nullable R lower, @Nullable R upper, boolean lowerInclude,
boolean upperInclude);
}
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 7e2e655321..e429ab00b4 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
@@ -722,6 +722,9 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
/** Upper row. */
private @Nullable RowT upperRow;
+ /** Cached skip range flag. */
+ private Boolean skip;
+
/** Row factory. */
private final RowFactory<RowT> factory;
@@ -768,12 +771,44 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
RowT res = factory.create();
scalar.execute(ctx, null, res);
+ RowHandler<RowT> hnd = ctx.rowHandler();
+
+ // Check bound for NULL values. If bound contains NULLs, the whole
range should be skipped.
+ // There is special placeholder for searchable NULLs, make this
replacement here too.
+ for (int i = 0; i < hnd.columnCount(res); i++) {
+ Object fldVal = hnd.get(i, res);
+
+ if (fldVal == null) {
+ skip = Boolean.TRUE;
+ }
+
+ if (fldVal == ctx.nullBound()) {
+ hnd.set(i, res, null);
+ }
+ }
+
return res;
}
/** Clear cached rows. */
void clearCache() {
lowerRow = upperRow = null;
+ skip = null;
+ }
+
+ /** Skip this range. */
+ public boolean skip() {
+ if (skip == null) {
+ // Precalculate skip flag.
+ lower();
+ upper();
+
+ if (skip == null) {
+ skip = Boolean.FALSE;
+ }
+ }
+
+ return skip;
}
}
@@ -793,9 +828,8 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
}
/** {@inheritDoc} */
- @Override
- public int size() {
- return ranges.size();
+ @Override public boolean multiBounds() {
+ return ranges.size() > 1;
}
/** {@inheritDoc} */
@@ -804,7 +838,11 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
ranges.forEach(b -> ((RangeConditionImpl) b).clearCache());
if (ranges.size() == 1) {
- return ranges.iterator();
+ if (((RangeConditionImpl) ranges.get(0)).skip()) {
+ return Collections.emptyIterator();
+ } else {
+ return ranges.iterator();
+ }
}
// Sort ranges using collation comparator to produce sorted
output. There should be no ranges
@@ -816,7 +854,7 @@ public class ExpressionFactoryImpl<RowT> implements
ExpressionFactory<RowT> {
sorted = true;
}
- return ranges.iterator();
+ return ranges.stream().filter(r -> !((RangeConditionImpl)
r).skip()).iterator();
}
}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RangeIterable.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RangeIterable.java
index 773b16a9a8..7684164cbb 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RangeIterable.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RangeIterable.java
@@ -21,6 +21,10 @@ package org.apache.ignite.internal.sql.engine.exec.exp;
* Iterable over range conditions.
*/
public interface RangeIterable<RowT> extends Iterable<RangeCondition<RowT>> {
- /** Count of ranges in iterable. */
- public int size();
+ /**
+ * Multi bounds condition.
+ *
+ * @return {@code True} if iterable can have more than one item.
+ */
+ public boolean multiBounds();
}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNode.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNode.java
index 0426df0fb6..fa54d78f71 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNode.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNode.java
@@ -96,7 +96,6 @@ public class IndexScanNode<RowT> extends
StorageScanNode<RowT> {
super(ctx, rowFactory, schemaTable, filters, rowTransformer,
requiredColumns);
assert partsWithTerms != null && !partsWithTerms.isEmpty();
- assert rangeConditions == null || rangeConditions.size() > 0;
this.schemaIndex = schemaIndex;
this.partsWithTerms = partsWithTerms;
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
index cb7f84d74d..ba2c56b36d 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
@@ -57,7 +57,7 @@ public abstract class AbstractIndexScan extends
ProjectableFilterableTableScan {
*
* @param input Serialized representation.
*/
- protected AbstractIndexScan(RelInput input) {
+ AbstractIndexScan(RelInput input) {
super(input);
idxName = input.getString("index");
type = input.getEnum("type", IgniteIndex.Type.class);
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNodeExecutionTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNodeExecutionTest.java
index e38cbd9c89..78e53ff7dc 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNodeExecutionTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNodeExecutionTest.java
@@ -431,7 +431,7 @@ public class IndexScanNodeExecutionTest extends
AbstractExecutionTest {
when(rangeIterable.iterator()).thenReturn(mockIterator);
when(mockIterator.hasNext()).thenReturn(true, false);
when(mockIterator.next()).thenReturn(range);
- when(rangeIterable.size()).thenReturn(1);
+ when(rangeIterable.multiBounds()).thenReturn(false);
}
IndexScanNode<Object[]> scanNode = new IndexScanNode<>(
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/SortedIndexSpoolExecutionTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/SortedIndexSpoolExecutionTest.java
index 13945d9373..9a7467b44d 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/SortedIndexSpoolExecutionTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/SortedIndexSpoolExecutionTest.java
@@ -253,9 +253,8 @@ public class SortedIndexSpoolExecutionTest extends
AbstractExecutionTest {
}
/** {@inheritDoc} */
- @Override
- public int size() {
- return 1;
+ @Override public boolean multiBounds() {
+ return false;
}
/** {@inheritDoc} */
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
index 783395754b..112c896b11 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
@@ -67,9 +67,7 @@ import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rel.type.RelDataTypeImpl;
import org.apache.calcite.rel.type.RelProtoDataType;
import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.schema.ColumnStrategy;
import org.apache.calcite.schema.Schema;
import org.apache.calcite.schema.SchemaPlus;
@@ -105,8 +103,6 @@ import
org.apache.ignite.internal.sql.engine.prepare.MappingQueryContext;
import org.apache.ignite.internal.sql.engine.prepare.PlannerHelper;
import org.apache.ignite.internal.sql.engine.prepare.PlanningContext;
import org.apache.ignite.internal.sql.engine.prepare.Splitter;
-import org.apache.ignite.internal.sql.engine.prepare.bounds.ExactBounds;
-import org.apache.ignite.internal.sql.engine.prepare.bounds.RangeBounds;
import org.apache.ignite.internal.sql.engine.prepare.bounds.SearchBounds;
import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
@@ -146,7 +142,7 @@ public abstract class AbstractPlannerTest extends
IgniteAbstractTest {
protected static final int DEFAULT_ZONE_ID = 0;
/** Last error message. */
- private String lastErrorMsg;
+ String lastErrorMsg;
interface TestVisitor {
void visit(RelNode node, int ordinal, RelNode parent);
@@ -1298,46 +1294,6 @@ public abstract class AbstractPlannerTest extends
IgniteAbstractTest {
}
}
- Predicate<SearchBounds> exact(Object val) {
- return b -> b instanceof ExactBounds && matchValue(val, ((ExactBounds)
b).bound());
- }
-
- void assertBounds(String sql, List<Object> params, IgniteSchema schema,
Predicate<SearchBounds>... predicates) throws Exception {
- assertPlan(sql, schema,
nodeOrAnyChild(isInstanceOf(IgniteIndexScan.class)
- .and(scan -> matchBounds(scan.searchBounds(), predicates))),
params);
- }
-
- boolean matchBounds(List<SearchBounds> searchBounds,
Predicate<SearchBounds>... predicates) {
- for (int i = 0; i < predicates.length; i++) {
- if (!predicates[i].test(searchBounds.get(i))) {
- lastErrorMsg = "Not expected bounds: " + searchBounds.get(i);
-
- return false;
- }
- }
-
- return true;
- }
-
- Predicate<SearchBounds> range(Object lower, Object upper, boolean
lowerInclude, boolean upperInclude) {
- return b -> b instanceof RangeBounds
- && matchValue(lower, ((RangeBounds) b).lowerBound())
- && matchValue(upper, ((RangeBounds) b).upperBound())
- && lowerInclude == ((RangeBounds) b).lowerInclude()
- && upperInclude == ((RangeBounds) b).upperInclude();
- }
-
- private boolean matchValue(Object val, RexNode bound) {
- if (val == null || bound == null) {
- return val == bound;
- }
-
- bound = RexUtil.removeCast(bound);
-
- return Objects.toString(val).equals(Objects.toString(
- bound instanceof RexLiteral ? ((RexLiteral)
bound).getValueAs(val.getClass()) : bound));
- }
-
Predicate<SearchBounds> empty() {
return Objects::isNull;
}
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
new file mode 100644
index 0000000000..a19b1edc18
--- /dev/null
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/IndexSearchBoundsPlannerTest.java
@@ -0,0 +1,510 @@
+/*
+ * 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.planner;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.Predicate;
+import java.util.stream.IntStream;
+import org.apache.calcite.rel.RelCollations;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.ignite.internal.index.ColumnCollation;
+import org.apache.ignite.internal.sql.engine.metadata.ColocationGroup;
+import org.apache.ignite.internal.sql.engine.prepare.MappingQueryContext;
+import org.apache.ignite.internal.sql.engine.prepare.bounds.ExactBounds;
+import org.apache.ignite.internal.sql.engine.prepare.bounds.MultiBounds;
+import org.apache.ignite.internal.sql.engine.prepare.bounds.RangeBounds;
+import org.apache.ignite.internal.sql.engine.prepare.bounds.SearchBounds;
+import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
+import org.apache.ignite.internal.sql.engine.rel.IgniteUnionAll;
+import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
+import org.apache.ignite.internal.sql.engine.trait.IgniteDistribution;
+import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
+import org.apache.ignite.internal.sql.engine.trait.TraitUtils;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem;
+import org.apache.ignite.internal.sql.engine.util.RexUtils;
+import org.jetbrains.annotations.Nullable;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Index bounds check tests.
+ */
+public class IndexSearchBoundsPlannerTest extends AbstractPlannerTest {
+ private static List<String> NODES = new ArrayList<>(4);
+
+ private IgniteSchema publicSchema;
+
+ private TestTable tbl;
+
+ @BeforeAll
+ public static void init() {
+ IntStream.rangeClosed(0, 3).forEach(i ->
NODES.add(UUID.randomUUID().toString()));
+ }
+
+ @BeforeEach
+ public void beforeEach() {
+ IgniteTypeFactory typeFactory = new
IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
+
+ tbl = new TestTable(
+ new RelDataTypeFactory.Builder(typeFactory)
+ .add("C1",
typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.INTEGER),
true))
+ .add("C2",
typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.VARCHAR),
true))
+ .add("C3",
typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.INTEGER),
true))
+ .add("C4",
typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.INTEGER),
true))
+ .build(), "TEST") {
+ @Override
+ public ColocationGroup colocationGroup(MappingQueryContext ctx) {
+ return ColocationGroup.forNodes(select(NODES, 0));
+ }
+
+ @Override
+ public IgniteDistribution distribution() {
+ return IgniteDistributions.single();
+ }
+ };
+
+ tbl.addIndex("C1C2C3", 0, 1, 2);
+
+ publicSchema = new IgniteSchema("PUBLIC");
+
+ publicSchema.addTable(tbl);
+ }
+
+ /** Simple case on one field, without multi tuple SEARCH/SARG. */
+ @Test
+ public void testBoundsOneFieldSingleTuple() throws Exception {
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1", exact(1));
+
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C3 = 1", exact(1),
empty(), empty());
+
+ assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C1 <= 3",
+ range(1, 3, false, true));
+
+ assertBounds("SELECT * FROM TEST WHERE C1 < 3 AND C1 IS NOT NULL",
+ range(null, 3, true, false));
+
+ // Redundant "IS NOT NULL condition".
+ assertBounds("SELECT * FROM TEST WHERE C1 > 3 AND C1 IS NOT NULL",
+ range(3, "$NULL_BOUND()", false, false));
+
+ // C4 field not in collation.
+ assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C1 <= 3 AND C4 = 1",
+ range(1, 3, false, true),
+ empty(),
+ empty()
+ );
+
+ // Cannot proceed to C3 without C2.
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C3 = 1",
+ exact(1),
+ empty(),
+ empty()
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C1 <= 3 AND C3 = 1",
+ range(1, 3, false, true),
+ empty(),
+ empty()
+ );
+ }
+
+ /** Simple SEARCH/SARG. */
+ @Test
+ public void testBoundsOneFieldSearch() throws Exception {
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3)",
+ multi(exact(1), exact(2), exact(3)));
+
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C1 IS NOT
NULL",
+ multi(exact(1), exact(2), exact(3)));
+
+ assertBounds("SELECT * FROM TEST WHERE (C1 > 1 AND C1 < 3) OR C1 IN
(4, 5) OR C1 = 6 OR C1 > 7",
+ multi(
+ range(1, 3, false, false),
+ exact(4),
+ exact(5),
+ exact(6),
+ range(7, "$NULL_BOUND()", false, false)));
+ }
+
+ /** Simple SEARCH/SARG, values deduplication. */
+ @Test
+ public void testBoundsOneFieldSearchDeduplication() throws Exception {
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3, 2, 1)",
+ multi(exact(1), exact(2), exact(3)));
+ }
+
+ /** Simple SEARCH/SARG, range optimization. */
+ @Test
+ public void testBoundsOneFieldSearchRangeOptimization() throws Exception {
+ assertBounds("SELECT * FROM TEST WHERE (C1 > 1 AND C1 < 4) OR (C1 > 3
AND C1 < 5) OR (C1 > 7) OR (C1 > 6)",
+ multi(
+ range(1, 5, false, false),
+ range(6, "$NULL_BOUND()", false, false)));
+
+ assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C1 < 3 AND C1 <> 2",
+ multi(
+ range(1, 2, false, false),
+ range(2, 3, false, false)));
+
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND "
+ + "((C2 > '1' AND C2 < '3') OR (C2 > '11' AND C2 <
'33') OR C2 > '4')",
+ exact(1),
+ multi(
+ range("1", "33", false, false),
+ range("4", "$NULL_BOUND()", false, false)));
+
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND (C2 > '1' OR C2 <
'3')",
+ exact(1));
+ }
+
+ /** Simple SEARCH/SARG with "IS NULL" condition. */
+ @Test
+ public void testBoundsOneFieldSearchWithNull() throws Exception {
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) OR C1 IS NULL",
+ multi(exact("$NULL_BOUND()"), exact(1), exact(2), exact(3)),
+ empty(),
+ empty()
+ );
+ }
+
+ /** Tests bounds with DESC ordering. */
+ @Test
+ public void testBoundsDescOrdering() throws Exception {
+ tbl.addIndex(RelCollations.of(TraitUtils.createFieldCollation(3,
ColumnCollation.DESC_NULLS_LAST),
+ TraitUtils.createFieldCollation(2,
ColumnCollation.ASC_NULLS_FIRST)), "C4");
+
+ tbl.addIndex(RelCollations.of(TraitUtils.createFieldCollation(3,
ColumnCollation.ASC_NULLS_FIRST),
+ TraitUtils.createFieldCollation(2,
ColumnCollation.ASC_NULLS_FIRST)), "C4IDX");
+
+ assertBounds("SELECT * FROM TEST WHERE C4 > 1",
+ range(null, 1, true, false));
+
+ assertBounds("SELECT * FROM TEST WHERE C4 < 1",
+ range(1, "$NULL_BOUND()", false, false));
+
+ assertBounds("SELECT * FROM TEST WHERE C4 IS NULL",
exact("$NULL_BOUND()"));
+
+ assertBounds("SELECT * FROM TEST WHERE C4 IS NOT NULL",
+ range(null, "$NULL_BOUND()", true, false));
+
+ assertBounds("SELECT * FROM TEST WHERE C4 IN (1, 2, 3) AND C3 > 1",
+ multi(exact(1), exact(2), exact(3)),
+ range(1, null, false, true)
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE ((C4 > 1 AND C4 < 5) OR (C4 > 7
AND C4 < 9)) AND C3 = 1",
+ multi(
+ range(5, 1, false, false),
+ range(9, 7, false, false))
+ );
+ }
+
+ /** Tests bounds with conditions on several fields. */
+ @Test
+ public void testBoundsSeveralFieldsSearch() throws Exception {
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 IN ('a', 'b')",
+ exact(1),
+ multi(exact("a"), exact("b"))
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 > 'a'",
+ exact(1),
+ range("a", "$NULL_BOUND()", false, false)
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C2 = 'a'",
+ multi(exact(1), exact(2), exact(3)),
+ exact("a")
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C2 IN ('a',
'b')",
+ multi(exact(1), exact(2), exact(3)),
+ multi(exact("a"), exact("b"))
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C2 IN ('a',
'b') AND C3 IN (4, 5) AND C4 = 1",
+ multi(exact(1), exact(2), exact(3)),
+ multi(exact("a"), exact("b")),
+ multi(exact(4), exact(5))
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C2 IN ('a',
'b') AND C3 > 4",
+ multi(exact(1), exact(2), exact(3)),
+ multi(exact("a"), exact("b")),
+ range(4, "$NULL_BOUND()", false, false)
+ );
+
+ // Cannot proceed to the next field after the range condition.
+ assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C2 = 'a'",
+ range(1, "$NULL_BOUND()", false, false),
+ empty(),
+ empty()
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C2 > 'a'",
+ range(1, "$NULL_BOUND()", false, false),
+ empty(),
+ empty()
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-13568 Fix to
exact("a")
+ assertBounds("SELECT * FROM TEST WHERE C1 >= 1 AND C2 = 'a'",
+ range(1, "$NULL_BOUND()", true, false),
+ empty()
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-13568 Fix to
range("a", null, false, true)
+ assertBounds("SELECT * FROM TEST WHERE C1 >= 1 AND C2 > 'a'",
+ range(1, "$NULL_BOUND()", true, false),
+ empty()
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 >= 1 AND C2 < 'a'",
+ range(1, "$NULL_BOUND()", true, false),
+ empty()
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 >= 1 AND C2 IN ('a', 'b')",
+ range(1, "$NULL_BOUND()", true, false),
+ empty()
+ );
+
+ // Cannot proceed to the next field after SEARCH/SARG with range
condition.
+ assertBounds("SELECT * FROM TEST WHERE ((C1 > 1 AND C1 < 3) OR C1 > 5)
AND C2 = 'a'",
+ multi(
+ range(1, 3, false, false),
+ range(5, "$NULL_BOUND()", false, false)),
+ empty()
+ );
+ }
+
+ /** Tests max complexity of SEARCH/SARG to include into index scan. */
+ @Test
+ public void testBoundsMaxComplexity() throws Exception {
+ int limit = RexUtils.MAX_SEARCH_BOUNDS_COMPLEXITY;
+
+ String inVals = String.join(", ", IntStream.range(0, limit +
1).mapToObj(Integer::toString)
+ .toArray(String[]::new));
+
+ assertPlan("SELECT * FROM TEST WHERE C1 IN (" + inVals + ")",
publicSchema, isTableScan("TEST"));
+
+ inVals = String.join(", ", IntStream.range(0, limit / 10 +
1).mapToObj(Integer::toString)
+ .toArray(String[]::new));
+
+ assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3, 4, 5) AND C2 IN
('a', 'b') AND C3 IN (" + inVals + ")",
+ multi(exact(1), exact(2), exact(3), exact(4), exact(5)),
+ multi(exact("a"), exact("b")),
+ empty()
+ );
+ }
+
+ /** Tests bounds with wrong literal types. */
+ @Test
+ public void testBoundsTypeConversion() throws Exception {
+ // Implicit cast of all filter values to INTEGER.
+ assertBounds("SELECT * FROM TEST WHERE C1 IN ('1', '2', '3')",
+ multi(exact(1), exact(2), exact(3))
+ );
+
+ // Implicit cast of '1' to INTEGER.
+ assertBounds("SELECT * FROM TEST WHERE C1 IN ('1', 2, 3)",
+ multi(exact(1), exact(2), exact(3))
+ );
+
+ // Casted to INTEGER type C2 column cannot be used as index bound.
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 > 1",
+ exact(1),
+ empty()
+ );
+
+ // Casted to INTEGER type C2 column cannot be used as index bound.
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 IN (2, 3)",
+ exact(1),
+ empty()
+ );
+
+ // Implicit cast of 2 to VARCHAR.
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 IN (2, '3')",
+ exact(1),
+ multi(exact("2"), exact("3"))
+ );
+ }
+
+ /** 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.
+ 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 = ? AND C2 IN ('a', 'b')",
List.of(1), publicSchema,
+ exact("?0"),
+ multi(exact("a"), exact("b"))
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 = ? AND C2 > ? AND C2 < ?",
List.of(1, 'a', 'w'), publicSchema,
+ exact("?0"),
+ range("?1", "?2", false, false)
+ );
+ }
+
+ /** Tests bounds with correlated value. */
+ @Test
+ public void testBoundsWithCorrelate() throws Exception {
+ assertBounds("SELECT (SELECT C1 FROM TEST t2 WHERE t2.C1 = t1.C1) FROM
TEST t1",
+ exact("$cor0.C1")
+ );
+
+ assertBounds(
+ "SELECT (SELECT C1 FROM TEST t2 WHERE C1 = 1 AND C2 = 'a' AND
C3 IN (t1.C3, 0, 1, 2)) FROM TEST t1",
+ exact(1),
+ exact("a"),
+ empty()
+ );
+ }
+
+ /** Tests bounds merge. */
+ @Test
+ public void testBoundsMerge() throws Exception {
+ assertBounds("SELECT * FROM TEST WHERE C1 > ? AND C1 >= 1",
List.of(10), publicSchema,
+ range("$GREATEST2(?0, 1)", "$NULL_BOUND()", true, false)
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 > ? AND C1 >= ? AND C1 > ?",
List.of(10, 10, 10), publicSchema,
+ range("$GREATEST2($GREATEST2(?0, ?1), ?2)", "$NULL_BOUND()",
true, false)
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 > ? AND C1 >= 1 AND C1 < ?
AND C1 < ?", List.of(10, 10, 10), publicSchema,
+ range("$GREATEST2(?0, 1)", "$LEAST2(?1, ?2)", true, false)
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 < ? AND C1 BETWEEN 1 AND 10
", List.of(10), publicSchema,
+ range(1, "$LEAST2(?0, 10)", true, true)
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 NOT IN (1, 2) AND C1 >= ?",
List.of(10), publicSchema,
+ range("?0", "$NULL_BOUND()", true, false)
+ );
+
+ tbl.addIndex(RelCollations.of(TraitUtils.createFieldCollation(3,
ColumnCollation.DESC_NULLS_LAST),
+ TraitUtils.createFieldCollation(2,
ColumnCollation.ASC_NULLS_FIRST)), "C4");
+
+ assertBounds("SELECT * FROM TEST WHERE C4 > ? AND C4 >= 1 AND C4 < ?
AND C4 < ?", List.of(10, 10, 10), publicSchema,
+ range("$LEAST2(?1, ?2)", "$GREATEST2(?0, 1)", false, true)
+ );
+ }
+
+ /** Tests complex bounds expressions. */
+ @Test
+ public void testBoundsComplex() throws Exception {
+ assertBounds("SELECT * FROM TEST WHERE C1 = ? + 10", List.of(1),
publicSchema,
+ exact("+(?0, 10)")
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 >
SUBSTRING(?::VARCHAR, 1, 2) || '3'", List.of('1'), publicSchema,
+ exact(1),
+ range("||(SUBSTRING(?0, 1, 2), _UTF-8'3')", "$NULL_BOUND()",
false, false)
+ );
+
+ assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 >
SUBSTRING(C3::VARCHAR, 1, 2) || '3'",
+ exact(1),
+ empty()
+ );
+
+ assertBounds("SELECT (SELECT C1 FROM TEST t2 WHERE t2.C1 = t1.C1 +
t1.C3 * ?) FROM TEST t1", List.of(1), publicSchema,
+ exact("+($cor0.C1, *($cor0.C3, ?0))")
+ );
+
+ assertPlan("SELECT * FROM TEST WHERE C1 = ? + C3", publicSchema,
isTableScan("TEST"), List.of(1));
+
+ assertPlan("SELECT (SELECT C1 FROM TEST t2 WHERE t2.C1 < t1.C1 +
t2.C1) FROM TEST t1", publicSchema,
+ nodeOrAnyChild(isIndexScan("TEST", "C1C2C3")).negate());
+ }
+
+ private static Predicate<SearchBounds> exact(Object val) {
+ return b -> b instanceof ExactBounds && matchValue(val, ((ExactBounds)
b).bound());
+ }
+
+ private Predicate<SearchBounds> multi(Predicate<SearchBounds>...
predicates) {
+ return b -> b instanceof MultiBounds
+ && ((MultiBounds) b).bounds().size() == predicates.length
+ && matchBounds(((MultiBounds) b).bounds(), predicates);
+ }
+
+ private void assertBounds(String sql, Predicate<SearchBounds>...
predicates) throws Exception {
+ assertPlan(sql, publicSchema,
nodeOrAnyChild(isInstanceOf(IgniteIndexScan.class)
+ .and(scan -> matchBounds(scan.searchBounds(), predicates))),
List.of());
+ }
+
+ private void assertBounds(
+ String sql,
+ List<Object> params,
+ IgniteSchema schema,
+ Predicate<SearchBounds>... predicates
+ ) throws Exception {
+ assertPlan(sql, schema,
nodeOrAnyChild(isInstanceOf(IgniteIndexScan.class)
+ .and(scan -> matchBounds(scan.searchBounds(), predicates))),
params);
+ }
+
+ private boolean matchBounds(List<SearchBounds> searchBounds,
Predicate<SearchBounds>... predicates) {
+ for (int i = 0; i < predicates.length; i++) {
+ if (!predicates[i].test(searchBounds.get(i))) {
+ lastErrorMsg = "Not expected bounds: " + searchBounds.get(i);
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static Predicate<SearchBounds> range(
+ @Nullable Object lower,
+ @Nullable Object upper,
+ boolean lowerInclude,
+ boolean upperInclude
+ ) {
+ return b -> b instanceof RangeBounds
+ && matchValue(lower, ((RangeBounds) b).lowerBound())
+ && matchValue(upper, ((RangeBounds) b).upperBound())
+ && lowerInclude == ((RangeBounds) b).lowerInclude()
+ && upperInclude == ((RangeBounds) b).upperInclude();
+ }
+
+ private static boolean matchValue(@Nullable Object val, RexNode bound) {
+ if (val == null || bound == null) {
+ return val == bound;
+ }
+
+ bound = RexUtil.removeCast(bound);
+
+ return Objects.toString(val).equals(Objects.toString(
+ bound instanceof RexLiteral ? ((RexLiteral)
bound).getValueAs(val.getClass()) : bound));
+ }
+}
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
index 2a6ab9f3d1..dec8c44b32 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
@@ -43,7 +43,6 @@ import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.util.ImmutableIntList;
import org.apache.calcite.util.Util;
-import org.apache.ignite.internal.index.ColumnCollation;
import org.apache.ignite.internal.sql.engine.metadata.ColocationGroup;
import org.apache.ignite.internal.sql.engine.metadata.MappingService;
import org.apache.ignite.internal.sql.engine.metadata.NodeWithTerm;
@@ -63,7 +62,6 @@ import
org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
import org.apache.ignite.internal.sql.engine.trait.IgniteDistribution;
import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
-import org.apache.ignite.internal.sql.engine.trait.TraitUtils;
import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem;
import org.apache.ignite.internal.sql.engine.util.BaseQueryContext;
@@ -436,122 +434,6 @@ public class PlannerTest extends AbstractPlannerTest {
assertEquals(3, plan.fragments().size());
}
- /** Tests bounds merge. */
- @Test
- public void testBoundsMerge() throws Exception {
- IgniteTypeFactory typeFactory = new
IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
-
- TestTable tbl = new TestTable(
- new RelDataTypeFactory.Builder(typeFactory)
- .add("C1", typeFactory.createJavaType(Integer.class))
- .add("C2", typeFactory.createJavaType(Integer.class))
- .add("C3", typeFactory.createJavaType(Integer.class))
- .add("C4", typeFactory.createJavaType(Integer.class))
- .build(), "TEST") {
- @Override
- public ColocationGroup colocationGroup(MappingQueryContext ctx) {
- return ColocationGroup.forNodes(select(NODES, 0));
- }
-
- @Override
- public IgniteDistribution distribution() {
- return IgniteDistributions.hash(List.of(0));
- }
- };
-
- tbl.addIndex("C1C2C3", 0, 1, 2);
-
- IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
-
- publicSchema.addTable(tbl);
-
- assertBounds("SELECT * FROM TEST WHERE C1 > ? AND C1 >= 1",
List.of(10), publicSchema,
- range("$GREATEST2(?0, 1)", "$NULL_BOUND()", true, false)
- );
-
- assertBounds("SELECT * FROM TEST WHERE C1 > ? AND C1 >= ? AND C1 > ?",
List.of(10, 10, 10), publicSchema,
- range("$GREATEST2($GREATEST2(?0, ?1), ?2)", "$NULL_BOUND()",
true, false)
- );
-
- assertBounds("SELECT * FROM TEST WHERE C1 > ? AND C1 >= 1 AND C1 < ?
AND C1 < ?", List.of(10, 10, 10), publicSchema,
- range("$GREATEST2(?0, 1)", "$LEAST2(?1, ?2)", true, false)
- );
-
- assertBounds("SELECT * FROM TEST WHERE C1 < ? AND C1 BETWEEN 1 AND 10
", List.of(10), publicSchema,
- range(1, "$LEAST2(?0, 10)", true, true)
- );
-
- assertBounds("SELECT * FROM TEST WHERE C1 NOT IN (1, 2) AND C1 >= ?",
List.of(10), publicSchema,
- range("?0", "$NULL_BOUND()", true, false)
- );
-
- tbl.addIndex(RelCollations.of(TraitUtils.createFieldCollation(3,
ColumnCollation.DESC_NULLS_LAST),
- TraitUtils.createFieldCollation(2,
ColumnCollation.ASC_NULLS_FIRST)), "C4");
-
- assertBounds("SELECT * FROM TEST WHERE C4 > ? AND C4 >= 1 AND C4 < ?
AND C4 < ?", List.of(10, 10, 10), publicSchema,
- range("$LEAST2(?1, ?2)", "$GREATEST2(?0, 1)", false, true)
- );
- }
-
- /** Tests complex bounds expressions. */
- @Test
- public void testBoundsComplex() throws Exception {
- IgniteTypeFactory typeFactory = new
IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
-
- TestTable tbl = new TestTable(
- new RelDataTypeFactory.Builder(typeFactory)
- .add("C1", typeFactory.createJavaType(Integer.class))
- .add("C2", typeFactory.createJavaType(Integer.class))
- .add("C3", typeFactory.createJavaType(Integer.class))
- .add("C4", typeFactory.createJavaType(Integer.class))
- .build(), "TEST") {
- @Override
- public ColocationGroup colocationGroup(MappingQueryContext ctx) {
- return ColocationGroup.forNodes(select(NODES, 0));
- }
-
- @Override
- public IgniteDistribution distribution() {
- return IgniteDistributions.hash(List.of(0));
- }
- };
-
- tbl.addIndex("C1C2C3", 0, 1, 2);
-
- IgniteSchema publicSchema = new IgniteSchema("PUBLIC");
-
- publicSchema.addTable(tbl);
-
- assertBounds("SELECT * FROM TEST WHERE C1 = ? + 10", List.of(10),
publicSchema,
- exact("+(?0, 10)")
- );
-
- assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 >
SUBSTRING(?::VARCHAR, 1, 2) || '3'", List.of("10"), publicSchema,
- exact(1),
- range("||(SUBSTRING(?0, 1, 2), _UTF-8'3')", "$NULL_BOUND()",
false, false)
- );
-
- assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 >
SUBSTRING(C3::VARCHAR, 1, 2) || '3'", List.of(), publicSchema,
- exact(1),
- empty()
- );
-
- assertBounds("SELECT (SELECT C1 FROM TEST t2 WHERE t2.C1 = t1.C1 +
t1.C3 * ?) FROM TEST t1", List.of(10), publicSchema,
- exact("+($cor0.C1, *($cor0.C3, ?0))")
- );
-
- assertPlan("SELECT * FROM TEST WHERE C1 = ? + C3", publicSchema,
nodeOrAnyChild(isTableScan("TEST")), List.of(10));
-
- assertPlan("SELECT (SELECT C1 FROM TEST t2 WHERE t2.C1 < t1.C1 +
t2.C1) FROM TEST t1", publicSchema,
- nodeOrAnyChild(isIndexScan("TEST", "C1C2C3")).negate());
- }
-
- /** String representation of LEAST or GREATEST operator converted to CASE.
*/
- private String leastOrGreatest(boolean least, String val0, String val1,
String type) {
- return "CASE(OR(IS NULL(" + val0 + "), IS NULL(" + val1 + ")), null:"
+ type + ", " + (least ? '<' : '>')
- + '(' + val0 + ", " + val1 + "), " + val0 + ", " + val1 + ')';
- }
-
@Test
public void testSplitterPartiallyColocated2() throws Exception {
IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);