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

amashenkov 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 33b578bcad IGNITE-18056 Sql. Make RangeConditions accordant to index 
row type (#1329)
33b578bcad is described below

commit 33b578bcad191ace4f2274dbb252e1f8ef6c78c8
Author: Andrew V. Mashenkov <[email protected]>
AuthorDate: Thu Nov 24 14:03:43 2022 +0300

    IGNITE-18056 Sql. Make RangeConditions accordant to index row type (#1329)
---
 .../apache/ignite/internal/index/HashIndex.java    |  11 +-
 .../org/apache/ignite/internal/index/Index.java    |  13 +-
 .../apache/ignite/internal/index/SortedIndex.java  |  12 +-
 .../ignite/internal/index/SortedIndexImpl.java     |  20 ++-
 .../internal/sql/engine/exec/ExecutionContext.java |  13 --
 .../sql/engine/exec/LogicalRelImplementor.java     |  20 ++-
 .../internal/sql/engine/exec/RowConverter.java     |  59 +++----
 .../sql/engine/exec/exp/ExpressionFactory.java     |   7 +-
 .../sql/engine/exec/exp/ExpressionFactoryImpl.java |  48 +++---
 .../internal/sql/engine/exec/exp/RexImpTable.java  |   9 ++
 .../sql/engine/exec/rel/IndexScanNode.java         |  21 +--
 .../engine/rel/logical/IgniteLogicalIndexScan.java |  39 +++--
 .../FilterSpoolMergeToSortedIndexSpoolRule.java    |   5 +-
 .../internal/sql/engine/schema/IgniteIndex.java    |  30 ++++
 .../sql/engine/schema/TableDescriptor.java         |   3 +-
 .../sql/engine/schema/TableDescriptorImpl.java     |   3 +-
 .../internal/sql/engine/trait/TraitUtils.java      |  23 ++-
 .../internal/sql/engine/util/IgniteMethod.java     |   2 -
 .../ignite/internal/sql/engine/util/RexUtils.java  |  45 +++---
 .../exec/rel/IndexScanNodeExecutionTest.java       | 169 ++++++++++++---------
 .../exec/rel/SortedIndexSpoolExecutionTest.java    |   3 +-
 .../CorrelatedNestedLoopJoinPlannerTest.java       |   9 +-
 .../planner/ProjectFilterScanMergePlannerTest.java |  58 ++++++-
 .../planner/SortedIndexSpoolPlannerTest.java       |  26 ++--
 .../replicator/PartitionReplicaListener.java       |  59 +++----
 25 files changed, 431 insertions(+), 276 deletions(-)

diff --git 
a/modules/index/src/main/java/org/apache/ignite/internal/index/HashIndex.java 
b/modules/index/src/main/java/org/apache/ignite/internal/index/HashIndex.java
index ae03fdd3c5..069b1f7b1b 100644
--- 
a/modules/index/src/main/java/org/apache/ignite/internal/index/HashIndex.java
+++ 
b/modules/index/src/main/java/org/apache/ignite/internal/index/HashIndex.java
@@ -28,6 +28,7 @@ import org.apache.ignite.internal.table.InternalTable;
 import org.apache.ignite.internal.table.TableImpl;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.network.ClusterNode;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * An object that represents a hash index.
@@ -76,13 +77,19 @@ public class HashIndex implements Index<IndexDescriptor> {
 
     /** {@inheritDoc} */
     @Override
-    public Publisher<BinaryRow> lookup(int partId, InternalTransaction tx, 
BinaryTuple key, BitSet columns) {
+    public Publisher<BinaryRow> lookup(int partId, @Nullable 
InternalTransaction tx, BinaryTuple key, @Nullable BitSet columns) {
         return table.lookup(partId, tx, id, key, columns);
     }
 
     /** {@inheritDoc} */
     @Override
-    public Publisher<BinaryRow> lookup(int partId, HybridTimestamp timestamp, 
ClusterNode recipientNode, BinaryTuple key, BitSet columns) {
+    public Publisher<BinaryRow> lookup(
+            int partId,
+            HybridTimestamp timestamp,
+            ClusterNode recipientNode,
+            BinaryTuple key,
+            @Nullable BitSet columns
+    ) {
         return table.lookup(partId, timestamp, recipientNode, id, key, 
columns);
     }
 }
diff --git 
a/modules/index/src/main/java/org/apache/ignite/internal/index/Index.java 
b/modules/index/src/main/java/org/apache/ignite/internal/index/Index.java
index 68eb57d93d..896ee3ab09 100644
--- a/modules/index/src/main/java/org/apache/ignite/internal/index/Index.java
+++ b/modules/index/src/main/java/org/apache/ignite/internal/index/Index.java
@@ -25,6 +25,7 @@ import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.BinaryTuple;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.network.ClusterNode;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * An object describing an abstract index.
@@ -41,7 +42,7 @@ public interface Index<DescriptorT extends IndexDescriptor> {
     /** Returns table id index belong to. */
     UUID tableId();
 
-    /** Returns index dewscriptor. */
+    /** Returns index descriptor. */
     DescriptorT descriptor();
 
     /**
@@ -53,7 +54,7 @@ public interface Index<DescriptorT extends IndexDescriptor> {
      * @param columns Columns to include.
      * @return A cursor from resulting rows.
      */
-    Publisher<BinaryRow> lookup(int partId, InternalTransaction tx, 
BinaryTuple key, BitSet columns);
+    Publisher<BinaryRow> lookup(int partId, @Nullable InternalTransaction tx, 
BinaryTuple key, @Nullable BitSet columns);
 
     /**
      * Returns cursor for the values corresponding to the given key.
@@ -65,5 +66,11 @@ public interface Index<DescriptorT extends IndexDescriptor> {
      * @param columns Columns to include.
      * @return A cursor from resulting rows.
      */
-    Publisher<BinaryRow> lookup(int partId, HybridTimestamp readTimestamp, 
ClusterNode recipientNode, BinaryTuple key, BitSet columns);
+    Publisher<BinaryRow> lookup(
+            int partId,
+            HybridTimestamp readTimestamp,
+            ClusterNode recipientNode,
+            BinaryTuple key,
+            @Nullable BitSet columns
+    );
 }
diff --git 
a/modules/index/src/main/java/org/apache/ignite/internal/index/SortedIndex.java 
b/modules/index/src/main/java/org/apache/ignite/internal/index/SortedIndex.java
index 08d971871a..0676ba557a 100644
--- 
a/modules/index/src/main/java/org/apache/ignite/internal/index/SortedIndex.java
+++ 
b/modules/index/src/main/java/org/apache/ignite/internal/index/SortedIndex.java
@@ -50,10 +50,10 @@ public interface SortedIndex extends 
Index<SortedIndexDescriptor> {
      */
     default Publisher<BinaryRow> scan(
             int partId,
-            InternalTransaction tx,
+            @Nullable InternalTransaction tx,
             @Nullable BinaryTuplePrefix left,
             @Nullable BinaryTuplePrefix right,
-            BitSet columns
+            @Nullable BitSet columns
     ) {
         return scan(partId, tx, left, right, INCLUDE_LEFT, columns);
     }
@@ -75,7 +75,7 @@ public interface SortedIndex extends 
Index<SortedIndexDescriptor> {
             ClusterNode recipientNode,
             @Nullable BinaryTuplePrefix left,
             @Nullable BinaryTuplePrefix right,
-            BitSet columns
+            @Nullable BitSet columns
     ) {
         return scan(partId, readTimestamp, recipientNode, left, right, 
INCLUDE_LEFT, columns);
     }
@@ -95,11 +95,11 @@ public interface SortedIndex extends 
Index<SortedIndexDescriptor> {
      */
     Publisher<BinaryRow> scan(
             int partId,
-            InternalTransaction tx,
+            @Nullable InternalTransaction tx,
             @Nullable BinaryTuplePrefix leftBound,
             @Nullable BinaryTuplePrefix rightBound,
             int flags,
-            BitSet columnsToInclude
+            @Nullable BitSet columnsToInclude
     );
 
 
@@ -124,6 +124,6 @@ public interface SortedIndex extends 
Index<SortedIndexDescriptor> {
             @Nullable BinaryTuplePrefix leftBound,
             @Nullable BinaryTuplePrefix rightBound,
             int flags,
-            BitSet columnsToInclude
+            @Nullable BitSet columnsToInclude
     );
 }
diff --git 
a/modules/index/src/main/java/org/apache/ignite/internal/index/SortedIndexImpl.java
 
b/modules/index/src/main/java/org/apache/ignite/internal/index/SortedIndexImpl.java
index 9b9f1be447..3ec520b895 100644
--- 
a/modules/index/src/main/java/org/apache/ignite/internal/index/SortedIndexImpl.java
+++ 
b/modules/index/src/main/java/org/apache/ignite/internal/index/SortedIndexImpl.java
@@ -78,13 +78,19 @@ public class SortedIndexImpl implements SortedIndex {
 
     /** {@inheritDoc} */
     @Override
-    public Publisher<BinaryRow> lookup(int partId, InternalTransaction tx, 
BinaryTuple key, BitSet columns) {
+    public Publisher<BinaryRow> lookup(int partId, @Nullable 
InternalTransaction tx, BinaryTuple key, @Nullable BitSet columns) {
         return table.lookup(partId, tx, id, key, columns);
     }
 
     /** {@inheritDoc} */
     @Override
-    public Publisher<BinaryRow> lookup(int partId, HybridTimestamp timestamp, 
ClusterNode recipientNode, BinaryTuple key, BitSet columns) {
+    public Publisher<BinaryRow> lookup(
+            int partId,
+            HybridTimestamp timestamp,
+            ClusterNode recipientNode,
+            BinaryTuple key,
+            @Nullable BitSet columns
+    ) {
         return table.lookup(partId, timestamp, recipientNode, id, key, 
columns);
     }
 
@@ -92,11 +98,11 @@ public class SortedIndexImpl implements SortedIndex {
     @Override
     public Publisher<BinaryRow> scan(
             int partId,
-            InternalTransaction tx,
-            BinaryTuplePrefix leftBound,
-            BinaryTuplePrefix rightBound,
+            @Nullable InternalTransaction tx,
+            @Nullable BinaryTuplePrefix leftBound,
+            @Nullable BinaryTuplePrefix rightBound,
             int flags,
-            BitSet columnsToInclude
+            @Nullable BitSet columnsToInclude
     ) {
         return table.scan(partId, tx, id, leftBound, rightBound, flags, 
columnsToInclude);
     }
@@ -110,7 +116,7 @@ public class SortedIndexImpl implements SortedIndex {
             @Nullable BinaryTuplePrefix leftBound,
             @Nullable BinaryTuplePrefix rightBound,
             int flags,
-            BitSet columnsToInclude
+            @Nullable BitSet columnsToInclude
     ) {
         return table.scan(partId, readTimestamp, recipientNode, id, leftBound, 
rightBound, flags, columnsToInclude);
     }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionContext.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionContext.java
index 5d79cf740a..ec2d373c02 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionContext.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionContext.java
@@ -54,15 +54,6 @@ import org.jetbrains.annotations.Nullable;
  * Runtime context allowing access to the tables in a database.
  */
 public class ExecutionContext<RowT> extends AbstractQueryContext implements 
DataContext {
-    /** Placeholder for values, which expressions are not specified. */
-    private static final Object UNSPECIFIED_VALUE = new Object() {
-        /** {@inheritDoc} */
-        @Override
-        public String toString() {
-            return "<unspecified_value>";
-        }
-    };
-
     private static final IgniteLogger LOG = 
Loggers.forClass(ExecutionContext.class);
 
     private static final TimeZone TIME_ZONE = TimeZone.getDefault(); // TODO 
DistributedSqlConfiguration#timeZone
@@ -380,10 +371,6 @@ public class ExecutionContext<RowT> extends 
AbstractQueryContext implements Data
         return cancelFlag.get();
     }
 
-    public Object unspecifiedValue() {
-        return UNSPECIFIED_VALUE;
-    }
-
     /** {@inheritDoc} */
     @Override
     public boolean equals(Object o) {
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
index 0c19d63b03..a258d9a57a 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
@@ -303,9 +303,17 @@ public class LogicalRelImplementor<RowT> implements 
IgniteRelVisitor<Node<RowT>>
         Predicate<RowT> filters = condition == null ? null : 
expressionFactory.predicate(condition, rowType);
         Function<RowT, RowT> prj = projects == null ? null : 
expressionFactory.project(projects, rowType);
 
-        //TODO: https://issues.apache.org/jira/browse/IGNITE-18056  Use 
'idx.getRowType()' instead of 'tbl.getRowType()'
-        RangeIterable<RowT> ranges = searchBounds == null ? null :
-                expressionFactory.ranges(searchBounds, rel.collation(), 
tbl.getRowType(typeFactory));
+        RangeIterable<RowT> ranges = null;
+
+        if (searchBounds != null) {
+            Comparator<RowT> searchRowComparator = null;
+
+            if (idx.collations() != null) {
+                searchRowComparator = 
expressionFactory.comparator(TraitUtils.createCollation(idx.collations()));
+            }
+
+            ranges = expressionFactory.ranges(searchBounds, 
idx.getRowType(typeFactory, tbl.descriptor()), searchRowComparator);
+        }
 
         RelCollation outputCollation = rel.collation();
 
@@ -329,7 +337,6 @@ public class LogicalRelImplementor<RowT> implements 
IgniteRelVisitor<Node<RowT>>
                 ctx.rowHandler().factory(ctx.getTypeFactory(), rowType),
                 idx,
                 tbl,
-                rel.collation().getKeys(),
                 group.partitions(ctx.localNode().name()),
                 comp,
                 ranges,
@@ -445,13 +452,14 @@ public class LogicalRelImplementor<RowT> implements 
IgniteRelVisitor<Node<RowT>>
         assert rel.searchBounds() != null : rel;
 
         Predicate<RowT> filter = expressionFactory.predicate(rel.condition(), 
rel.getRowType());
-        RangeIterable<RowT> ranges = 
expressionFactory.ranges(rel.searchBounds(), collation, rel.getRowType());
+        Comparator<RowT> comparator = expressionFactory.comparator(collation);
+        RangeIterable<RowT> ranges = 
expressionFactory.ranges(rel.searchBounds(), rel.getRowType(), comparator);
 
         IndexSpoolNode<RowT> node = IndexSpoolNode.createTreeSpool(
                 ctx,
                 rel.getRowType(),
                 collation,
-                expressionFactory.comparator(collation),
+                comparator,
                 filter,
                 ranges
         );
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/RowConverter.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/RowConverter.java
index 1daff10c43..c71b240e21 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/RowConverter.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/RowConverter.java
@@ -18,8 +18,7 @@
 package org.apache.ignite.internal.sql.engine.exec;
 
 import java.nio.ByteBuffer;
-import java.util.stream.IntStream;
-import org.apache.calcite.util.ImmutableIntList;
+import java.util.List;
 import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.binarytuple.BinaryTuplePrefixBuilder;
 import org.apache.ignite.internal.schema.BinaryConverter;
@@ -28,8 +27,10 @@ import org.apache.ignite.internal.schema.BinaryTuplePrefix;
 import org.apache.ignite.internal.schema.BinaryTupleSchema;
 import org.apache.ignite.internal.schema.BinaryTupleSchema.Element;
 import org.apache.ignite.internal.schema.NativeTypeSpec;
+import org.apache.ignite.internal.sql.engine.exec.exp.RexImpTable;
 import org.apache.ignite.internal.sql.engine.schema.TableDescriptor;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.apache.ignite.internal.util.IgniteUtils;
 
 /**
  * Helper class provides method to convert binary tuple to rows and vice-versa.
@@ -38,9 +39,9 @@ public final class RowConverter {
     /**
      * Creates binary tuple schema for index rows.
      */
-    public static BinaryTupleSchema createIndexRowSchema(TableDescriptor 
tableDescriptor, ImmutableIntList idxColumnMapping) {
-        Element[] elements = IntStream.of(idxColumnMapping.toIntArray())
-                .mapToObj(tableDescriptor::columnDescriptor)
+    public static BinaryTupleSchema createIndexRowSchema(List<String> 
indexedColumns, TableDescriptor tableDescriptor) {
+        Element[] elements = indexedColumns.stream()
+                .map(tableDescriptor::columnDescriptor)
                 .map(colDesc -> new Element(colDesc.physicalType(), true))
                 .toArray(Element[]::new);
 
@@ -60,29 +61,28 @@ public final class RowConverter {
     public static <RowT> BinaryTuplePrefix toBinaryTuplePrefix(
             ExecutionContext<RowT> ectx,
             BinaryTupleSchema binarySchema,
-            ImmutableIntList idxColumnMapper,
             RowHandler.RowFactory<RowT> factory,
             RowT searchRow
     ) {
         RowHandler<RowT> handler = factory.handler();
 
-        int prefixColumnsCount = binarySchema.elementCount();
+        int indexedColumnsCount = binarySchema.elementCount();
+        int prefixColumnsCount = handler.columnCount(searchRow);
 
-        //TODO IGNITE-18056: Uncomment. Search row must be a valid index row 
prefix.
-        // assert handler.columnCount(searchRow) <= 
binarySchema.elementCount() : "Invalid range condition";
-        //
-        // int specifiedCols = handler.columnCount(searchRow);
+        assert prefixColumnsCount == indexedColumnsCount : "Invalid range 
condition";
 
         int specifiedCols = 0;
         for (int i = 0; i < prefixColumnsCount; i++) {
-            if (handler.get(idxColumnMapper.get(i), searchRow) != 
ectx.unspecifiedValue()) {
-                specifiedCols++;
+            if (handler.get(i, searchRow) == 
RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER) {
+                break;
             }
+
+            specifiedCols++;
         }
 
-        BinaryTuplePrefixBuilder tupleBuilder = new 
BinaryTuplePrefixBuilder(specifiedCols, prefixColumnsCount);
+        BinaryTuplePrefixBuilder tupleBuilder = new 
BinaryTuplePrefixBuilder(specifiedCols, indexedColumnsCount);
 
-        return new BinaryTuplePrefix(binarySchema, toByteBuffer(ectx, 
binarySchema, idxColumnMapper, handler, tupleBuilder, searchRow));
+        return new BinaryTuplePrefix(binarySchema, toByteBuffer(ectx, 
binarySchema, handler, tupleBuilder, searchRow));
     }
 
     /**
@@ -98,35 +98,42 @@ public final class RowConverter {
     public static <RowT> BinaryTuple toBinaryTuple(
             ExecutionContext<RowT> ectx,
             BinaryTupleSchema binarySchema,
-            ImmutableIntList idxColumnMapper,
             RowHandler.RowFactory<RowT> factory,
             RowT searchRow
     ) {
         RowHandler<RowT> handler = factory.handler();
 
-        int prefixColumnsCount = binarySchema.elementCount();
+        int rowColumnsCount = handler.columnCount(searchRow);
 
-        //TODO IGNITE-18056: Uncomment. Search row must be a valid index row.
-        // assert handler.columnCount(searchRow) == 
binarySchema.elementCount() : "Invalid lookup condition";
+        assert rowColumnsCount == binarySchema.elementCount() : "Invalid 
lookup key.";
 
-        BinaryTupleBuilder tupleBuilder = new 
BinaryTupleBuilder(prefixColumnsCount, binarySchema.hasNullableElements());
+        if (IgniteUtils.assertionsEnabled()) {
+            for (int i = 0; i < rowColumnsCount; i++) {
+                if (handler.get(i, searchRow) == 
RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER) {
+                    throw new AssertionError("Invalid lookup key.");
+                }
+            }
+        }
 
-        return new BinaryTuple(binarySchema, toByteBuffer(ectx, binarySchema, 
idxColumnMapper, handler, tupleBuilder, searchRow));
+        BinaryTupleBuilder tupleBuilder = new 
BinaryTupleBuilder(rowColumnsCount, binarySchema.hasNullableElements());
+
+        return new BinaryTuple(binarySchema, toByteBuffer(ectx, binarySchema, 
handler, tupleBuilder, searchRow));
     }
 
     private static <RowT> ByteBuffer toByteBuffer(
             ExecutionContext<RowT> ectx,
             BinaryTupleSchema binarySchema,
-            ImmutableIntList idxColumnMapper,
             RowHandler<RowT> handler,
             BinaryTupleBuilder tupleBuilder,
             RowT searchRow
     ) {
-        for (int i = 0; i < binarySchema.elementCount(); i++) {
-            Object val = handler.get(idxColumnMapper.get(i), searchRow);
+        int columnsCount = handler.columnCount(searchRow);
 
-            if (val == ectx.unspecifiedValue()) {
-                break;
+        for (int i = 0; i < columnsCount; i++) {
+            Object val = handler.get(i, searchRow);
+
+            if (val == RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER) {
+                break; // No more columns in prefix.
             }
 
             Element element = binarySchema.element(i);
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactory.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactory.java
index 9867073d55..0940ebe5ca 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactory.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactory.java
@@ -32,6 +32,7 @@ import org.apache.calcite.rex.RexNode;
 import org.apache.ignite.internal.sql.engine.exec.exp.agg.AccumulatorWrapper;
 import org.apache.ignite.internal.sql.engine.exec.exp.agg.AggregateType;
 import org.apache.ignite.internal.sql.engine.prepare.bounds.SearchBounds;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Expression factory.
@@ -110,13 +111,13 @@ public interface ExpressionFactory<RowT> {
      * Creates iterable search bounds tuples (lower row/upper row) by search 
bounds expressions.
      *
      * @param searchBounds Search bounds.
-     * @param collation Collation.
      * @param rowType Row type.
+     * @param comparator Comparator to return bounds in particular order.
      */
     RangeIterable<RowT> ranges(
             List<SearchBounds> searchBounds,
-            RelCollation collation,
-            RelDataType rowType
+            RelDataType rowType,
+            @Nullable Comparator<RowT> comparator
     );
 
     /**
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 1bc734e54e..beb5d14dbd 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
@@ -77,6 +77,7 @@ import 
org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.IgniteMethod;
 import org.apache.ignite.internal.sql.engine.util.Primitives;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Implements rex expression into a function object. Uses JaninoRexCompiler 
under the hood. Each expression compiles into a class and a
@@ -141,7 +142,7 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
             @Override
             public int compare(RowT o1, RowT o2) {
                 RowHandler<RowT> hnd = ctx.rowHandler();
-                Object unspecifiedVal = ctx.unspecifiedValue();
+                Object unspecifiedVal = 
RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER;
 
                 for (RelFieldCollation field : collation.getFieldCollations()) 
{
                     int fieldIdx = field.getFieldIndex();
@@ -302,8 +303,8 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
     @Override
     public RangeIterable<RowT> ranges(
             List<SearchBounds> searchBounds,
-            RelCollation collation,
-            RelDataType rowType
+            RelDataType rowType,
+            @Nullable Comparator<RowT> comparator
     ) {
         RowFactory<RowT> rowFactory = ctx.rowHandler().factory(typeFactory, 
rowType);
 
@@ -314,7 +315,6 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
                 searchBounds,
                 rowType,
                 rowFactory,
-                collation.getKeys(),
                 0,
                 Arrays.asList(new RexNode[searchBounds.size()]),
                 Arrays.asList(new RexNode[searchBounds.size()]),
@@ -322,7 +322,7 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
                 true
         );
 
-        return new RangeIterableImpl(ranges, comparator(collation));
+        return new RangeIterableImpl(ranges, comparator);
     }
 
     /**
@@ -332,8 +332,7 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
      * @param searchBounds Search bounds.
      * @param rowType Row type.
      * @param rowFactory Row factory.
-     * @param collationKeys Collation keys.
-     * @param collationKeyIdx Current collation key index (field to process).
+     * @param fieldIdx Current field index (field to process).
      * @param curLower Current lower row.
      * @param curUpper Current upper row.
      * @param lowerInclude Include current lower row.
@@ -344,16 +343,15 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
             List<SearchBounds> searchBounds,
             RelDataType rowType,
             RowFactory<RowT> rowFactory,
-            List<Integer> collationKeys,
-            int collationKeyIdx,
+            int fieldIdx,
             List<RexNode> curLower,
             List<RexNode> curUpper,
             boolean lowerInclude,
             boolean upperInclude
     ) {
-        if ((collationKeyIdx >= collationKeys.size())
+        if ((fieldIdx >= searchBounds.size())
                 || (!lowerInclude && !upperInclude)
-                || searchBounds.get(collationKeys.get(collationKeyIdx)) == 
null) {
+                || searchBounds.get(fieldIdx) == null) {
             ranges.add(new RangeConditionImpl(
                     scalar(curLower, rowType),
                     scalar(curUpper, rowType),
@@ -365,7 +363,6 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
             return;
         }
 
-        int fieldIdx = collationKeys.get(collationKeyIdx);
         SearchBounds fieldBounds = searchBounds.get(fieldIdx);
 
         Collection<SearchBounds> fieldMultiBounds = fieldBounds instanceof 
MultiBounds
@@ -405,8 +402,7 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
                     searchBounds,
                     rowType,
                     rowFactory,
-                    collationKeys,
-                    collationKeyIdx + 1,
+                    fieldIdx + 1,
                     curLower,
                     curUpper,
                     lowerInclude && fieldLowerInclude,
@@ -415,7 +411,7 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
         }
 
         curLower.set(fieldIdx, null);
-        curLower.set(fieldIdx, null);
+        curUpper.set(fieldIdx, null);
     }
 
     /**
@@ -432,7 +428,7 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
     /**
      * Creates {@link SingleScalar}, a code-generated expressions evaluator.
      *
-     * @param nodes Expressions. {@code Null} expressions will be evaluated to 
{@link ExecutionContext#unspecifiedValue()}.
+     * @param nodes Expressions. {@code Null} expressions will be evaluated to 
{@link RexImpTable#UNSPECIFIED_VALUE_PLACEHOLDER}.
      * @param type Row type.
      * @return SingleScalar.
      */
@@ -511,8 +507,9 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
         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);
+            Expression val = unspecifiedValues.get(i)
+                    ? Expressions.field(null, RexImpTable.class, 
"UNSPECIFIED_VALUE_PLACEHOLDER")
+                    : projects.get(i);
 
             builder.add(
                     Expressions.statement(
@@ -716,10 +713,10 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
         private final boolean upperInclude;
 
         /** Lower row. */
-        private RowT lowerRow;
+        private @Nullable RowT lowerRow;
 
         /** Upper row. */
-        private RowT upperRow;
+        private @Nullable RowT upperRow;
 
         /** Row factory. */
         private final RowFactory<RowT> factory;
@@ -771,7 +768,7 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
         }
 
         /** Clear cached rows. */
-        public void clearCache() {
+        void clearCache() {
             lowerRow = upperRow = null;
         }
     }
@@ -779,11 +776,14 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
     private class RangeIterableImpl implements RangeIterable<RowT> {
         private final List<RangeCondition<RowT>> ranges;
 
-        private final Comparator<RowT> comparator;
+        private final @Nullable Comparator<RowT> comparator;
 
         private boolean sorted;
 
-        public RangeIterableImpl(List<RangeCondition<RowT>> ranges, 
Comparator<RowT> comparator) {
+        RangeIterableImpl(
+                List<RangeCondition<RowT>> ranges,
+                @Nullable Comparator<RowT> comparator
+        ) {
             this.ranges = ranges;
             this.comparator = comparator;
         }
@@ -807,7 +807,7 @@ public class ExpressionFactoryImpl<RowT> implements 
ExpressionFactory<RowT> {
             // intersection.
             // Do not sort again if ranges already were sorted before, 
different values of correlated variables
             // should not affect ordering.
-            if (!sorted) {
+            if (!sorted && comparator != null) {
                 ranges.sort((o1, o2) -> comparator.compare(o1.lower(), 
o2.lower()));
                 sorted = true;
             }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java
index a08e8b00ef..bac45c0a5e 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java
@@ -247,6 +247,15 @@ public class RexImpTable {
     /** Placeholder for DEFAULT operator value. */
     public static final Object DEFAULT_VALUE_PLACEHOLDER = new 
DefaultValuePlaceholder();
 
+    /** Placeholder for values, which expressions are not specified. */
+    public static final Object UNSPECIFIED_VALUE_PLACEHOLDER = new Object() {
+        /** {@inheritDoc} */
+        @Override
+        public String toString() {
+            return "<unspecified_value>";
+        }
+    };
+
     /**
      * Constructor.
      * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
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 f02932d710..3fd4976ba1 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
@@ -32,7 +32,6 @@ import java.util.concurrent.Flow.Subscription;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.function.Function;
 import java.util.function.Predicate;
-import org.apache.calcite.util.ImmutableIntList;
 import org.apache.ignite.internal.index.SortedIndex;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.BinaryTuple;
@@ -78,8 +77,6 @@ public class IndexScanNode<RowT> extends AbstractNode<RowT> {
 
     private final Function<BinaryRow, RowT> tableRowConverter;
 
-    private final ImmutableIntList idxColumnMapping;
-
     /** Participating columns. */
     private final @Nullable BitSet requiredColumns;
 
@@ -95,7 +92,7 @@ public class IndexScanNode<RowT> extends AbstractNode<RowT> {
 
     private boolean inLoop;
 
-    private Subscription activeSubscription;
+    private @Nullable Subscription activeSubscription;
 
     private boolean rangeConditionsProcessed;
 
@@ -117,7 +114,6 @@ public class IndexScanNode<RowT> extends AbstractNode<RowT> 
{
             RowHandler.RowFactory<RowT> rowFactory,
             IgniteIndex schemaIndex,
             InternalIgniteTable schemaTable,
-            ImmutableIntList idxColumnMapping,
             int[] parts,
             @Nullable Comparator<RowT> comp,
             @Nullable RangeIterable<RowT> rangeConditions,
@@ -129,7 +125,7 @@ public class IndexScanNode<RowT> extends AbstractNode<RowT> 
{
 
         assert !nullOrEmpty(parts);
 
-        assert context().transaction() != null || context().transactionTime() 
!= null : "Transaction not initialized.";
+        assert ctx.transaction() != null || ctx.transactionTime() != null : 
"Transaction not initialized.";
 
         this.schemaIndex = schemaIndex;
         this.parts = parts;
@@ -137,7 +133,6 @@ public class IndexScanNode<RowT> extends AbstractNode<RowT> 
{
         this.rowTransformer = rowTransformer;
         this.requiredColumns = requiredColumns;
         this.rangeConditions = rangeConditions;
-        this.idxColumnMapping = idxColumnMapping;
         this.comp = comp;
         this.factory = rowFactory;
 
@@ -145,7 +140,7 @@ public class IndexScanNode<RowT> extends AbstractNode<RowT> 
{
 
         tableRowConverter = row -> schemaTable.toRow(context(), row, factory, 
requiredColumns);
 
-        indexRowSchema = 
RowConverter.createIndexRowSchema(schemaTable.descriptor(), idxColumnMapping);
+        indexRowSchema = 
RowConverter.createIndexRowSchema(schemaIndex.columns(), 
schemaTable.descriptor());
     }
 
     /** {@inheritDoc} */
@@ -321,11 +316,9 @@ public class IndexScanNode<RowT> extends 
AbstractNode<RowT> {
             );
         } else {
             assert schemaIndex.type() == Type.HASH;
-            BinaryTuple key = null;
+            assert cond != null && cond.lower() != null : "Invalid hash index 
condition.";
 
-            if (cond != null) {
-                key = toBinaryTuple(cond.lower());
-            }
+            BinaryTuple key = toBinaryTuple(cond.lower());
 
             pub = schemaIndex.index().lookup(
                     part,
@@ -412,7 +405,7 @@ public class IndexScanNode<RowT> extends AbstractNode<RowT> 
{
             return null;
         }
 
-        return RowConverter.toBinaryTuplePrefix(context(), indexRowSchema, 
idxColumnMapping, factory, condition);
+        return RowConverter.toBinaryTuplePrefix(context(), indexRowSchema, 
factory, condition);
     }
 
     @Contract("null -> null")
@@ -421,7 +414,7 @@ public class IndexScanNode<RowT> extends AbstractNode<RowT> 
{
             return null;
         }
 
-        return RowConverter.toBinaryTuple(context(), indexRowSchema, 
idxColumnMapping, factory, condition);
+        return RowConverter.toBinaryTuple(context(), indexRowSchema, factory, 
condition);
     }
 
     private RowT convert(BinaryRow binaryRow) {
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/logical/IgniteLogicalIndexScan.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/logical/IgniteLogicalIndexScan.java
index 34f3226577..aef005a0a5 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/logical/IgniteLogicalIndexScan.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/logical/IgniteLogicalIndexScan.java
@@ -56,16 +56,24 @@ public class IgniteLogicalIndexScan extends 
AbstractIndexScan {
         IgniteIndex index = tbl.getIndex(idxName);
         RelCollation collation = TraitUtils.createCollation(index.columns(), 
index.collations(), tbl.descriptor());
 
-        if (requiredColumns != null) {
-            Mappings.TargetMapping targetMapping = 
Commons.mapping(requiredColumns,
-                    tbl.getRowType(typeFactory).getFieldCount());
-            collation = collation.apply(targetMapping);
-        }
-
         List<SearchBounds> searchBounds;
         if (index.type() == Type.HASH) {
-            searchBounds = buildHashIndexConditions(cluster, tbl, 
index.columns(), cond, requiredColumns);
+            if (requiredColumns != null) {
+                Mappings.TargetMapping targetMapping = 
Commons.mapping(requiredColumns, tbl.getRowType(typeFactory).getFieldCount());
+                RelCollation outputCollation = collation.apply(targetMapping);
+
+                searchBounds = (collation.getFieldCollations().size() == 
outputCollation.getFieldCollations().size())
+                        ? buildHashIndexConditions(cluster, tbl, 
outputCollation, cond, requiredColumns)
+                        : null;
+            } else {
+                searchBounds = buildHashIndexConditions(cluster, tbl, 
collation, cond, requiredColumns);
+            }
         } else if (index.type() == Type.SORTED) {
+            if (requiredColumns != null) {
+                Mappings.TargetMapping targetMapping = 
Commons.mapping(requiredColumns, tbl.getRowType(typeFactory).getFieldCount());
+                collation = collation.apply(targetMapping);
+            }
+
             searchBounds = buildSortedIndexConditions(cluster, tbl, collation, 
cond, requiredColumns);
         } else {
             throw new AssertionError("Unknown index type [type=" + 
index.type() + "]");
@@ -110,7 +118,7 @@ public class IgniteLogicalIndexScan extends 
AbstractIndexScan {
         super(cluster, traits, List.of(), tbl, idxName, type, proj, cond, 
searchBounds, requiredCols);
     }
 
-    private static List<SearchBounds> buildSortedIndexConditions(
+    private static @Nullable List<SearchBounds> buildSortedIndexConditions(
             RelOptCluster cluster,
             InternalIgniteTable table,
             RelCollation collation,
@@ -118,7 +126,7 @@ public class IgniteLogicalIndexScan extends 
AbstractIndexScan {
             @Nullable ImmutableBitSet requiredColumns
     ) {
         if (collation.getFieldCollations().isEmpty()) {
-            return List.of();
+            return null;
         }
 
         return RexUtils.buildSortedIndexConditions(
@@ -133,11 +141,16 @@ public class IgniteLogicalIndexScan extends 
AbstractIndexScan {
     private static List<SearchBounds> buildHashIndexConditions(
             RelOptCluster cluster,
             InternalIgniteTable table,
-            List<String> indexedColumns,
-            @Nullable RexNode cond,
+            RelCollation collation,
+            RexNode cond,
             @Nullable ImmutableBitSet requiredColumns
     ) {
-        return RexUtils.buildHashIndexConditions(cluster, indexedColumns, cond,
-                table.getRowType(Commons.typeFactory(cluster)), 
requiredColumns);
+        return RexUtils.buildHashIndexConditions(
+                cluster,
+                collation,
+                cond,
+                table.getRowType(Commons.typeFactory(cluster)),
+                requiredColumns
+        );
     }
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/FilterSpoolMergeToSortedIndexSpoolRule.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/FilterSpoolMergeToSortedIndexSpoolRule.java
index 2d1467f5f6..f0048d28a6 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/FilterSpoolMergeToSortedIndexSpoolRule.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rule/FilterSpoolMergeToSortedIndexSpoolRule.java
@@ -24,7 +24,6 @@ import it.unimi.dsi.fastutil.ints.IntList;
 import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
 import it.unimi.dsi.fastutil.ints.IntSet;
 import java.util.List;
-import java.util.stream.Collectors;
 import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptRule;
@@ -120,8 +119,8 @@ public class FilterSpoolMergeToSortedIndexSpoolRule extends 
RelRule<FilterSpoolM
 
             List<RelFieldCollation> collationFields = 
inCollation.getFieldCollations().subList(0, searchKeys.size());
 
-            assert 
searchKeys.containsAll(collationFields.stream().map(RelFieldCollation::getFieldIndex)
-                    .collect(Collectors.toSet())) : "Search condition should 
be a prefix of collation [searchKeys="
+            assert searchKeys.size() == collationFields.size()
+                    : "Search condition should be a prefix of collation 
[searchKeys="
                     + searchKeys + ", collation=" + inCollation + ']';
 
             searchCollation = RelCollations.of(collationFields);
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteIndex.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteIndex.java
index 750c3bd555..459baaec3e 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteIndex.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/IgniteIndex.java
@@ -17,13 +17,18 @@
 
 package org.apache.ignite.internal.sql.engine.schema;
 
+import static 
org.apache.ignite.internal.sql.engine.util.TypeUtils.native2relationalType;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.ignite.internal.index.ColumnCollation;
 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.sql.engine.type.IgniteTypeFactory;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -114,6 +119,31 @@ public class IgniteIndex {
         return type;
     }
 
+    //TODO: cache rowType as it can't be changed.
+
+    /** Returns index row type.
+     *
+     * <p>This is a struct type whose fields describe the names and types of 
indexed columns.</p>
+     *
+     * <p>The implementer must use the type factory provided. This ensures that
+     * the type is converted into a canonical form; other equal types in the 
same
+     * query will use the same object.</p>
+     *
+     * @param typeFactory Type factory with which to create the type
+     * @param tableDescriptor Table descriptor.
+     * @return Row type.
+     */
+    public RelDataType getRowType(IgniteTypeFactory typeFactory, 
TableDescriptor tableDescriptor) {
+        RelDataTypeFactory.Builder b = new 
RelDataTypeFactory.Builder(typeFactory);
+
+        for (String colName : columns) {
+            ColumnDescriptor colDesc = 
tableDescriptor.columnDescriptor(colName);
+            b.add(colName, native2relationalType(typeFactory, 
colDesc.physicalType(), colDesc.nullable()));
+        }
+
+        return b.build();
+    }
+
     private static @Nullable List<Collation> deriveCollations(Index<?> index) {
         if (index.descriptor() instanceof SortedIndexDescriptor) {
             SortedIndexDescriptor descriptor = (SortedIndexDescriptor) 
index.descriptor();
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/TableDescriptor.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/TableDescriptor.java
index fcfe5e83f5..e7b7e6723a 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/TableDescriptor.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/TableDescriptor.java
@@ -25,6 +25,7 @@ import 
org.apache.calcite.sql2rel.InitializerExpressionFactory;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistribution;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * TableDescriptor interface.
@@ -77,7 +78,7 @@ public interface TableDescriptor extends RelProtoDataType, 
InitializerExpression
      * @param usedColumns Participating columns numeration.
      * @return Row type.
      */
-    RelDataType rowType(IgniteTypeFactory factory, ImmutableBitSet 
usedColumns);
+    RelDataType rowType(IgniteTypeFactory factory, @Nullable ImmutableBitSet 
usedColumns);
 
     /**
      * Checks whether is possible to update a column with a given index.
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/TableDescriptorImpl.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/TableDescriptorImpl.java
index 969c375156..75500159c7 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/TableDescriptorImpl.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/schema/TableDescriptorImpl.java
@@ -38,6 +38,7 @@ import 
org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * TableDescriptorImpl.
@@ -174,7 +175,7 @@ public class TableDescriptorImpl extends 
NullInitializerExpressionFactory implem
 
     /** {@inheritDoc} */
     @Override
-    public RelDataType rowType(IgniteTypeFactory factory, ImmutableBitSet 
usedColumns) {
+    public RelDataType rowType(IgniteTypeFactory factory, @Nullable 
ImmutableBitSet usedColumns) {
         RelDataTypeFactory.Builder b = new RelDataTypeFactory.Builder(factory);
 
         if (usedColumns == null) {
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/trait/TraitUtils.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/trait/TraitUtils.java
index 7a6bb2efd8..0666e2d5fe 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/trait/TraitUtils.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/trait/TraitUtils.java
@@ -70,6 +70,7 @@ import 
org.apache.ignite.internal.sql.engine.rel.IgniteTableSpool;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTrimExchange;
 import org.apache.ignite.internal.sql.engine.schema.ColumnDescriptor;
 import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
+import org.apache.ignite.internal.sql.engine.schema.IgniteIndex.Collation;
 import org.apache.ignite.internal.sql.engine.schema.TableDescriptor;
 import org.apache.ignite.lang.ErrorGroups.Common;
 import org.apache.ignite.lang.IgniteInternalException;
@@ -501,6 +502,22 @@ public class TraitUtils {
         );
     }
 
+    /**
+     * Creates {@link RelCollation} object from a given collations.
+     *
+     * @param collations List of collations.
+     * @return a {@link RelCollation} object.
+     */
+    public static RelCollation createCollation(List<Collation> collations) {
+        List<RelFieldCollation> fieldCollations = new 
ArrayList<>(collations.size());
+
+        for (int i = 0; i < collations.size(); i++) {
+            fieldCollations.add(createFieldCollation(i,  collations.get(i)));
+        }
+
+        return RelCollations.of(fieldCollations);
+    }
+
     /**
      * Creates {@link RelCollation} object from a given collations.
      *
@@ -514,9 +531,9 @@ public class TraitUtils {
             @Nullable List<IgniteIndex.Collation> collations,
             TableDescriptor descriptor
     ) {
-        if (collations == null) { // Build collation for Hash index.
-            List<RelFieldCollation> fieldCollations = new ArrayList<>();
+        List<RelFieldCollation> fieldCollations = new 
ArrayList<>(indexedColumns.size());
 
+        if (collations == null) { // Build collation for Hash index.
             for (int i = 0; i < indexedColumns.size(); i++) {
                 String columnName = indexedColumns.get(i);
                 ColumnDescriptor columnDesc = 
descriptor.columnDescriptor(columnName);
@@ -527,8 +544,6 @@ public class TraitUtils {
             return RelCollations.of(fieldCollations);
         }
 
-        List<RelFieldCollation> fieldCollations = new ArrayList<>();
-
         for (int i = 0; i < indexedColumns.size(); i++) {
             String columnName = indexedColumns.get(i);
             IgniteIndex.Collation collation = collations.get(i);
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteMethod.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteMethod.java
index 5b8c0bbe86..c63290af52 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteMethod.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteMethod.java
@@ -48,8 +48,6 @@ public enum IgniteMethod {
     /** 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),
 
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/RexUtils.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/RexUtils.java
index 0e837c557c..0482e981d9 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/RexUtils.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/RexUtils.java
@@ -212,12 +212,12 @@ public class RexUtils {
     /**
      * Builds sorted index search bounds.
      */
-    public static List<SearchBounds> buildSortedIndexConditions(
+    public static @Nullable List<SearchBounds> buildSortedIndexConditions(
             RelOptCluster cluster,
             RelCollation collation,
-            RexNode condition,
+            @Nullable RexNode condition,
             RelDataType rowType,
-            ImmutableBitSet requiredColumns
+            @Nullable ImmutableBitSet requiredColumns
     ) {
         if (condition == null) {
             return null;
@@ -255,11 +255,13 @@ public class RexUtils {
             mapping = Commons.inverseMapping(requiredColumns, types.size());
         }
 
-        List<SearchBounds> bounds = Arrays.asList(new 
SearchBounds[types.size()]);
+        List<SearchBounds> bounds = Arrays.asList(new 
SearchBounds[collation.getFieldCollations().size()]);
         boolean boundsEmpty = true;
         int prevComplexity = 1;
 
-        for (RelFieldCollation fc : collation.getFieldCollations()) {
+        List<RelFieldCollation> fieldCollations = 
collation.getFieldCollations();
+        for (int i = 0; i < fieldCollations.size(); i++) {
+            RelFieldCollation fc = fieldCollations.get(i);
             int collFldIdx = fc.getFieldIndex();
 
             List<RexCall> collFldPreds = fieldsToPredicates.get(collFldIdx);
@@ -280,7 +282,7 @@ public class RexUtils {
 
             boundsEmpty = false;
 
-            bounds.set(collFldIdx, fldBounds);
+            bounds.set(i, fldBounds);
 
             if (fldBounds instanceof MultiBounds) {
                 prevComplexity *= ((MultiBounds) fldBounds).bounds().size();
@@ -304,10 +306,10 @@ public class RexUtils {
      */
     public static List<SearchBounds> buildHashIndexConditions(
             RelOptCluster cluster,
-            List<String> indexedColumns,
+            RelCollation collation,
             RexNode condition,
             RelDataType rowType,
-            ImmutableBitSet requiredColumns
+            @Nullable ImmutableBitSet requiredColumns
     ) {
         if (condition == null) {
             return null;
@@ -321,26 +323,23 @@ public class RexUtils {
             return null;
         }
 
-        List<SearchBounds> bounds = Arrays.asList(new 
SearchBounds[rowType.getFieldCount()]);
+        List<RelDataType> types = RelOptUtil.getFieldTypeList(rowType);
+
+        List<SearchBounds> bounds = Arrays.asList(new 
SearchBounds[collation.getFieldCollations().size()]);
 
         Mappings.TargetMapping toTrimmedRowMapping = null;
         if (requiredColumns != null) {
-            toTrimmedRowMapping = Commons.mapping(requiredColumns, 
rowType.getFieldCount());
+            toTrimmedRowMapping = Commons.inverseMapping(requiredColumns, 
types.size());
         }
 
-        for (String columnName : indexedColumns) {
-            RelDataTypeField field = rowType.getField(columnName, true, false);
-
-            if (field == null) {
-                return null;
-            }
-
-            int collFldIdx = toTrimmedRowMapping == null ? field.getIndex() : 
toTrimmedRowMapping.getTargetOpt(field.getIndex());
+        List<RelFieldCollation> fieldCollations = 
collation.getFieldCollations();
+        for (int i = 0; i < fieldCollations.size(); i++) {
+            int collFldIdx = fieldCollations.get(i).getFieldIndex();
 
             List<RexCall> collFldPreds = fieldsToPredicates.get(collFldIdx);
 
             if (nullOrEmpty(collFldPreds)) {
-                return null; // Hash index can't be applied to partial 
condition.
+                return null; // Partial condition implies index scan, which is 
not supported.
             }
 
             RexCall columnPred = collFldPreds.stream()
@@ -348,10 +347,14 @@ public class RexUtils {
                     .findAny().orElse(null);
 
             if (columnPred == null) {
-                return null;
+                return null; // Non-equality conditions are not expected.
+            }
+
+            if (toTrimmedRowMapping != null) {
+                collFldIdx = toTrimmedRowMapping.getSourceOpt(collFldIdx);
             }
 
-            bounds.set(collFldIdx, createBounds(null, 
Collections.singletonList(columnPred), cluster, field.getType(), 1));
+            bounds.set(i, createBounds(null, 
Collections.singletonList(columnPred), cluster, types.get(collFldIdx), 1));
         }
 
         return bounds;
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 39fa23fec7..b3d924244e 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
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.sql.engine.exec.rel;
 
+import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCause;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.IsEqual.equalTo;
 import static org.mockito.Mockito.when;
@@ -34,7 +35,6 @@ import java.util.concurrent.ThreadLocalRandom;
 import java.util.stream.IntStream;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory.Builder;
-import org.apache.calcite.util.ImmutableIntList;
 import org.apache.ignite.internal.index.ColumnCollation;
 import org.apache.ignite.internal.index.Index;
 import org.apache.ignite.internal.index.IndexDescriptor;
@@ -53,6 +53,7 @@ import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 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.sql.engine.exec.exp.RexImpTable;
 import org.apache.ignite.internal.sql.engine.planner.AbstractPlannerTest;
 import org.apache.ignite.internal.sql.engine.schema.IgniteIndex;
 import org.apache.ignite.internal.sql.engine.schema.IgniteIndex.Type;
@@ -62,7 +63,6 @@ import 
org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
-import org.apache.ignite.internal.testframework.IgniteTestUtils;
 import org.hamcrest.Matchers;
 import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.BeforeAll;
@@ -74,6 +74,7 @@ import org.mockito.Mockito;
  * Note: we just bounds are valid and don't care that data meets bound 
conditions.
  * Bound condition applies in underlying storage, which is mocked here.
  */
+@SuppressWarnings("ThrowableNotThrown")
 public class IndexScanNodeExecutionTest extends AbstractExecutionTest {
     private static final Comparator<Object[]> comp = 
Comparator.comparingLong(v -> (long) ((Object[]) v)[0]);
 
@@ -112,135 +113,170 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
     }
 
     @Test
-    public void sortedIndexScan() {
+    public void sortedIndexScanWithExactBound() {
+        // Lower bound.
         validateSortedIndexScan(
                 sortedIndexData,
-                new Object[]{null, 2, 1, null},
-                new Object[]{null, 3, 0, null},
+                new Object[]{2L, 1},
+                null,
                 sortedScanResult
         );
-    }
-
-    @Test
-    public void sortedIndexScan2() {
         validateSortedIndexScan(
                 sortedIndexData,
-                new Object[]{null, 2, 1, null},
-                new Object[]{null, 4, null, null},
+                new Object[]{2L, null},
+                null,
                 sortedScanResult
-
         );
         validateSortedIndexScan(
                 sortedIndexData,
-                new Object[]{null, 2, null, null},
-                new Object[]{null, 4, 0, null},
+                new Object[]{null, 1},
+                null,
                 sortedScanResult
         );
-    }
-
-    @Test
-    public void sortedIndexScanNoUpperBound() {
         validateSortedIndexScan(
                 sortedIndexData,
-                new Object[]{null, 2, 1, null},
+                new Object[]{null, null},
                 null,
                 sortedScanResult
         );
-
+        // Upper bound.
+        validateSortedIndexScan(
+                sortedIndexData,
+                null,
+                new Object[]{4L, 0},
+                sortedScanResult
+        );
+        validateSortedIndexScan(
+                sortedIndexData,
+                null,
+                new Object[]{4L, null},
+                sortedScanResult
+        );
         validateSortedIndexScan(
                 sortedIndexData,
-                new Object[]{null, null, null, null},
                 null,
+                new Object[]{null, 0},
+                sortedScanResult
+        );
+        validateSortedIndexScan(
+                sortedIndexData,
+                null,
+                new Object[]{null, null},
                 sortedScanResult
         );
     }
 
     @Test
-    public void sortedIndexScanNoLowerBound() {
+    public void sortedIndexScanWithPrefixBound() {
         validateSortedIndexScan(
                 sortedIndexData,
+                new Object[]{2L, RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER},
+                null,
+                sortedScanResult
+        );
+        validateSortedIndexScan(
+                sortedIndexData,
+                new Object[]{null, RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER},
                 null,
-                new Object[]{null, 4, 0, null},
                 sortedScanResult
         );
 
         validateSortedIndexScan(
                 sortedIndexData,
                 null,
-                new Object[]{null, null, null, null},
+                new Object[]{4L, RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER},
+                sortedScanResult
+        );
+        validateSortedIndexScan(
+                sortedIndexData,
+                null,
+                new Object[]{null, RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER},
                 sortedScanResult
         );
     }
 
     @Test
     public void sortedIndexScanInvalidBounds() {
-        IgniteTestUtils.assertThrowsWithCause(() ->
+        assertThrowsWithCause(() ->
                 validateSortedIndexScan(
                         sortedIndexData,
-                        new Object[]{null, 2, "Brutus", null},
-                        new Object[]{null, 3.9, 0, null},
+                        new Object[]{2L, "Brutus"},
+                        null,
                         EMPTY
                 ), ClassCastException.class, "class java.lang.String cannot be 
cast to class java.lang.Integer");
 
-        IgniteTestUtils.assertThrowsWithCause(() ->
+        assertThrowsWithCause(() ->
                 validateSortedIndexScan(
                         sortedIndexData,
-                        new Object[]{null, 2},
-                        new Object[]{null, 3},
+                        null,
+                        new Object[]{3.9, 0},
                         EMPTY
-                ), ArrayIndexOutOfBoundsException.class, "Index 2 out of 
bounds for length 2");
+                ), ClassCastException.class, "class java.lang.Double cannot be 
cast to class java.lang.Long");
+
+        assertThrowsWithCause(() ->
+                validateSortedIndexScan(
+                        sortedIndexData,
+                        new Object[]{1L},
+                        null,
+                        EMPTY
+                ), AssertionError.class, "Invalid range condition");
     }
 
     @Test
     public void hashIndexLookupOverEmptyIndex() {
         validateHashIndexScan(
                 EMPTY,
-                new Object[]{null, 1, 3, null},
+                new Object[]{1L, 3},
                 EMPTY
         );
     }
 
     @Test
     public void hashIndexLookupNoKey() {
-        // Validate data.
-        validateHashIndexScan(
-                hashIndexData,
-                null,
-                hashScanResult
-        );
+        assertThrowsWithCause(() ->
+                validateHashIndexScan(
+                        hashIndexData,
+                        null,
+                        hashScanResult
+                ), AssertionError.class, "Invalid hash index condition.");
     }
 
     @Test
     public void hashIndexLookup() {
         validateHashIndexScan(
                 hashIndexData,
-                new Object[]{null, 4, 2, null},
+                new Object[]{4L, 2},
                 hashScanResult);
-    }
 
-    @Test
-    public void hashIndexLookupEmptyKey() {
         validateHashIndexScan(
                 hashIndexData,
-                new Object[]{null, null, null, null},
+                new Object[]{null, null},
                 hashScanResult);
     }
 
     @Test
     public void hashIndexLookupInvalidKey() {
-        IgniteTestUtils.assertThrowsWithCause(() ->
+        // Hash index doesn't support range scans with prefix bounds.
+        assertThrowsWithCause(() ->
                 validateHashIndexScan(
                         hashIndexData,
-                        new Object[]{2},
+                        new Object[]{2L},
                         EMPTY
-                ), ArrayIndexOutOfBoundsException.class, "Index 2 out of 
bounds for length 1");
+                ), AssertionError.class, "Invalid lookup key");
 
-        IgniteTestUtils.assertThrowsWithCause(() ->
+        assertThrowsWithCause(() ->
                 validateHashIndexScan(
                         hashIndexData,
-                        new Object[]{null, 2, "Brutus", null},
+                        new Object[]{2L, "Brutus"},
                         EMPTY
                 ), ClassCastException.class, "class java.lang.String cannot be 
cast to class java.lang.Integer");
+
+        assertThrowsWithCause(() ->
+                validateHashIndexScan(
+                        sortedIndexData,
+                        new Object[]{1L, 
RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER},
+                        EMPTY
+                ), AssertionError.class, "Invalid lookup key");
     }
 
     private static Object[][] generateIndexData(int partCnt, int partSize, 
boolean sorted) {
@@ -265,7 +301,7 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
                 data[rowNum] = new Object[4];
 
                 int bound1 = ThreadLocalRandom.current().nextInt(3);
-                int bound2 = ThreadLocalRandom.current().nextInt(3);
+                long bound2 = ThreadLocalRandom.current().nextLong(3);
 
                 data[rowNum][0] = uniqueNumList.get(rowNum);
                 data[rowNum][1] = bound1 == 0 ? null : bound1;
@@ -277,13 +313,13 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
         return data;
     }
 
-    private void validateHashIndexScan(Object[][] tableData, @Nullable 
Object[] key, Object[][] expRes) {
+    private void validateHashIndexScan(Object[][] tableData, @Nullable Object 
@Nullable [] key, Object[][] expRes) {
         SchemaDescriptor schemaDescriptor = new SchemaDescriptor(
                 1,
                 new Column[]{new Column("key", NativeTypes.INT64, false)},
                 new Column[]{
                         new Column("idxCol1", NativeTypes.INT32, true),
-                        new Column("idxCol2", NativeTypes.INT32, true),
+                        new Column("idxCol2", NativeTypes.INT64, true),
                         new Column("val", 
NativeTypes.stringOf(Integer.MAX_VALUE), true)
                 }
         );
@@ -310,13 +346,13 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
         Mockito.doReturn(hashIndexMock).when(indexMock).index();
         Mockito.doReturn(indexDescriptor.columns()).when(indexMock).columns();
 
-        validateIndexScan(tableData, schemaDescriptor, indexMock, key, key, 
expRes);
+        validateIndexScan(schemaDescriptor, indexMock, key, key, expRes);
     }
 
     private void validateSortedIndexScan(
             Object[][] tableData,
-            Object[] lowerBound,
-            Object[] upperBound,
+            Object @Nullable [] lowerBound,
+            Object @Nullable [] upperBound,
             Object[][] expectedData
     ) {
         SchemaDescriptor schemaDescriptor = new SchemaDescriptor(
@@ -324,7 +360,7 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
                 new Column[]{new Column("key", NativeTypes.INT64, false)},
                 new Column[]{
                         new Column("idxCol1", NativeTypes.INT32, true),
-                        new Column("idxCol2", NativeTypes.INT32, true),
+                        new Column("idxCol2", NativeTypes.INT64, true),
                         new Column("val", 
NativeTypes.stringOf(Integer.MAX_VALUE), true)
                 }
         );
@@ -332,7 +368,7 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
         SortedIndexDescriptor indexDescriptor = new SortedIndexDescriptor(
                 "IDX1",
                 List.of("idxCol2", "idxCol1"),
-                List.of(ColumnCollation.ASC_NULLS_FIRST, 
ColumnCollation.ASC_NULLS_LAST)
+                List.of(ColumnCollation.ASC_NULLS_LAST, 
ColumnCollation.ASC_NULLS_LAST)
 
         );
 
@@ -357,15 +393,14 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
         Mockito.doReturn(sortedIndexMock).when(indexMock).index();
         Mockito.doReturn(indexDescriptor.columns()).when(indexMock).columns();
 
-        validateIndexScan(tableData, schemaDescriptor, indexMock, lowerBound, 
upperBound, expectedData);
+        validateIndexScan(schemaDescriptor, indexMock, lowerBound, upperBound, 
expectedData);
     }
 
     private void validateIndexScan(
-            Object[][] tableData,
             SchemaDescriptor schemaDescriptor,
             IgniteIndex index,
-            Object[] lowerBound,
-            Object[] upperBound,
+            Object @Nullable [] lowerBound,
+            Object @Nullable [] upperBound,
             Object[][] expectedData
     ) {
         ExecutionContext<Object[]> ectx = executionContext(true);
@@ -388,17 +423,11 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
             when(rangeIterable.iterator()).thenAnswer(inv -> 
List.of(range).iterator());
         }
 
-        ImmutableIntList idxColMapping = 
ImmutableIntList.of(index.columns().stream()
-                .map(schemaDescriptor::column)
-                .mapToInt(Column::schemaIndex)
-                .toArray());
-
         IndexScanNode<Object[]> scanNode = new IndexScanNode<>(
                 ectx,
                 ectx.rowHandler().factory(ectx.getTypeFactory(), rowType),
                 index,
                 new TestTable(rowType, schemaDescriptor),
-                idxColMapping,
                 new int[]{0, 2},
                 index.type() == Type.SORTED ? comp : null,
                 rangeIterable,
@@ -423,7 +452,7 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
         Builder rowTypeBuilder = new Builder(typeFactory);
 
         IntStream.range(0, schemaDescriptor.length())
-                .mapToObj(i -> schemaDescriptor.column(i))
+                .mapToObj(schemaDescriptor::column)
                 .forEach(col -> rowTypeBuilder.add(col.name(), 
TypeUtils.native2relationalType(typeFactory, col.type(), col.nullable())));
 
         return rowTypeBuilder.build();
@@ -515,9 +544,11 @@ public class IndexScanNodeExecutionTest extends 
AbstractExecutionTest {
 
         for (int i = 0; i < bound.count(); i++) {
             Column col = schemaDescriptor.column(idxCols.get(i));
-            Object val = bound.value(i);
+            Object val = bound.hasNullValue(i) ? null : bound.value(i);
+
+            if (val == null) {
+                assertThat("Unexpected null value: columnName" + 
idxCols.get(i), col.nullable(), Matchers.is(Boolean.TRUE));
 
-            if (col.nullable() && val == null) {
                 continue;
             }
 
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 cc12312b35..13945d9373 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
@@ -33,6 +33,7 @@ import org.apache.calcite.util.ImmutableIntList;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 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.sql.engine.exec.exp.RexImpTable;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
@@ -181,7 +182,7 @@ public class SortedIndexSpoolExecutionTest extends 
AbstractExecutionTest {
         RootRewindable<Object[]> root = new RootRewindable<>(ctx);
         root.register(spool);
 
-        Object x = ctx.unspecifiedValue(); // Unspecified filter value.
+        Object x = RexImpTable.UNSPECIFIED_VALUE_PLACEHOLDER; // Unspecified 
filter value.
 
         // Test tuple (lower, upper, expected result size).
         List<TestParams> testBounds = Arrays.asList(
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/CorrelatedNestedLoopJoinPlannerTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/CorrelatedNestedLoopJoinPlannerTest.java
index 53bce77d6d..56b0f66cf1 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/CorrelatedNestedLoopJoinPlannerTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/CorrelatedNestedLoopJoinPlannerTest.java
@@ -102,12 +102,11 @@ public class CorrelatedNestedLoopJoinPlannerTest extends 
AbstractPlannerTest {
         List<SearchBounds> searchBounds = idxScan.searchBounds();
 
         assertNotNull(searchBounds, "Invalid plan\n" + 
RelOptUtil.toString(phys));
-        assertEquals(3, searchBounds.size());
+        assertEquals(2, searchBounds.size());
 
-        assertNull(searchBounds.get(0));
-        assertTrue(searchBounds.get(1) instanceof ExactBounds);
-        assertTrue(((ExactBounds) searchBounds.get(1)).bound() instanceof 
RexFieldAccess);
-        assertNull(searchBounds.get(2));
+        assertTrue(searchBounds.get(0) instanceof ExactBounds);
+        assertTrue(((ExactBounds) searchBounds.get(0)).bound() instanceof 
RexFieldAccess);
+        assertNull(searchBounds.get(1));
     }
 
     /**
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ProjectFilterScanMergePlannerTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ProjectFilterScanMergePlannerTest.java
index 600d46a7f4..0cbd5ea986 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ProjectFilterScanMergePlannerTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ProjectFilterScanMergePlannerTest.java
@@ -35,7 +35,7 @@ import 
org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem;
-import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 
@@ -48,7 +48,7 @@ public class ProjectFilterScanMergePlannerTest extends 
AbstractPlannerTest {
     private IgniteSchema publicSchema;
 
     /** {@inheritDoc} */
-    @BeforeAll
+    @BeforeEach
     public void setup() {
         publicSchema = new IgniteSchema("PUBLIC");
 
@@ -87,7 +87,7 @@ public class ProjectFilterScanMergePlannerTest extends 
AbstractPlannerTest {
     }
 
     @Test
-    public void testProjectFilterMergeIndex() throws Exception {
+    public void testProjectFilterMergeSortedIndex() throws Exception {
         // Test project and filter merge into index scan.
         TestTable tbl = ((TestTable) publicSchema.getTable("TBL"));
         tbl.addIndex(new 
IgniteIndex(TestSortedIndex.create(RelCollations.of(2), "IDX_C", tbl)));
@@ -113,6 +113,33 @@ public class ProjectFilterScanMergePlannerTest extends 
AbstractPlannerTest {
         );
     }
 
+    @Test
+    public void testProjectFilterMergeHashIndex() throws Exception {
+        // Test project and filter merge into index scan.
+        TestTable tbl = ((TestTable) publicSchema.getTable("TBL"));
+        tbl.addIndex(new IgniteIndex(TestHashIndex.create(List.of("c"), 
"IDX_C")));
+
+        // Without index condition shift.
+        assertPlan("SELECT a, b FROM tbl WHERE c = 0", publicSchema, 
isInstanceOf(IgniteIndexScan.class)
+                .and(scan -> scan.projects() != null)
+                .and(scan -> "[$t0, $t1]".equals(scan.projects().toString()))
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=($t2, 0)".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(0, 1, 
2).equals(scan.requiredColumns()))
+                .and(scan -> "[=($t2, 
0)]".equals(searchBoundsCondition(scan.searchBounds()).toString()))
+        );
+
+        // Index condition shifted according to requiredColumns.
+        assertPlan("SELECT b FROM tbl WHERE c = 0", publicSchema, 
isInstanceOf(IgniteIndexScan.class)
+                .and(scan -> scan.projects() != null)
+                .and(scan -> "[$t0]".equals(scan.projects().toString()))
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=($t1, 0)".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(1, 
2).equals(scan.requiredColumns()))
+                .and(scan -> "[=($t1, 
0)]".equals(searchBoundsCondition(scan.searchBounds()).toString()))
+        );
+    }
+
     @Test
     public void testIdentityFilterMergeIndex() throws Exception {
         // Test project and filter merge into index scan.
@@ -138,6 +165,31 @@ public class ProjectFilterScanMergePlannerTest extends 
AbstractPlannerTest {
         );
     }
 
+    @Test
+    public void testIdentityFilterMergeHashIndex() throws Exception {
+        // Test project and filter merge into index scan.
+        TestTable tbl = ((TestTable) publicSchema.getTable("TBL"));
+        tbl.addIndex(new IgniteIndex(TestHashIndex.create(List.of("c"), 
"IDX_C")));
+
+        // Without index condition shift.
+        assertPlan("SELECT a, b, c FROM tbl WHERE c = 0", publicSchema, 
isInstanceOf(IgniteIndexScan.class)
+                .and(scan -> scan.projects() == null)
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=($t2, 0)".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(0, 1, 
2).equals(scan.requiredColumns()))
+                .and(scan -> "[=($t2, 
0)]".equals(searchBoundsCondition(scan.searchBounds()).toString()))
+        );
+
+        // Index condition shift and identity.
+        assertPlan("SELECT b, c FROM tbl WHERE c = 0", publicSchema, 
isInstanceOf(IgniteIndexScan.class)
+                .and(scan -> scan.projects() == null)
+                .and(scan -> scan.condition() != null)
+                .and(scan -> "=($t1, 0)".equals(scan.condition().toString()))
+                .and(scan -> ImmutableBitSet.of(1, 
2).equals(scan.requiredColumns()))
+                .and(scan -> "[=($t1, 
0)]".equals(searchBoundsCondition(scan.searchBounds()).toString()))
+        );
+    }
+
     @Test
     public void testProjectFilterProjectMerge() throws Exception {
         // Inner query contains correlate, it prevents filter to be moved 
below project, and after HEP_FILTER_PUSH_DOWN
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/SortedIndexSpoolPlannerTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/SortedIndexSpoolPlannerTest.java
index cebf23eda7..9f2496826a 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/SortedIndexSpoolPlannerTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/SortedIndexSpoolPlannerTest.java
@@ -106,12 +106,11 @@ public class SortedIndexSpoolPlannerTest extends 
AbstractPlannerTest {
         List<SearchBounds> searchBounds = idxSpool.searchBounds();
 
         assertNotNull(searchBounds, "Invalid plan\n" + 
RelOptUtil.toString(phys));
-        assertEquals(3, searchBounds.size());
+        assertEquals(2, searchBounds.size());
 
-        assertNull(searchBounds.get(0));
-        assertTrue(searchBounds.get(1) instanceof ExactBounds);
-        assertTrue(((ExactBounds) searchBounds.get(1)).bound() instanceof 
RexFieldAccess);
-        assertNull(searchBounds.get(2));
+        assertTrue(searchBounds.get(0) instanceof ExactBounds);
+        assertTrue(((ExactBounds) searchBounds.get(0)).bound() instanceof 
RexFieldAccess);
+        assertNull(searchBounds.get(1));
     }
 
     /**
@@ -154,7 +153,7 @@ public class SortedIndexSpoolPlannerTest extends 
AbstractPlannerTest {
                         return IgniteDistributions.affinity(0, "T1", "hash");
                     }
                 }
-                        .addIndex("t1_jid0_idx", 1, 0)
+                        .addIndex("t1_jid0_idx", 2, 1)
         );
 
         String sql = "select * "
@@ -174,13 +173,12 @@ public class SortedIndexSpoolPlannerTest extends 
AbstractPlannerTest {
         List<SearchBounds> searchBounds = idxSpool.searchBounds();
 
         assertNotNull(searchBounds, "Invalid plan\n" + 
RelOptUtil.toString(phys));
-        assertEquals(4, searchBounds.size());
+        assertEquals(2, searchBounds.size());
 
-        assertNull(searchBounds.get(0));
+        assertTrue(searchBounds.get(0) instanceof ExactBounds);
+        assertTrue(((ExactBounds) searchBounds.get(0)).bound() instanceof 
RexFieldAccess);
         assertTrue(searchBounds.get(1) instanceof ExactBounds);
         assertTrue(((ExactBounds) searchBounds.get(1)).bound() instanceof 
RexFieldAccess);
-        assertNull(searchBounds.get(2));
-        assertNull(searchBounds.get(3));
     }
 
     /**
@@ -210,17 +208,15 @@ public class SortedIndexSpoolPlannerTest extends 
AbstractPlannerTest {
                                     // Condition is LESS_THEN, but we have 
DESC field and condition should be in lower bound
                                     // instead of upper bound.
                                     assertNotNull(searchBounds);
-                                    assertEquals(3, searchBounds.size());
+                                    assertEquals(1, searchBounds.size());
 
-                                    assertNull(searchBounds.get(0));
-                                    assertTrue(searchBounds.get(1) instanceof 
RangeBounds);
-                                    RangeBounds fld1Bounds = (RangeBounds) 
searchBounds.get(1);
+                                    assertTrue(searchBounds.get(0) instanceof 
RangeBounds);
+                                    RangeBounds fld1Bounds = (RangeBounds) 
searchBounds.get(0);
                                     assertTrue(fld1Bounds.lowerBound() 
instanceof RexFieldAccess);
                                     assertFalse(fld1Bounds.lowerInclude());
                                     // NULLS LAST in collation, so nulls can 
be skipped by upper bound.
                                     assertTrue(((RexLiteral) 
fld1Bounds.upperBound()).isNull());
                                     assertFalse(fld1Bounds.upperInclude());
-                                    assertNull(searchBounds.get(2));
 
                                     return true;
                                 })
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/PartitionReplicaListener.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/PartitionReplicaListener.java
index dbc0c2585f..5dbb8f0475 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/PartitionReplicaListener.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/PartitionReplicaListener.java
@@ -640,13 +640,13 @@ public class PartitionReplicaListener implements 
ReplicaListener {
 
         BinaryTuple key = request.exactKey();
 
-        @SuppressWarnings("resource") Cursor<RowId> cursor = (Cursor<RowId>) 
cursors.computeIfAbsent(cursorId,
+        Cursor<RowId> cursor = (Cursor<RowId>) 
cursors.computeIfAbsent(cursorId,
                 id -> indexStorage.get(key));
 
         final ArrayList<BinaryRow> result = new ArrayList<>(batchCount);
 
         return continueReadOnlyIndexLookup(cursor, timestamp, batchCount, 
result)
-                .thenCompose(ignore -> 
CompletableFuture.completedFuture(result));
+                .thenCompose(ignore -> completedFuture(result));
     }
 
     private CompletableFuture<List<BinaryRow>> lookupIndex(
@@ -666,14 +666,11 @@ public class PartitionReplicaListener implements 
ReplicaListener {
             return lockManager.acquire(txId, new LockKey(tableId), 
LockMode.IS).thenCompose(tblLock -> { // Table IS lock
                 return lockManager.acquire(txId, new LockKey(indexId, 
exactKey.byteBuffer()), LockMode.S)
                         .thenCompose(indRowLock -> { // Hash index bucket S 
lock
-                            @SuppressWarnings("resource") Cursor<RowId> cursor 
= (Cursor<RowId>) cursors.computeIfAbsent(cursorId,
-                                    id -> {
-                                        return indexStorage.get(exactKey);
-                                    });
+                            Cursor<RowId> cursor = (Cursor<RowId>) 
cursors.computeIfAbsent(cursorId, id -> indexStorage.get(exactKey));
 
                             final ArrayList<BinaryRow> result = new 
ArrayList<>(batchCount);
 
-                            return continueIndexLookup(txId, indexId, cursor, 
batchCount, result)
+                            return continueIndexLookup(txId, cursor, 
batchCount, result)
                                     .thenApply(ignore -> result);
                         });
             });
@@ -705,7 +702,7 @@ public class PartitionReplicaListener implements 
ReplicaListener {
 
         return lockManager.acquire(txId, new LockKey(indexId), 
LockMode.IS).thenCompose(idxLock -> { // Index IS lock
             return lockManager.acquire(txId, new LockKey(tableId), 
LockMode.IS).thenCompose(tblLock -> { // Table IS lock
-                @SuppressWarnings("resource") Cursor<IndexRow> cursor = 
(Cursor<IndexRow>) cursors.computeIfAbsent(cursorId,
+                Cursor<IndexRow> cursor = (Cursor<IndexRow>) 
cursors.computeIfAbsent(cursorId,
                         id -> {
                             // TODO 
https://issues.apache.org/jira/browse/IGNITE-18057
                             // Fix scan cursor return item closet to 
lowerbound and <= lowerbound
@@ -714,7 +711,6 @@ public class PartitionReplicaListener implements 
ReplicaListener {
                                     lowerBound,
                                     // We need upperBound next value for 
correct range lock.
                                     upperBound,
-                                    // TODO IGNITE-18055: Add support 
null-bounds.
                                     flags
                             );
                         });
@@ -723,7 +719,7 @@ public class PartitionReplicaListener implements 
ReplicaListener {
 
                 final ArrayList<BinaryRow> result = new 
ArrayList<>(batchCount);
 
-                return continueIndexScan(txId, indexId, indexLocker, cursor, 
batchCount, result)
+                return continueIndexScan(txId, indexLocker, cursor, 
batchCount, result)
                         .thenApply(ignore -> result);
             });
         });
@@ -751,29 +747,27 @@ public class PartitionReplicaListener implements 
ReplicaListener {
 
         int flags = request.flags();
 
-        @SuppressWarnings("resource") Cursor<IndexRow> cursor = 
(Cursor<IndexRow>) cursors.computeIfAbsent(cursorId,
-                id -> {
-                    return indexStorage.scan(
-                            lowerBound,
-                            upperBound,
-                            flags
-                    );
-                });
+        Cursor<IndexRow> cursor = (Cursor<IndexRow>) 
cursors.computeIfAbsent(cursorId,
+                id -> indexStorage.scan(
+                        lowerBound,
+                        upperBound,
+                        flags
+                ));
 
         final ArrayList<BinaryRow> result = new ArrayList<>(batchCount);
 
         return continueReadOnlyIndexScan(cursor, timestamp, batchCount, result)
-                .thenCompose(ignore -> 
CompletableFuture.completedFuture(result));
+                .thenCompose(ignore -> completedFuture(result));
     }
 
-    CompletableFuture<Void> continueReadOnlyIndexScan(
+    private CompletableFuture<Void> continueReadOnlyIndexScan(
             Cursor<IndexRow> cursor,
             HybridTimestamp timestamp,
             int batchSize,
             List<BinaryRow> result
     ) {
         if (result.size() >= batchSize || !cursor.hasNext()) {
-            return CompletableFuture.completedFuture(null);
+            return completedFuture(null);
         }
 
         IndexRow indexRow = cursor.next();
@@ -805,32 +799,30 @@ public class PartitionReplicaListener implements 
ReplicaListener {
     }
 
     /**
-     * Index scan loop. Retrives next row from index, takes locks, fetches 
associated data row and collects to the result.
+     * Index scan loop. Retrieves next row from index, takes locks, fetches 
associated data row and collects to the result.
      *
      * @param txId Transaction id.
-     * @param indexId Index id.
      * @param indexLocker Index locker.
      * @param indexCursor Index cursor.
      * @param batchSize Batch size.
      * @param result Result collection.
      * @return Future.
      */
-    CompletableFuture<Void> continueIndexScan(
+    private CompletableFuture<Void> continueIndexScan(
             UUID txId,
-            UUID indexId,
             SortedIndexLocker indexLocker,
             Cursor<IndexRow> indexCursor,
             int batchSize,
             List<BinaryRow> result
     ) {
         if (result.size() == batchSize) { // Batch is full, exit loop.
-            return CompletableFuture.completedFuture(null);
+            return completedFuture(null);
         }
 
         return indexLocker.locksForScan(txId, indexCursor)
                 .thenCompose(currentRow -> { // Index row S lock
                     if (currentRow == null) {
-                        return CompletableFuture.completedFuture(null); // End 
of range reached. Exit loop.
+                        return completedFuture(null); // End of range reached. 
Exit loop.
                     }
 
                     return lockManager.acquire(txId, new LockKey(tableId, 
currentRow.rowId()), LockMode.S)
@@ -844,22 +836,21 @@ public class PartitionReplicaListener implements 
ReplicaListener {
 
                                 // Proceed scan.
                                 return CompletableFuture.supplyAsync(
-                                        () -> continueIndexScan(txId, indexId, 
indexLocker, indexCursor, batchSize, result),
+                                        () -> continueIndexScan(txId, 
indexLocker, indexCursor, batchSize, result),
                                         scanRequestExecutor
                                 ).thenCompose(Function.identity());
                             });
                 });
     }
 
-    CompletableFuture<Void> continueIndexLookup(
+    private CompletableFuture<Void> continueIndexLookup(
             UUID txId,
-            UUID indexId,
             Cursor<RowId> indexCursor,
             int batchSize,
             List<BinaryRow> result
     ) {
         if (result.size() >= batchSize || !indexCursor.hasNext()) {
-            return CompletableFuture.completedFuture(null);
+            return completedFuture(null);
         }
 
         RowId rowId = indexCursor.next();
@@ -875,20 +866,20 @@ public class PartitionReplicaListener implements 
ReplicaListener {
 
                     // Proceed lookup.
                     return CompletableFuture.supplyAsync(
-                            () -> continueIndexLookup(txId, indexId, 
indexCursor, batchSize, result),
+                            () -> continueIndexLookup(txId, indexCursor, 
batchSize, result),
                             scanRequestExecutor
                     ).thenCompose(Function.identity());
                 });
     }
 
-    CompletableFuture<Void> continueReadOnlyIndexLookup(
+    private CompletableFuture<Void> continueReadOnlyIndexLookup(
             Cursor<RowId> indexCursor,
             HybridTimestamp timestamp,
             int batchSize,
             List<BinaryRow> result
     ) {
         if (result.size() >= batchSize || !indexCursor.hasNext()) {
-            return CompletableFuture.completedFuture(null);
+            return completedFuture(null);
         }
 
         RowId rowId = indexCursor.next();


Reply via email to