You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by al...@apache.org on 2022/02/04 12:27:33 UTC

[ignite] branch sql-calcite updated: IGNITE-16443 Fix nulls processing in search rows - Fixes #9792.

This is an automated email from the ASF dual-hosted git repository.

alexpl pushed a commit to branch sql-calcite
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/sql-calcite by this push:
     new 4757b3f  IGNITE-16443 Fix nulls processing in search rows - Fixes #9792.
4757b3f is described below

commit 4757b3f468243fd049b61e453dd3e5c14a0a9547
Author: Aleksey Plekhanov <pl...@gmail.com>
AuthorDate: Fri Feb 4 15:15:15 2022 +0300

    IGNITE-16443 Fix nulls processing in search rows - Fixes #9792.
    
    Signed-off-by: Aleksey Plekhanov <pl...@gmail.com>
---
 .../query/calcite/exec/ExecutionContext.java       |   8 ++
 .../processors/query/calcite/exec/IndexScan.java   |   2 +-
 .../query/calcite/exec/RuntimeHashIndex.java       |  31 ++++-
 .../query/calcite/exec/SystemViewScan.java         |   2 +-
 .../query/calcite/exec/exp/ExpressionFactory.java  |   2 +-
 .../calcite/exec/exp/ExpressionFactoryImpl.java    | 152 ++++++++++++---------
 .../query/calcite/util/IgniteMethod.java           |   2 +
 .../processors/query/calcite/util/RexUtils.java    |   7 +-
 .../calcite/exec/rel/AbstractExecutionTest.java    |  15 ++
 .../exec/rel/HashIndexSpoolExecutionTest.java      |  60 ++++++--
 .../exec/rel/SortedIndexSpoolExecutionTest.java    |  81 +++++++++--
 .../integration/HashSpoolIntegrationTest.java      |  67 +++++++++
 .../integration/IndexScanlIntegrationTest.java     | 146 ++++++++++++++++++++
 .../CorrelatedNestedLoopJoinPlannerTest.java       |  10 +-
 .../calcite/planner/HashIndexSpoolPlannerTest.java |  24 ++--
 .../planner/SortedIndexSpoolPlannerTest.java       |  22 ++-
 .../ignite/testsuites/IntegrationTestSuite.java    |   4 +
 .../query/h2/opt/GridH2ValueCacheObject.java       |  20 +--
 18 files changed, 499 insertions(+), 156 deletions(-)

diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionContext.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionContext.java
index dad750c..71fbeb6 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionContext.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionContext.java
@@ -49,6 +49,9 @@ import static org.apache.ignite.internal.processors.query.calcite.util.Commons.c
  * Runtime context allowing access to the tables in a database.
  */
 public class ExecutionContext<Row> extends AbstractQueryContext implements DataContext {
+    /** Placeholder for values, which expressions is not specified. */
+    private static final Object UNSPECIFIED_VALUE = new Object();
+
     /** */
     private final UUID qryId;
 
@@ -324,6 +327,11 @@ public class ExecutionContext<Row> extends AbstractQueryContext implements DataC
         return cancelFlag.get();
     }
 
+    /** */
+    public Object unspecifiedValue() {
+        return UNSPECIFIED_VALUE;
+    }
+
     /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (this == o)
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java
index b7f304f..419ff7b 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexScan.java
@@ -183,7 +183,7 @@ public class IndexScan<Row> extends AbstractIndexScan<Row, IndexRow> {
             int fieldIdx = idxFieldMapping.getInt(i);
             Object key = rowHnd.get(fieldIdx, bound);
 
-            if (key != null) {
+            if (key != ectx.unspecifiedValue()) {
                 key = TypeUtils.fromInternal(ectx, key, fieldsStoreTypes[fieldIdx]);
 
                 keys[i] = IndexKeyFactory.wrap(key, idxRowHnd.indexKeyDefinitions().get(i).idxType(),
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeHashIndex.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeHashIndex.java
index 1666ba2..303103f 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeHashIndex.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/RuntimeHashIndex.java
@@ -26,12 +26,19 @@ import java.util.function.Supplier;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.GroupKey;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.X;
 import org.jetbrains.annotations.NotNull;
 
 /**
  * Runtime hash index based on on-heap hash map.
  */
 public class RuntimeHashIndex<Row> implements RuntimeIndex<Row> {
+    /**
+     * Placeholder for keys containing NULL values. Used to skip rows with such keys, since condition NULL=NULL
+     * should not satisfy the filter.
+     */
+    private static final GroupKey NULL_KEY = new GroupKey(X.EMPTY_OBJECT_ARRAY);
+
     /** */
     protected final ExecutionContext<Row> ectx;
 
@@ -58,7 +65,12 @@ public class RuntimeHashIndex<Row> implements RuntimeIndex<Row> {
 
     /** {@inheritDoc} */
     @Override public void push(Row r) {
-        List<Row> eqRows = rows.computeIfAbsent(key(r), k -> new ArrayList<>());
+        GroupKey key = key(r);
+
+        if (key == NULL_KEY)
+            return;
+
+        List<Row> eqRows = rows.computeIfAbsent(key, k -> new ArrayList<>());
 
         eqRows.add(r);
     }
@@ -77,8 +89,14 @@ public class RuntimeHashIndex<Row> implements RuntimeIndex<Row> {
     private GroupKey key(Row r) {
         GroupKey.Builder b = GroupKey.builder(keys.cardinality());
 
-        for (Integer field : keys)
-            b.add(ectx.rowHandler().get(field, r));
+        for (Integer field : keys) {
+            Object fieldVal = ectx.rowHandler().get(field, r);
+
+            if (fieldVal == null)
+                return NULL_KEY;
+
+            b.add(fieldVal);
+        }
 
         return b.build();
     }
@@ -104,7 +122,12 @@ public class RuntimeHashIndex<Row> implements RuntimeIndex<Row> {
 
         /** {@inheritDoc} */
         @NotNull @Override public Iterator<Row> iterator() {
-            List<Row> eqRows = rows.get(key(searchRow.get()));
+            GroupKey key = key(searchRow.get());
+
+            if (key == NULL_KEY)
+                return Collections.emptyIterator();
+
+            List<Row> eqRows = rows.get(key);
 
             return eqRows == null ? Collections.emptyIterator() : eqRows.iterator();
         }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/SystemViewScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/SystemViewScan.java
index 58f21c9..89392be 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/SystemViewScan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/SystemViewScan.java
@@ -118,7 +118,7 @@ public class SystemViewScan<Row, ViewRow> implements Iterable<Row> {
 
                 Object val = rowHnd.get(i, searchValues);
 
-                if (val != null) {
+                if (val != ectx.unspecifiedValue()) {
                     if (filterMap == null)
                         filterMap = new HashMap<>();
 
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactory.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactory.java
index e52dad9..0893229 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactory.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactory.java
@@ -100,7 +100,7 @@ public interface ExpressionFactory<Row> {
     /**
      * Creates row from RexNodes.
      *
-     * @param values Values.
+     * @param values Values ({@code null} values allowed in this list).
      * @return Row.
      */
     Supplier<Row> rowSource(List<RexNode> values);
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
index 31c9fd5..b1ce953 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.processors.query.calcite.exec.exp;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
@@ -28,8 +29,6 @@ import java.util.function.BiPredicate;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Ordering;
 import com.google.common.primitives.Primitives;
@@ -56,6 +55,7 @@ import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.rex.RexProgramBuilder;
 import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
 import org.apache.ignite.internal.processors.query.calcite.exec.RowHandler;
@@ -67,7 +67,6 @@ import org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.Aggregat
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
 import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.calcite.util.IgniteMethod;
-import org.apache.ignite.internal.processors.query.h2.opt.GridH2ValueCacheObject;
 import org.apache.ignite.internal.util.GridBoundedConcurrentLinkedHashMap;
 import org.apache.ignite.internal.util.typedef.F;
 
@@ -92,6 +91,9 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
     private final RelDataType emptyType;
 
     /** */
+    private final RelDataType nullType;
+
+    /** */
     private final ExecutionContext<Row> ctx;
 
     /** */
@@ -107,6 +109,7 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
         this.rexBuilder = rexBuilder;
 
         emptyType = new RelDataTypeFactory.Builder(this.typeFactory).build();
+        nullType = typeFactory.createSqlType(SqlTypeName.NULL);
     }
 
     /** {@inheritDoc} */
@@ -125,12 +128,34 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
     @Override public Comparator<Row> comparator(RelCollation collation) {
         if (collation == null || collation.getFieldCollations().isEmpty())
             return null;
-        else if (collation.getFieldCollations().size() == 1)
-            return comparator(collation.getFieldCollations().get(0));
-        return Ordering.compound(collation.getFieldCollations()
-            .stream()
-            .map(this::comparator)
-            .collect(Collectors.toList()));
+
+        return new Comparator<Row>() {
+            @Override public int compare(Row o1, Row o2) {
+                RowHandler<Row> hnd = ctx.rowHandler();
+                Object unspecifiedVal = ctx.unspecifiedValue();
+
+                for (RelFieldCollation field : collation.getFieldCollations()) {
+                    int fieldIdx = field.getFieldIndex();
+                    int nullComparison = field.nullDirection.nullComparison;
+
+                    Object c1 = hnd.get(fieldIdx, o1);
+                    Object c2 = hnd.get(fieldIdx, o2);
+
+                    // If filter for some field is unspecified, assume equality for this field and all subsequent fields.
+                    if (c1 == unspecifiedVal || c2 == unspecifiedVal)
+                        return 0;
+
+                    int res = (field.direction == RelFieldCollation.Direction.ASCENDING) ?
+                        ExpressionFactoryImpl.compare(c1, c2, nullComparison) :
+                        ExpressionFactoryImpl.compare(c2, c1, -nullComparison);
+
+                    if (res != 0)
+                        return res;
+                }
+
+                return 0;
+            }
+        };
     }
 
     /** {@inheritDoc} */
@@ -148,47 +173,6 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
     }
 
     /** */
-    @SuppressWarnings("rawtypes")
-    private Comparator<Row> comparator(RelFieldCollation fieldCollation) {
-        final int nullComparison = fieldCollation.nullDirection.nullComparison;
-        final int x = fieldCollation.getFieldIndex();
-        RowHandler<Row> handler = ctx.rowHandler();
-
-        if (fieldCollation.direction == RelFieldCollation.Direction.ASCENDING) {
-            return (o1, o2) -> {
-                Object obj1 = handler.get(x, o1);
-                Object obj2 = handler.get(x, o2);
-
-                boolean o1Comparable = obj1 instanceof Comparable;
-                boolean o2Comparable = obj2 instanceof Comparable;
-
-                if (o1Comparable && o2Comparable) {
-                    final Comparable c1 = (Comparable)obj1;
-                    final Comparable c2 = (Comparable)obj2;
-                    return RelFieldCollation.compare(c1, c2, nullComparison);
-                }
-                else {
-                    if (obj1 == obj2)
-                        return 0;
-                    else if (obj1 == null)
-                        return nullComparison;
-                    else if (obj2 == null)
-                        return -nullComparison;
-                    else
-                        return GridH2ValueCacheObject.compareHashOrBytes(obj1, obj2);
-                }
-            };
-        }
-
-        return (o1, o2) -> {
-            final Comparable c1 = (Comparable)handler.get(x, o1);
-            final Comparable c2 = (Comparable)handler.get(x, o2);
-            return RelFieldCollation.compare(c2, c1, -nullComparison);
-        };
-    }
-
-    /** */
-    @SuppressWarnings("rawtypes")
     private Comparator<Row> comparator(RelFieldCollation left, RelFieldCollation right) {
         final int nullComparison = left.nullDirection.nullComparison;
 
@@ -204,19 +188,27 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
 
         if (left.direction == RelFieldCollation.Direction.ASCENDING) {
             return (o1, o2) -> {
-                final Comparable c1 = (Comparable)handler.get(lIdx, o1);
-                final Comparable c2 = (Comparable)handler.get(rIdx, o2);
-                return RelFieldCollation.compare(c1, c2, nullComparison);
+                final Object c1 = handler.get(lIdx, o1);
+                final Object c2 = handler.get(rIdx, o2);
+                return compare(c1, c2, nullComparison);
             };
         }
 
         return (o1, o2) -> {
-            final Comparable c1 = (Comparable)handler.get(lIdx, o1);
-            final Comparable c2 = (Comparable)handler.get(rIdx, o2);
-            return RelFieldCollation.compare(c2, c1, -nullComparison);
+            final Object c1 = handler.get(lIdx, o1);
+            final Object c2 = handler.get(rIdx, o2);
+            return compare(c2, c1, -nullComparison);
         };
     }
 
+    /** */
+    @SuppressWarnings("rawtypes")
+    private static int compare(Object o1, Object o2, int nullComparison) {
+        final Comparable c1 = (Comparable)o1;
+        final Comparable c2 = (Comparable)o2;
+        return RelFieldCollation.compare(c1, c2, nullComparison);
+    }
+
     /** {@inheritDoc} */
     @Override public Predicate<Row> predicate(RexNode filter, RelDataType rowType) {
         return new PredicateImpl(scalar(filter, rowType));
@@ -234,7 +226,8 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
 
     /** {@inheritDoc} */
     @Override public Supplier<Row> rowSource(List<RexNode> values) {
-        return new ValuesImpl(scalar(values, null), ctx.rowHandler().factory(typeFactory, RexUtil.types(values)));
+        return new ValuesImpl(scalar(values, null), ctx.rowHandler().factory(typeFactory,
+            Commons.transform(values, v -> v != null ? v.getType() : nullType)));
     }
 
     /** {@inheritDoc} */
@@ -284,7 +277,7 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
     /**
      * Creates {@link SingleScalar}, a code-generated expressions evaluator.
      *
-     * @param nodes Expressions.
+     * @param nodes Expressions. {@code Null} expressions will be evaluated to {@link ExecutionContext#unspecifiedValue()}.
      * @param type Row type.
      * @return SingleScalar.
      */
@@ -308,14 +301,26 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
     }
 
     /** */
-    private Scalar compile(Iterable<RexNode> nodes, RelDataType type, boolean biInParams) {
+    private Scalar compile(List<RexNode> nodes, RelDataType type, boolean biInParams) {
         if (type == null)
             type = emptyType;
 
         RexProgramBuilder programBuilder = new RexProgramBuilder(type, rexBuilder);
 
-        for (RexNode node : nodes)
-            programBuilder.addProject(node, null);
+        BitSet unspecifiedValues = new BitSet(nodes.size());
+
+        for (int i = 0; i < nodes.size(); i++) {
+            RexNode node = nodes.get(i);
+
+            if (node != null)
+                programBuilder.addProject(node, null);
+            else {
+                unspecifiedValues.set(i);
+
+                programBuilder.addProject(rexBuilder.makeNullLiteral(type == emptyType ?
+                    nullType : type.getFieldList().get(i).getType()), null);
+            }
+        }
 
         RexProgram program = programBuilder.getProgram();
 
@@ -348,12 +353,17 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
         List<Expression> projects = RexToLixTranslator.translateProjects(program, typeFactory, conformance,
             builder, null, ctx_, inputGetter, correlates);
 
+        assert nodes.size() == projects.size();
+
         for (int i = 0; i < projects.size(); i++) {
+            Expression val = unspecifiedValues.get(i) ? Expressions.call(ctx_,
+                IgniteMethod.CONTEXT_UNSPECIFIED_VALUE.method()) : projects.get(i);
+
             builder.add(
                 Expressions.statement(
                     Expressions.call(hnd_,
                         IgniteMethod.ROW_HANDLER_SET.method(),
-                        Expressions.constant(i), out_, projects.get(i))));
+                        Expressions.constant(i), out_, val)));
         }
 
         String methodName = biInParams ? IgniteMethod.BI_SCALAR_EXECUTE.method().getName() :
@@ -381,9 +391,15 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
             if (i > 0)
                 b.append(';');
 
-            b.append(nodes.get(i));
+            RexNode node = nodes.get(i);
+
+            b.append(node);
+
+            if (node == null)
+                continue;
+
             b.append(':');
-            b.append(nodes.get(i).getType().getFullTypeString());
+            b.append(node.getType().getFullTypeString());
 
             new RexShuttle() {
                 @Override public RexNode visitFieldAccess(RexFieldAccess fieldAccess) {
@@ -391,7 +407,7 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
 
                     return super.visitFieldAccess(fieldAccess);
                 }
-            }.apply(nodes.get(i));
+            }.apply(node);
         }
 
         b.append(", biParam=").append(biParam);
@@ -635,8 +651,10 @@ public class ExpressionFactoryImpl<Row> implements ExpressionFactory<Row> {
         /** */
         public Function1<String, InputGetter> build(Iterable<RexNode> nodes) {
             try {
-                for (RexNode node : nodes)
-                    node.accept(this);
+                for (RexNode node : nodes) {
+                    if (node != null)
+                        node.accept(this);
+                }
 
                 return correlates == null ? null : correlates::get;
             }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteMethod.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteMethod.java
index 94a3592..b1ac6cd 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteMethod.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteMethod.java
@@ -39,6 +39,8 @@ public enum IgniteMethod {
         Object.class, Object.class),
     /** See {@link ExecutionContext#rowHandler()} */
     CONTEXT_ROW_HANDLER(ExecutionContext.class, "rowHandler"),
+    /** See {@link ExecutionContext#unspecifiedValue()} */
+    CONTEXT_UNSPECIFIED_VALUE(ExecutionContext.class, "unspecifiedValue"),
     /** See {@link ExecutionContext#getCorrelated(int)} */
     CONTEXT_GET_CORRELATED_VALUE(ExecutionContext.class, "getCorrelated", int.class),
     /** See {@link SingleScalar#execute(ExecutionContext, Object, Object)} */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java
index 23a1d74..759d125 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/RexUtils.java
@@ -407,11 +407,6 @@ public class RexUtils {
     }
 
     /** */
-    private static List<RexNode> makeListOfNullLiterals(RexBuilder builder, List<RelDataType> types) {
-        return Commons.transform(types, builder::makeNullLiteral);
-    }
-
-    /** */
     public static boolean isNotNull(RexNode op) {
         if (op == null)
             return false;
@@ -427,7 +422,7 @@ public class RexUtils {
 
         RexBuilder builder = builder(cluster);
         List<RelDataType> types = RelOptUtil.getFieldTypeList(rowType);
-        List<RexNode> res = makeListOfNullLiterals(builder, types);
+        List<RexNode> res = Arrays.asList(new RexNode[types.size()]);
 
         for (RexNode pred : idxCond) {
             assert pred instanceof RexCall;
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/AbstractExecutionTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/AbstractExecutionTest.java
index 6647016..1766dac 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/AbstractExecutionTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/AbstractExecutionTest.java
@@ -443,6 +443,21 @@ public class AbstractExecutionTest extends GridCommonAbstractTest {
         }
 
         /** */
+        public int rowsCount() {
+            int cnt = 0;
+
+            while (hasNext()) {
+                next();
+
+                cnt++;
+            }
+
+            rewind();
+
+            return cnt;
+        }
+
+        /** */
         public void closeRewindableRoot() {
             super.closeInternal();
         }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashIndexSpoolExecutionTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashIndexSpoolExecutionTest.java
index be17583..9b84f4f 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashIndexSpoolExecutionTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashIndexSpoolExecutionTest.java
@@ -19,9 +19,9 @@ package org.apache.ignite.internal.processors.query.calcite.exec.rel;
 
 import java.util.Arrays;
 import java.util.Iterator;
+import java.util.List;
 import java.util.UUID;
 import java.util.function.Predicate;
-
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
@@ -29,6 +29,7 @@ import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactor
 import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils;
 import org.apache.ignite.internal.util.lang.GridTuple3;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.T2;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.testframework.junits.WithSystemProperty;
 import org.jetbrains.annotations.NotNull;
@@ -129,25 +130,56 @@ public class HashIndexSpoolExecutionTest extends AbstractExecutionTest {
                     testFilter.delegate = bound.get1();
                     System.arraycopy(bound.get2(), 0, searchRow, 0, searchRow.length);
 
-                    int cnt = 0;
+                    assertEquals("Invalid result size", (int)bound.get3(), root.rowsCount());
+                }
 
-                    while (root.hasNext()) {
-                        root.next();
+                root.closeRewindableRoot();
+            }
+        }
+    }
 
-                        cnt++;
-                    }
+    /** */
+    @Test
+    public void testNullsInSearchRow() {
+        ExecutionContext<Object[]> ctx = executionContext(F.first(nodes()), UUID.randomUUID(), 0);
+        RelDataType rowType = TypeUtils.createRowType(ctx.getTypeFactory(), int.class, int.class);
 
-                    assertEquals(
-                        "Invalid result size",
-                        (int)bound.get3(),
-                        cnt);
+        ScanNode<Object[]> scan = new ScanNode<>(ctx, rowType, new TestTable(
+            4,
+            rowType,
+            (rowId) -> (rowId & 1) == 0 ? null : 1,
+            (rowId) -> (rowId & 2) == 0 ? null : 1
+        ));
 
-                    root.rewind();
-                }
+        Object[] searchRow = new Object[2];
 
-                root.closeRewindableRoot();
-            }
+        IndexSpoolNode<Object[]> spool = IndexSpoolNode.createHashSpool(
+            ctx,
+            rowType,
+            ImmutableBitSet.of(0, 1),
+            () -> searchRow
+        );
+
+        spool.register(scan);
+
+        RootRewindable<Object[]> root = new RootRewindable<>(ctx, rowType);
+
+        root.register(spool);
+
+        List<T2<Object[], Integer>> testBounds = F.asList(
+            new T2<>(new Object[] {null, null}, 0),
+            new T2<>(new Object[] {1, null}, 0),
+            new T2<>(new Object[] {null, 1}, 0),
+            new T2<>(new Object[] {1, 1}, 1)
+        );
+
+        for (T2<Object[], Integer> bound : testBounds) {
+            System.arraycopy(bound.get1(), 0, searchRow, 0, searchRow.length);
+
+            assertEquals("Invalid result size", (int)bound.get2(), root.rowsCount());
         }
+
+        root.closeRewindableRoot();
     }
 
     /** */
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/SortedIndexSpoolExecutionTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/SortedIndexSpoolExecutionTest.java
index 736418e..866af24 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/SortedIndexSpoolExecutionTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/SortedIndexSpoolExecutionTest.java
@@ -19,9 +19,10 @@ package org.apache.ignite.internal.processors.query.calcite.exec.rel;
 
 import java.util.Arrays;
 import java.util.Iterator;
+import java.util.List;
 import java.util.UUID;
 import java.util.function.Predicate;
-
+import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.util.ImmutableIntList;
@@ -30,6 +31,7 @@ import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactor
 import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils;
 import org.apache.ignite.internal.util.lang.GridTuple4;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.T3;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.testframework.junits.WithSystemProperty;
 import org.jetbrains.annotations.NotNull;
@@ -155,20 +157,7 @@ public class SortedIndexSpoolExecutionTest extends AbstractExecutionTest {
                 System.arraycopy(bound.get2(), 0, lower, 0, lower.length);
                 System.arraycopy(bound.get3(), 0, upper, 0, upper.length);
 
-                int cnt = 0;
-
-                while (root.hasNext()) {
-                    root.next();
-
-                    cnt++;
-                }
-
-                assertEquals(
-                    "Invalid result size",
-                    (int)bound.get4(),
-                    cnt);
-
-                root.rewind();
+                assertEquals("Invalid result size", (int)bound.get4(), root.rowsCount());
             }
 
             root.closeRewindableRoot();
@@ -176,6 +165,68 @@ public class SortedIndexSpoolExecutionTest extends AbstractExecutionTest {
     }
 
     /** */
+    @Test
+    public void testUnspecifiedValuesInSearchRow() {
+        ExecutionContext<Object[]> ctx = executionContext(F.first(nodes()), UUID.randomUUID(), 0);
+        IgniteTypeFactory tf = ctx.getTypeFactory();
+        RelDataType rowType = TypeUtils.createRowType(tf, int.class, String.class, int.class);
+
+        ScanNode<Object[]> scan = new ScanNode<>(
+            ctx,
+            rowType,
+            new TestTable(100, rowType, rowId -> rowId / 10, rowId -> rowId % 10, rowId -> rowId)
+        );
+
+        Object[] lower = new Object[3];
+        Object[] upper = new Object[3];
+
+        RelCollation collation = RelCollations.of(ImmutableIntList.of(0, 1));
+
+        IndexSpoolNode<Object[]> spool = IndexSpoolNode.createTreeSpool(
+            ctx,
+            rowType,
+            collation,
+            ctx.expressionFactory().comparator(collation),
+            v -> true,
+            () -> lower,
+            () -> upper
+        );
+
+        spool.register(scan);
+
+        RootRewindable<Object[]> root = new RootRewindable<>(ctx, rowType);
+        root.register(spool);
+
+        Object x = ctx.unspecifiedValue(); // Unspecified filter value.
+
+        // Test tuple (lower, upper, expected result size).
+        List<T3<Object[], Object[], Integer>> testBounds = F.asList(
+            new T3<>(new Object[] {x, x, x}, new Object[] {x, x, x}, 100),
+            new T3<>(new Object[] {0, 0, x}, new Object[] {4, 9, x}, 50),
+            new T3<>(new Object[] {0, x, x}, new Object[] {4, 9, x}, 50),
+            new T3<>(new Object[] {0, 0, x}, new Object[] {4, x, x}, 50),
+            new T3<>(new Object[] {4, x, x}, new Object[] {4, x, x}, 10),
+            // This is a special case, we shouldn't compare the next field if current field bound value is null, or we
+            // can accidentally find wrong lower/upper row. So, {x, 4} bound must be converted to {x, x} and redunant
+            // rows must be filtered out by predicate.
+            new T3<>(new Object[] {x, 4, x}, new Object[] {x, 5, x}, 100)
+        );
+
+        for (T3<Object[], Object[], Integer> bound : testBounds) {
+            log.info("Check: lowerBound=" + Arrays.toString(bound.get1()) +
+                ", upperBound=" + Arrays.toString(bound.get2()));
+
+            // Set up bounds.
+            System.arraycopy(bound.get1(), 0, lower, 0, lower.length);
+            System.arraycopy(bound.get2(), 0, upper, 0, upper.length);
+
+            assertEquals("Invalid result size", (int)bound.get3(), root.rowsCount());
+        }
+
+        root.closeRewindableRoot();
+    }
+
+    /** */
     static class TestPredicate implements Predicate<Object[]> {
         /** */
         Predicate<Object[]> delegate;
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/HashSpoolIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/HashSpoolIntegrationTest.java
new file mode 100644
index 0000000..cca21ef
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/HashSpoolIntegrationTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.processors.query.calcite.integration;
+
+import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
+import org.junit.Test;
+
+/**
+ * Hash spool test.
+ */
+public class HashSpoolIntegrationTest extends AbstractBasicIntegrationTest {
+    /** */
+    @Test
+    public void testNullsInSearchRow() {
+        executeSql("CREATE TABLE t(i1 INTEGER, i2 INTEGER)");
+        executeSql("INSERT INTO t VALUES (null, 0), (1, 1), (2, 2), (3, null)");
+
+        assertQuery("SELECT i1, (SELECT i2 FROM t WHERE i1=t1.i1) FROM t t1")
+            .matches(QueryChecker.containsSubPlan("IgniteHashIndexSpool"))
+            .returns(null, null)
+            .returns(1, 1)
+            .returns(2, 2)
+            .returns(3, null)
+            .check();
+
+        assertQuery("SELECT (SELECT i1 FROM t WHERE i2=t1.i2), i2 FROM t t1")
+            .matches(QueryChecker.containsSubPlan("IgniteHashIndexSpool"))
+            .returns(null, 0)
+            .returns(1, 1)
+            .returns(2, 2)
+            .returns(null, null)
+            .check();
+    }
+
+    /** */
+    @Test
+    public void testNullsInSearchRowMultipleColumns() {
+        executeSql("CREATE TABLE t0(i1 INTEGER, i2 INTEGER)");
+        executeSql("CREATE TABLE t1(i1 INTEGER, i2 INTEGER)");
+        executeSql("INSERT INTO t0 VALUES (null, 0), (1, null), (null, 2), (3, null), (1, 1)");
+        executeSql("INSERT INTO t1 VALUES (null, 0), (null, 1), (2, null), (3, null), (1, 1)");
+
+        String sql = "SELECT /*+ DISABLE_RULE ('MergeJoinConverter', 'NestedLoopJoinConverter', " +
+            "'FilterSpoolMergeToSortedIndexSpoolRule')*/ * " +
+            "FROM t0 JOIN t1 ON t0.i1=t1.i1 AND t0.i2=t1.i2";
+
+        assertQuery(sql)
+            .matches(QueryChecker.containsSubPlan("IgniteHashIndexSpool"))
+            .returns(1, 1, 1, 1)
+            .check();
+    }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java
new file mode 100644
index 0000000..97056f3
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.processors.query.calcite.integration;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
+import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
+import org.apache.ignite.internal.processors.query.calcite.rel.logical.IgniteLogicalIndexScan;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
+import org.apache.ignite.internal.processors.query.calcite.util.IndexConditions;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+
+/**
+ * Index scan test.
+ */
+public class IndexScanlIntegrationTest extends AbstractBasicIntegrationTest {
+    /** {@inheritDoc} */
+    @Override protected int nodeCount() {
+        return 1;
+    }
+
+    /** */
+    @Test
+    public void testNullsInSearchRow() {
+        executeSql("CREATE TABLE t(i1 INTEGER, i2 INTEGER) WITH TEMPLATE=REPLICATED ");
+        executeSql("INSERT INTO t VALUES (0, null), (1, null), (2, 2), (3, null), (4, null)");
+        executeSql("CREATE INDEX t_idx ON t(i1)");
+
+        IgniteTable tbl = (IgniteTable)queryProcessor(grid(0)).schemaHolder().schema("PUBLIC").getTable("T");
+        IgniteIndex idxOld = tbl.getIndex("T_IDX");
+        TestIgniteIndex idxNew = new TestIgniteIndex(idxOld);
+
+        tbl.addIndex(idxNew);
+
+        String sql = "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ t1.i1, t2.i1 " +
+            "FROM t t1 " +
+            "LEFT JOIN t t2 ON t1.i2 = t2.i1";
+
+        assertQuery(sql)
+            .matches(QueryChecker.containsSubPlan("IgniteCorrelatedNestedLoopJoin"))
+            .matches(QueryChecker.containsIndexScan("PUBLIC", "T", "T_IDX"))
+            .returns(0, null)
+            .returns(1, null)
+            .returns(2, 2)
+            .returns(3, null)
+            .returns(4, 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, idxNew.filteredRows);
+    }
+
+    /** */
+    private static class TestIgniteIndex implements IgniteIndex {
+        /** */
+        private final IgniteIndex delegate;
+
+        /** */
+        private int filteredRows;
+
+        /** */
+        public TestIgniteIndex(IgniteIndex delegate) {
+            this.delegate = delegate;
+        }
+
+        /** {@inheritDoc} */
+        @Override public RelCollation collation() {
+            return delegate.collation();
+        }
+
+        /** {@inheritDoc} */
+        @Override public String name() {
+            return delegate.name();
+        }
+
+        /** {@inheritDoc} */
+        @Override public IgniteTable table() {
+            return delegate.table();
+        }
+
+        /** {@inheritDoc} */
+        @Override public IgniteLogicalIndexScan toRel(
+            RelOptCluster cluster,
+            RelOptTable relOptTbl,
+            @Nullable List<RexNode> proj,
+            @Nullable RexNode cond,
+            @Nullable ImmutableBitSet requiredColumns
+        ) {
+            return delegate.toRel(cluster, relOptTbl, proj, cond, requiredColumns);
+        }
+
+        /** {@inheritDoc} */
+        @Override public IndexConditions toIndexCondition(
+            RelOptCluster cluster,
+            @Nullable RexNode cond,
+            @Nullable ImmutableBitSet requiredColumns
+        ) {
+            return delegate.toIndexCondition(cluster, cond, requiredColumns);
+        }
+
+        /** {@inheritDoc} */
+        @Override public <Row> Iterable<Row> scan(
+            ExecutionContext<Row> execCtx,
+            ColocationGroup grp,
+            Predicate<Row> filters,
+            Supplier<Row> lowerIdxConditions,
+            Supplier<Row> upperIdxConditions,
+            Function<Row, Row> rowTransformer,
+            @Nullable ImmutableBitSet requiredColumns
+        ) {
+            Predicate<Row> filter = row -> { filteredRows++; return true; };
+
+            filters = filter.and(filters);
+
+            return delegate.scan(execCtx, grp, filters, lowerIdxConditions, upperIdxConditions, rowTransformer,
+                requiredColumns);
+        }
+    }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/CorrelatedNestedLoopJoinPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/CorrelatedNestedLoopJoinPlannerTest.java
index e735993..570fd8d 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/CorrelatedNestedLoopJoinPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/CorrelatedNestedLoopJoinPlannerTest.java
@@ -18,12 +18,10 @@
 package org.apache.ignite.internal.processors.query.calcite.planner;
 
 import java.util.List;
-
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rex.RexFieldAccess;
-import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
@@ -102,18 +100,18 @@ public class CorrelatedNestedLoopJoinPlannerTest extends AbstractPlannerTest {
         assertNotNull("Invalid plan\n" + RelOptUtil.toString(phys), lBound);
         assertEquals(3, lBound.size());
 
-        assertTrue(((RexLiteral)lBound.get(0)).isNull());
-        assertTrue(((RexLiteral)lBound.get(2)).isNull());
+        assertNull(lBound.get(0));
         assertTrue(lBound.get(1) instanceof RexFieldAccess);
+        assertNull(lBound.get(2));
 
         List<RexNode> uBound = idxScan.upperBound();
 
         assertNotNull("Invalid plan\n" + RelOptUtil.toString(phys), uBound);
         assertEquals(3, uBound.size());
 
-        assertTrue(((RexLiteral)uBound.get(0)).isNull());
-        assertTrue(((RexLiteral)uBound.get(2)).isNull());
+        assertNull(uBound.get(0));
         assertTrue(uBound.get(1) instanceof RexFieldAccess);
+        assertNull(uBound.get(2));
     }
 
     /**
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashIndexSpoolPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashIndexSpoolPlannerTest.java
index fa33898..ffa91b3 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashIndexSpoolPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashIndexSpoolPlannerTest.java
@@ -18,12 +18,10 @@
 package org.apache.ignite.internal.processors.query.calcite.planner;
 
 import java.util.List;
-
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rex.RexFieldAccess;
-import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
@@ -101,9 +99,9 @@ public class HashIndexSpoolPlannerTest extends AbstractPlannerTest {
         assertNotNull(searchRow);
         assertEquals(3, searchRow.size());
 
-        assertTrue(((RexLiteral)searchRow.get(0)).isNull());
-        assertTrue(((RexLiteral)searchRow.get(2)).isNull());
+        assertNull(searchRow.get(0));
         assertTrue(searchRow.get(1) instanceof RexFieldAccess);
+        assertNull(searchRow.get(2));
     }
 
     /** */
@@ -159,15 +157,15 @@ public class HashIndexSpoolPlannerTest extends AbstractPlannerTest {
 
         IgniteHashIndexSpool idxSpool = findFirstNode(phys, byClass(IgniteHashIndexSpool.class));
 
-        List<RexNode> searcRow = idxSpool.searchRow();
+        List<RexNode> searchRow = idxSpool.searchRow();
 
-        assertNotNull(searcRow);
-        assertEquals(4, searcRow.size());
+        assertNotNull(searchRow);
+        assertEquals(4, searchRow.size());
 
-        assertTrue(((RexLiteral)searcRow.get(0)).isNull());
-        assertTrue(searcRow.get(1) instanceof RexFieldAccess);
-        assertTrue(searcRow.get(2) instanceof RexFieldAccess);
-        assertTrue(((RexLiteral)searcRow.get(3)).isNull());
+        assertNull(searchRow.get(0));
+        assertTrue(searchRow.get(1) instanceof RexFieldAccess);
+        assertTrue(searchRow.get(2) instanceof RexFieldAccess);
+        assertNull(searchRow.get(3));
     }
 
     /**
@@ -227,8 +225,8 @@ public class HashIndexSpoolPlannerTest extends AbstractPlannerTest {
         assertNotNull(searchRow);
         assertEquals(3, searchRow.size());
 
-        assertTrue(((RexLiteral)searchRow.get(0)).isNull());
-        assertTrue(((RexLiteral)searchRow.get(2)).isNull());
+        assertNull(searchRow.get(0));
         assertTrue(searchRow.get(1) instanceof RexFieldAccess);
+        assertNull(searchRow.get(2));
     }
 }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/SortedIndexSpoolPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/SortedIndexSpoolPlannerTest.java
index f6840b2..1a1bfa4 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/SortedIndexSpoolPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/SortedIndexSpoolPlannerTest.java
@@ -18,12 +18,10 @@
 package org.apache.ignite.internal.processors.query.calcite.planner;
 
 import java.util.List;
-
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rex.RexFieldAccess;
-import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
@@ -99,18 +97,18 @@ public class SortedIndexSpoolPlannerTest extends AbstractPlannerTest {
         assertNotNull(lBound);
         assertEquals(3, lBound.size());
 
-        assertTrue(((RexLiteral)lBound.get(0)).isNull());
-        assertTrue(((RexLiteral)lBound.get(2)).isNull());
+        assertNull(lBound.get(0));
         assertTrue(lBound.get(1) instanceof RexFieldAccess);
+        assertNull(lBound.get(2));
 
         List<RexNode> uBound = idxSpool.indexCondition().upperBound();
 
         assertNotNull(uBound);
         assertEquals(3, uBound.size());
 
-        assertTrue(((RexLiteral)uBound.get(0)).isNull());
-        assertTrue(((RexLiteral)uBound.get(2)).isNull());
+        assertNull(uBound.get(0));
         assertTrue(uBound.get(1) instanceof RexFieldAccess);
+        assertNull(uBound.get(2));
     }
 
     /**
@@ -176,19 +174,19 @@ public class SortedIndexSpoolPlannerTest extends AbstractPlannerTest {
         assertNotNull(lBound);
         assertEquals(4, lBound.size());
 
-        assertTrue(((RexLiteral)lBound.get(0)).isNull());
-        assertTrue(((RexLiteral)lBound.get(2)).isNull());
-        assertTrue(((RexLiteral)lBound.get(3)).isNull());
+        assertNull(lBound.get(0));
         assertTrue(lBound.get(1) instanceof RexFieldAccess);
+        assertNull(lBound.get(2));
+        assertNull(lBound.get(3));
 
         List<RexNode> uBound = idxSpool.indexCondition().upperBound();
 
         assertNotNull(uBound);
         assertEquals(4, uBound.size());
 
-        assertTrue(((RexLiteral)uBound.get(0)).isNull());
-        assertTrue(((RexLiteral)lBound.get(2)).isNull());
-        assertTrue(((RexLiteral)lBound.get(3)).isNull());
+        assertNull(uBound.get(0));
         assertTrue(uBound.get(1) instanceof RexFieldAccess);
+        assertNull(uBound.get(2));
+        assertNull(uBound.get(3));
     }
 }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
index c20a195..ef18e1b 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
@@ -30,7 +30,9 @@ import org.apache.ignite.internal.processors.query.calcite.integration.Aggregate
 import org.apache.ignite.internal.processors.query.calcite.integration.CalciteBasicSecondaryIndexIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.CalciteErrorHandlilngIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.CorrelatesIntegrationTest;
+import org.apache.ignite.internal.processors.query.calcite.integration.HashSpoolIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.IndexDdlIntegrationTest;
+import org.apache.ignite.internal.processors.query.calcite.integration.IndexScanlIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.IndexSpoolIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.IntervalTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.JoinIntegrationTest;
@@ -82,6 +84,8 @@ import org.junit.runners.Suite;
     TableDmlIntegrationTest.class,
     DataTypesTest.class,
     IndexSpoolIntegrationTest.class,
+    HashSpoolIntegrationTest.class,
+    IndexScanlIntegrationTest.class,
     SetOpIntegrationTest.class,
     UnstableTopologyTest.class,
     JoinCommuteRulesTest.class,
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2ValueCacheObject.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2ValueCacheObject.java
index 295ffb8..5b1dfcd 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2ValueCacheObject.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/opt/GridH2ValueCacheObject.java
@@ -20,7 +20,6 @@ package org.apache.ignite.internal.processors.query.h2.opt;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.sql.Types;
-import java.util.Comparator;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.internal.binary.BinaryObjectImpl;
 import org.apache.ignite.internal.processors.cache.CacheObject;
@@ -163,26 +162,15 @@ public class GridH2ValueCacheObject extends Value {
             return o1.getClass().getName().compareTo(o2.getClass().getName());
         }
 
-        return compareHashOrBytes(this, v, (v1, v2) -> Bits.compareNotNullSigned(((Value)v1).getBytesNoCopy(),
-            ((Value)v2).getBytesNoCopy()));
-    }
-
-    /** Compare hash codes. */
-    public static int compareHashOrBytes(Object o1, Object o2) {
-        return compareHashOrBytes(o1, o2, null);
-    }
-
-    /** Compare hash codes. */
-    private static int compareHashOrBytes(Object o1, Object o2, Comparator<Object> comp) {
-        int h1 = o1.hashCode();
-        int h2 = o2.hashCode();
+        // Compare hash codes.
+        int h1 = hashCode();
+        int h2 = v.hashCode();
 
         if (h1 == h2) {
             if (o1.equals(o2))
                 return 0;
 
-            return comp == null ? Bits.compareNotNullSigned(JdbcUtils.serialize(o1, null),
-                JdbcUtils.serialize(o2, null)) : comp.compare(o1, o2);
+            return Bits.compareNotNullSigned(getBytesNoCopy(), v.getBytesNoCopy());
         }
 
         return h1 > h2 ? 1 : -1;