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

tkalkirill pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 8bbb7571c5e IGNITE-28331 Fix SQL search by _key for a composite pk in 
Calcite engine (#12926)
8bbb7571c5e is described below

commit 8bbb7571c5e8997d46c93ca3d5cd258d873620fe
Author: Kirill Tkalenko <[email protected]>
AuthorDate: Tue Apr 7 08:03:56 2026 +0300

    IGNITE-28331 Fix SQL search by _key for a composite pk in Calcite engine 
(#12926)
---
 .../query/calcite/exec/IndexWrappedKeyScan.java    | 105 +++++
 .../calcite/exec/exp/ExpressionFactoryImpl.java    |  15 +-
 .../query/calcite/exec/exp/IgniteSqlFunctions.java |  49 +++
 .../query/calcite/exec/exp/RexImpTable.java        |   2 +-
 .../query/calcite/exec/exp/agg/Accumulators.java   |  39 +-
 .../query/calcite/schema/CacheIndexImpl.java       | 118 ++++--
 .../calcite/schema/CacheWrappedKeyIndexImpl.java   | 100 +++++
 .../query/calcite/schema/SchemaHolderImpl.java     |  27 +-
 .../processors/query/calcite/util/Commons.java     |  30 ++
 .../query/calcite/integration/DataTypesTest.java   |   4 +-
 .../calcite/integration/SelectByKeyFieldTest.java  | 429 +++++++++++++++++++++
 .../ignite/testsuites/IntegrationTestSuite.java    |   3 +
 .../query/schema/management/IndexDescriptor.java   |   5 +
 13 files changed, 868 insertions(+), 58 deletions(-)

diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexWrappedKeyScan.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexWrappedKeyScan.java
new file mode 100644
index 00000000000..98b24622381
--- /dev/null
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/IndexWrappedKeyScan.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.calcite.exec;
+
+import java.util.Map;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.calcite.util.ImmutableIntList;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyDefinition;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyType;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexPlainRowImpl;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
+import 
org.apache.ignite.internal.cache.query.index.sorted.InlineIndexRowHandler;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndex;
+import org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKey;
+import 
org.apache.ignite.internal.cache.query.index.sorted.keys.IndexKeyFactory;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import 
org.apache.ignite.internal.processors.query.calcite.exec.exp.RangeIterable;
+import 
org.apache.ignite.internal.processors.query.calcite.schema.CacheTableDescriptor;
+import 
org.apache.ignite.internal.processors.query.calcite.schema.ColumnDescriptor;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils;
+import org.jetbrains.annotations.Nullable;
+
+/** Extension for column {@value QueryUtils#KEY_FIELD_NAME} in case of 
composite primary key. */
+public class IndexWrappedKeyScan<Row> extends IndexScan<Row> {
+    /** */
+    public IndexWrappedKeyScan(
+        ExecutionContext<Row> ectx,
+        CacheTableDescriptor desc,
+        InlineIndex idx,
+        ImmutableIntList idxFieldMapping,
+        int[] parts,
+        RangeIterable<Row> ranges,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        super(ectx, desc, idx, idxFieldMapping, parts, ranges, 
requiredColumns);
+    }
+
+    /** */
+    @Override protected IndexRow row2indexRow(Row bound) {
+        if (bound == null)
+            return null;
+
+        RowHandler<Row> rowHnd = ectx.rowHandler();
+
+        Object key = rowHnd.get(QueryUtils.KEY_COL, bound);
+        assert key != null : String.format("idxName=%s, bound=%s", idx.name(), 
Commons.toString(rowHnd, bound));
+
+        if (key instanceof BinaryObject)
+            return binaryObject2indexRow((BinaryObject)key);
+
+        throw new IgniteException(String.format(
+            "Unsupported type for index boundary: [expected=%s, current=%s]",
+            BinaryObject.class.getName(), key.getClass().getName()
+        ));
+    }
+
+    /** */
+    private IndexRow binaryObject2indexRow(BinaryObject o) {
+        assert 
o.type().typeName().equals(idx.indexDefinition().typeDescriptor().keyTypeName())
 : String.format(
+            "idx=%s, o=%s, oType=%s, idxKeyType=%s",
+            idx.name(), o, o.type().typeName(), 
idx.indexDefinition().typeDescriptor().keyTypeName()
+        );
+
+        InlineIndexRowHandler idxRowHnd = idx.segment(0).rowHandler();
+        IndexKey[] keys = new 
IndexKey[idx.indexDefinition().indexKeyDefinitions().size()];
+
+        int i = 0;
+        for (Map.Entry<String, IndexKeyDefinition> e : 
idx.indexDefinition().indexKeyDefinitions().entrySet()) {
+            String keyName = e.getKey();
+
+            ColumnDescriptor fieldDesc = desc.columnDescriptor(keyName);
+            assert fieldDesc != null : String.format("idx=%s, o=%s, 
keyName=%s", idx.name(), o, keyName);
+
+            Object field = o.field(keyName);
+            Object key = TypeUtils.fromInternal(ectx, field, 
fieldDesc.storageType());
+
+            keys[i++] = wrapIndexKey(key, e.getValue().indexKeyType());
+        }
+
+        return new IndexPlainRowImpl(keys, idxRowHnd);
+    }
+
+    /** */
+    private IndexKey wrapIndexKey(Object key, IndexKeyType keyType) {
+        return IndexKeyFactory.wrap(key, keyType, cctx.cacheObjectContext(), 
idx.indexDefinition().keyTypeSettings());
+    }
+}
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
index 8e828f5720d..4af8a6bda03 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
@@ -33,7 +33,7 @@ import java.util.function.BiPredicate;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Primitives;
 import org.apache.calcite.DataContext;
@@ -46,6 +46,7 @@ import org.apache.calcite.linq4j.tree.MethodDeclaration;
 import org.apache.calcite.linq4j.tree.ParameterExpression;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelFieldCollation;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.type.RelDataType;
@@ -81,6 +82,8 @@ import 
org.apache.ignite.internal.processors.query.calcite.util.IgniteMethod;
 import org.apache.ignite.internal.util.GridBoundedConcurrentLinkedHashMap;
 import org.apache.ignite.internal.util.typedef.F;
 
+import static java.util.stream.Collectors.toList;
+
 /**
  * Implements rex expression into a function object. Uses JaninoRexCompiler 
under the hood.
  * Each expression compiles into a class and a wrapper over it is returned.
@@ -338,6 +341,14 @@ public class ExpressionFactoryImpl<Row> implements 
ExpressionFactory<Row> {
 
         List<RangeConditionImpl> ranges = new ArrayList<>();
 
+        if (collation.getKeys().isEmpty()) {
+            collation = RelCollations.of(IntStream.range(0, 
searchBounds.size())
+                .filter(i -> searchBounds.get(i) != null)
+                .mapToObj(RelFieldCollation::new)
+                .collect(toList())
+            );
+        }
+
         Comparator<Row> rowComparator = comparator(collation);
 
         expandBounds(
@@ -1003,7 +1014,7 @@ public class ExpressionFactoryImpl<Row> implements 
ExpressionFactory<Row> {
             // should not affect ordering.
             if (!sorted) {
                 ranges = ranges.stream().filter(r -> 
!r.skip()).sorted(RangeConditionImpl::compareTo)
-                    .collect(Collectors.toList());
+                    .collect(toList());
 
                 List<RangeConditionImpl> ranges0 = new 
ArrayList<>(ranges.size());
 
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteSqlFunctions.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteSqlFunctions.java
index cfca232e4c4..be9dc99330d 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteSqlFunctions.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteSqlFunctions.java
@@ -27,6 +27,7 @@ import org.apache.calcite.linq4j.Enumerator;
 import org.apache.calcite.linq4j.Linq4j;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.schema.ScannableTable;
 import org.apache.calcite.schema.Schema;
 import org.apache.calcite.schema.Statistic;
@@ -293,4 +294,52 @@ public class IgniteSqlFunctions {
             return true;
         }
     }
+
+    /** SQL >=. */
+    public static boolean geAny(Object a, Object b) {
+        if (Commons.isBinaryComparable(a, b))
+            return Commons.compareBinary(a, b) >= 0;
+
+        return SqlFunctions.geAny(a, b);
+    }
+
+    /** SQL >. */
+    public static boolean gtAny(Object a, Object b) {
+        if (Commons.isBinaryComparable(a, b))
+            return Commons.compareBinary(a, b) > 0;
+
+        return SqlFunctions.gtAny(a, b);
+    }
+
+    /** SQL <=. */
+    public static boolean leAny(Object a, Object b) {
+        if (Commons.isBinaryComparable(a, b))
+            return Commons.compareBinary(a, b) <= 0;
+
+        return SqlFunctions.leAny(a, b);
+    }
+
+    /** SQL <. */
+    public static boolean ltAny(Object a, Object b) {
+        if (Commons.isBinaryComparable(a, b))
+            return Commons.compareBinary(a, b) < 0;
+
+        return SqlFunctions.ltAny(a, b);
+    }
+
+    /** SQL =. */
+    public static boolean eqAny(Object a, Object b) {
+        if (Commons.isBinaryComparable(a, b))
+            return Commons.compareBinary(a, b) == 0;
+
+        return SqlFunctions.eqAny(a, b);
+    }
+
+    /** SQL <>. */
+    public static boolean neAny(Object a, Object b) {
+        if (Commons.isBinaryComparable(a, b))
+            return Commons.compareBinary(a, b) != 0;
+
+        return SqlFunctions.neAny(a, b);
+    }
 }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexImpTable.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexImpTable.java
index f1b5802b702..0772d7287d4 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexImpTable.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/RexImpTable.java
@@ -1298,7 +1298,7 @@ public class RexImpTable {
             // one or both of parameter(s) is(are) ANY type
             final Expression expression0 = maybeBox(expressions.get(0));
             final Expression expression1 = maybeBox(expressions.get(1));
-            return Expressions.call(SqlFunctions.class, 
backupMethodNameForAnyType,
+            return Expressions.call(IgniteSqlFunctions.class, 
backupMethodNameForAnyType,
                 expression0, expression1);
         }
 
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/agg/Accumulators.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/agg/Accumulators.java
index 4dee4cc83b4..9c15e4a7a23 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/agg/Accumulators.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/agg/Accumulators.java
@@ -38,6 +38,7 @@ import org.apache.calcite.sql.type.SqlTypeName;
 import 
org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
 import org.apache.ignite.internal.processors.query.calcite.exec.RowHandler;
 import 
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.util.typedef.F;
 
 import static org.apache.calcite.sql.type.SqlTypeName.ANY;
@@ -234,7 +235,11 @@ public class Accumulators {
                 return () -> new ComparableMinMax<Row, UUID>(call, hnd, true,
                     tf -> 
tf.createTypeWithNullability(tf.createSqlType(SqlTypeName.UUID), true));
             case ANY:
-                throw new UnsupportedOperationException("MIN() is not 
supported for type '" + call.type + "'.");
+                return () -> new ComparableMinMax<>(call, hnd, true,
+                    tf -> tf.createTypeWithNullability(tf.createSqlType(ANY), 
true));
+            case OTHER:
+                return () -> new ComparableMinMax<>(call, hnd, true,
+                    tf -> 
tf.createTypeWithNullability(tf.createSqlType(SqlTypeName.OTHER), true));
             case BIGINT:
             default:
                 return () -> new LongMinMax<>(call, hnd, true);
@@ -263,7 +268,11 @@ public class Accumulators {
                 return () -> new ComparableMinMax<Row, UUID>(call, hnd, false,
                     tf -> 
tf.createTypeWithNullability(tf.createSqlType(SqlTypeName.UUID), true));
             case ANY:
-                throw new UnsupportedOperationException("MAX() is not 
supported for type '" + call.type + "'.");
+                return () -> new ComparableMinMax<>(call, hnd, false,
+                    tf -> tf.createTypeWithNullability(tf.createSqlType(ANY), 
true));
+            case OTHER:
+                return () -> new ComparableMinMax<>(call, hnd, false,
+                    tf -> 
tf.createTypeWithNullability(tf.createSqlType(SqlTypeName.OTHER), true));
             case BIGINT:
             default:
                 return () -> new LongMinMax<>(call, hnd, false);
@@ -1116,7 +1125,7 @@ public class Accumulators {
     }
 
     /** */
-    private static class ComparableMinMax<Row, T extends Comparable<T>> 
extends AbstractAccumulator<Row> {
+    private static class ComparableMinMax<Row, T> extends 
AbstractAccumulator<Row> {
         /** */
         private final boolean min;
 
@@ -1149,8 +1158,8 @@ public class Accumulators {
                 return;
 
             val = empty ? in : min ?
-                (val.compareTo(in) < 0 ? val : in) :
-                (val.compareTo(in) < 0 ? in : val);
+                (compare(val, in) < 0 ? val : in) :
+                (compare(val, in) < 0 ? in : val);
 
             empty = false;
         }
@@ -1163,8 +1172,8 @@ public class Accumulators {
                 return;
 
             val = empty ? other0.val : min ?
-                (val.compareTo(other0.val) < 0 ? val : other0.val) :
-                (val.compareTo(other0.val) < 0 ? other0.val : val);
+                (compare(val, other0.val) < 0 ? val : other0.val) :
+                (compare(val, other0.val) < 0 ? other0.val : val);
 
             empty = false;
         }
@@ -1183,6 +1192,22 @@ public class Accumulators {
         @Override public RelDataType returnType(IgniteTypeFactory typeFactory) 
{
             return typeSupplier.apply(typeFactory);
         }
+
+        /** */
+        @SuppressWarnings({"rawtypes", "unchecked"})
+        private int compare(Object a, Object b) {
+            if (Commons.isBinaryComparable(a, b))
+                return Commons.compareBinary(a, b);
+
+            if (a.getClass() != b.getClass()) {
+                throw new UnsupportedOperationException(String.format(
+                    "%s() is not supported for different value types: 
[type0=%s, type1=%s]",
+                    min ? "MIN" : "MAX", a.getClass().getName(), 
b.getClass().getName()
+                ));
+            }
+
+            return ((Comparable)a).compareTo(b);
+        }
     }
 
     /** */
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
index 3806436eb87..992247f55fa 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheIndexImpl.java
@@ -50,16 +50,16 @@ import org.jetbrains.annotations.Nullable;
  */
 public class CacheIndexImpl implements IgniteIndex {
     /** */
-    private final RelCollation collation;
+    protected final RelCollation collation;
 
     /** */
-    private final String idxName;
+    protected final String idxName;
 
     /** */
-    private final @Nullable Index idx;
+    protected final @Nullable Index idx;
 
     /** */
-    private final IgniteCacheTable tbl;
+    protected final IgniteCacheTable tbl;
 
     /** */
     public CacheIndexImpl(RelCollation collation, String name, @Nullable Index 
idx, IgniteCacheTable tbl) {
@@ -97,10 +97,8 @@ public class CacheIndexImpl implements IgniteIndex {
         @Nullable ImmutableBitSet requiredColumns
     ) {
         UUID locNodeId = execCtx.localNodeId();
-        if (grp.nodeIds().contains(locNodeId) && idx != null) {
-            return new IndexScan<>(execCtx, tbl.descriptor(), 
idx.unwrap(InlineIndex.class), collation.getKeys(),
-                grp.partitions(locNodeId), ranges, requiredColumns);
-        }
+        if (grp.nodeIds().contains(locNodeId) && idx != null)
+            return createIndexScan(execCtx, grp, ranges, requiredColumns);
 
         return Collections.emptyList();
     }
@@ -113,18 +111,8 @@ public class CacheIndexImpl implements IgniteIndex {
         @Nullable ImmutableBitSet requiredColumns
     ) {
         UUID locNodeId = ectx.localNodeId();
-
-        if (grp.nodeIds().contains(locNodeId) && idx != null) {
-            return new IndexFirstLastScan<>(
-                first,
-                ectx,
-                tbl.descriptor(),
-                idx.unwrap(InlineIndexImpl.class),
-                collation.getKeys(),
-                grp.partitions(locNodeId),
-                requiredColumns
-            );
-        }
+        if (grp.nodeIds().contains(locNodeId) && idx != null)
+            return createIndexFirstLastScan(first, ectx, grp, requiredColumns);
 
         return Collections.emptyList();
     }
@@ -147,24 +135,9 @@ public class CacheIndexImpl implements IgniteIndex {
         @Nullable RexNode cond,
         @Nullable ImmutableBitSet requiredColumns
     ) {
-        RelCollation collation = this.collation;
         RelDataType rowType = tbl.getRowType(cluster.getTypeFactory());
 
-        if (requiredColumns != null)
-            collation = collation.apply(Commons.mapping(requiredColumns, 
rowType.getFieldCount()));
-
-        if (!collation.getFieldCollations().isEmpty()) {
-            return RexUtils.buildSortedSearchBounds(
-                cluster,
-                collation,
-                cond,
-                rowType,
-                requiredColumns
-            );
-        }
-
-        // Empty index find predicate.
-        return null;
+        return buildSearchBounds(cluster, cond, rowType, requiredColumns);
     }
 
     /** {@inheritDoc} */
@@ -197,4 +170,77 @@ public class CacheIndexImpl implements IgniteIndex {
 
         return true;
     }
+
+    /** */
+    protected @Nullable List<SearchBounds> buildSearchBounds(
+        RelOptCluster cluster,
+        @Nullable RexNode cond,
+        RelDataType rowType,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        RelCollation collation = mapByRequireColumns(this.collation, rowType, 
requiredColumns);
+
+        if (collation.getFieldCollations().isEmpty())
+            return null; // Empty index find predicate.
+
+        return RexUtils.buildSortedSearchBounds(cluster, collation, cond, 
rowType, requiredColumns);
+    }
+
+    /** */
+    protected <Row> IndexScan<Row> createIndexScan(
+        ExecutionContext<Row> ectx,
+        ColocationGroup grp,
+        RangeIterable<Row> ranges,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        return new IndexScan<>(
+            ectx,
+            tbl.descriptor(),
+            idx.unwrap(InlineIndex.class),
+            collation.getKeys(),
+            grp.partitions(ectx.localNodeId()),
+            ranges,
+            requiredColumns
+        );
+    }
+
+    /** */
+    protected <Row> IndexScan<Row> createIndexFirstLastScan(
+        boolean first,
+        ExecutionContext<Row> ectx,
+        ColocationGroup grp,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        return new IndexFirstLastScan<>(
+            first,
+            ectx,
+            tbl.descriptor(),
+            idx.unwrap(InlineIndexImpl.class),
+            collation.getKeys(),
+            grp.partitions(ectx.localNodeId()),
+            requiredColumns
+        );
+    }
+
+    /** */
+    protected CacheIndexImpl copy(IgniteCacheTable newTbl) {
+        return new CacheIndexImpl(collation, idxName, idx, newTbl);
+    }
+
+    /** */
+    protected CacheIndexImpl copy(IgniteCacheTable newTbl, RelCollation 
newCollation) {
+        return new CacheIndexImpl(newCollation, idxName, idx, newTbl);
+    }
+
+    /** */
+    static RelCollation mapByRequireColumns(
+        RelCollation collation,
+        RelDataType rowType,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        if (requiredColumns != null)
+            collation = collation.apply(Commons.mapping(requiredColumns, 
rowType.getFieldCount()));
+
+        return collation;
+    }
 }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheWrappedKeyIndexImpl.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheWrappedKeyIndexImpl.java
new file mode 100644
index 00000000000..60c87ef5bbe
--- /dev/null
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheWrappedKeyIndexImpl.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.calcite.schema;
+
+import java.util.List;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.internal.cache.query.index.Index;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.InlineIndex;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import 
org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
+import org.apache.ignite.internal.processors.query.calcite.exec.IndexScan;
+import 
org.apache.ignite.internal.processors.query.calcite.exec.IndexWrappedKeyScan;
+import 
org.apache.ignite.internal.processors.query.calcite.exec.exp.RangeIterable;
+import 
org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup;
+import 
org.apache.ignite.internal.processors.query.calcite.prepare.bounds.SearchBounds;
+import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
+import org.jetbrains.annotations.Nullable;
+
+/** Extension for column {@value QueryUtils#KEY_FIELD_NAME} in case of 
composite primary key. */
+class CacheWrappedKeyIndexImpl extends CacheIndexImpl {
+    /** */
+    CacheWrappedKeyIndexImpl(RelCollation collation, String idxName, Index 
idx, IgniteCacheTable tbl) {
+        super(collation, idxName, idx, tbl);
+    }
+
+    /** */
+    @Override protected @Nullable List<SearchBounds> buildSearchBounds(
+        RelOptCluster cluster,
+        @Nullable RexNode cond,
+        RelDataType rowType,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        if (cond != null) {
+            List<SearchBounds> bounds = 
RexUtils.buildHashSearchBounds(cluster, cond, rowType, requiredColumns, true);
+
+            if (bounds != null && bounds.get(QueryUtils.KEY_COL) != null)
+                return bounds;
+        }
+
+        return null; // Empty index find predicate.
+    }
+
+    /** */
+    @Override protected <Row> IndexScan<Row> createIndexScan(
+        ExecutionContext<Row> ectx,
+        ColocationGroup grp,
+        RangeIterable<Row> ranges,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        return new IndexWrappedKeyScan<>(
+            ectx,
+            tbl.descriptor(),
+            idx.unwrap(InlineIndex.class),
+            collation.getKeys(),
+            grp.partitions(ectx.localNodeId()),
+            ranges,
+            requiredColumns
+        );
+    }
+
+    /** */
+    @Override protected <Row> IndexScan<Row> createIndexFirstLastScan(
+        boolean first,
+        ExecutionContext<Row> ectx,
+        ColocationGroup grp,
+        @Nullable ImmutableBitSet requiredColumns
+    ) {
+        throw new IgniteException(String.format("Should not be created for 
wrappped %s index", QueryUtils.KEY_FIELD_NAME));
+    }
+
+    /** */
+    @Override protected CacheIndexImpl copy(IgniteCacheTable newTbl) {
+        return new CacheWrappedKeyIndexImpl(collation, idxName, idx, newTbl);
+    }
+
+    /** */
+    @Override protected CacheIndexImpl copy(IgniteCacheTable newTbl, 
RelCollation newCollation) {
+        return new CacheWrappedKeyIndexImpl(collation, idxName, idx, newTbl);
+    }
+}
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java
index b9eae82520f..0c5b793a830 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java
@@ -55,6 +55,7 @@ import 
org.apache.ignite.internal.processors.query.calcite.util.AbstractService;
 import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.schema.SchemaChangeListener;
 import 
org.apache.ignite.internal.processors.query.schema.management.IndexDescriptor;
+import 
org.apache.ignite.internal.processors.query.schema.management.SchemaManager;
 import 
org.apache.ignite.internal.processors.subscription.GridInternalSubscriptionProcessor;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.lang.IgnitePredicate;
@@ -203,16 +204,13 @@ public class SchemaHolderImpl extends AbstractService 
implements SchemaHolder, S
         List<QueryField> cols
     ) {
         IgniteCacheTable oldTbl = table(schemaName, typeDesc.tableName());
-        assert oldTbl != null;
+        assert oldTbl != null : String.format("schemaName=%s, tableName=%s", 
schemaName, typeDesc.tableName());
 
         IgniteCacheTable newTbl = createTable(typeDesc, cacheInfo);
 
         // Recreate indexes for the new table without columns shift.
-        for (IgniteIndex idx : oldTbl.indexes().values()) {
-            CacheIndexImpl idx0 = (CacheIndexImpl)idx;
-
-            newTbl.addIndex(new CacheIndexImpl(idx0.collation(), idx0.name(), 
idx0.queryIndex(), newTbl));
-        }
+        for (IgniteIndex idx : oldTbl.indexes().values())
+            newTbl.addIndex(((CacheIndexImpl)idx).copy(newTbl));
 
         publishTable(schemaName, typeDesc.tableName(), newTbl);
     }
@@ -225,7 +223,7 @@ public class SchemaHolderImpl extends AbstractService 
implements SchemaHolder, S
         List<String> cols
     ) {
         IgniteCacheTable oldTbl = table(schemaName, typeDesc.tableName());
-        assert oldTbl != null;
+        assert oldTbl != null : String.format("schemaName=%s, tableName=%s", 
schemaName, typeDesc.tableName());
 
         IgniteCacheTable newTbl = createTable(typeDesc, cacheInfo);
 
@@ -242,8 +240,9 @@ public class SchemaHolderImpl extends AbstractService 
implements SchemaHolder, S
         for (IgniteIndex idx : oldTbl.indexes().values()) {
             CacheIndexImpl idx0 = (CacheIndexImpl)idx;
 
-            newTbl.addIndex(new 
CacheIndexImpl(RelCollations.permute(idx0.collation(), mapping), idx0.name(),
-                idx0.queryIndex(), newTbl));
+            RelCollation newCollation = 
RelCollations.permute(idx0.collation(), mapping);
+
+            newTbl.addIndex(idx0.copy(newTbl, newCollation));
         }
 
         publishTable(schemaName, typeDesc.tableName(), newTbl);
@@ -301,12 +300,20 @@ public class SchemaHolderImpl extends AbstractService 
implements SchemaHolder, S
         IndexDescriptor idxDesc
     ) {
         IgniteCacheTable tbl = table(schemaName, tblName);
-        assert tbl != null;
+        assert tbl != null : String.format("schemaName=%s, tableName=%s, 
idxName=%s", schemaName, tblName, idxName);
 
         RelCollation idxCollation = deriveSecondaryIndexCollation(idxDesc, 
tbl);
 
         IgniteIndex idx = new CacheIndexImpl(idxCollation, idxName, 
idxDesc.index(), tbl);
         tbl.addIndex(idx);
+
+        // For a composite PK index, we need to create another proxy index 
that will expand the passed boundaries into
+        // index keys for BinaryObject/Key classes. That is, _key -> 
idx_field_0, idx_field_1, etc.
+        if (idxDesc.isPk() && idxDesc.isComposite()) {
+            tbl.addIndex(new CacheWrappedKeyIndexImpl(
+                RelCollations.EMPTY, 
SchemaManager.generateProxyIdxName(idxName), idxDesc.index(), tbl
+            ));
+        }
     }
 
     /**
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/Commons.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/Commons.java
index c0fc0697716..fc91eb44832 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/Commons.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/Commons.java
@@ -57,8 +57,10 @@ import org.apache.calcite.util.mapping.MappingType;
 import org.apache.calcite.util.mapping.Mappings;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.binary.BinaryObject;
 import org.apache.ignite.internal.GridComponent;
 import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
 import org.apache.ignite.internal.processors.cache.GridCacheSharedContext;
 import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
@@ -74,6 +76,7 @@ import 
org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
 import 
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.A;
+import org.apache.ignite.internal.util.typedef.internal.SB;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.codehaus.commons.compiler.CompilerFactoryFactory;
 import org.codehaus.commons.compiler.IClassBodyEvaluator;
@@ -539,4 +542,31 @@ public final class Commons {
     public static @Nullable GridCacheVersion queryTransactionVersion(Context 
ctx) {
         return ctx.unwrap(GridCacheVersion.class);
     }
+
+    /** Returns a string representation of the contents of the specified row. 
*/
+    public static <Row> String toString(RowHandler<Row> hnd, @Nullable Row r) {
+        if (r == null)
+            return "null";
+
+        SB sb = new SB('[');
+
+        for (int i = 0; i < hnd.columnCount(r); i++) {
+            if (i != 0)
+                sb.a(", ");
+
+            sb.a(hnd.get(i, r));
+        }
+
+        return sb.a(']').toString();
+    }
+
+    /** Checks if objects can be compared as {@link BinaryObject} via {@link 
#compareBinary(Object, Object)}. */
+    public static boolean isBinaryComparable(Object o1, Object o2) {
+        return BinaryUtils.isBinaryObjectImpl(o1) || 
BinaryUtils.isBinaryObjectImpl(o2);
+    }
+
+    /** Compares {@link BinaryObject} for DML operation. Before use, call 
{@link #isBinaryComparable(Object, Object)}. */
+    public static int compareBinary(Object o1, Object o2) {
+        return BinaryUtils.binariesFactory.compareForDml(o1, o2);
+    }
 }
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
index 8e0a973731a..e3f9d5e7cd9 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
@@ -168,10 +168,10 @@ public class DataTypesTest extends 
AbstractBasicIntegrationTransactionalTest {
             .check();
 
         assertThrows("SELECT MIN(oth) FROM t", 
UnsupportedOperationException.class,
-            "MIN() is not supported for type 'OTHER'.");
+            "MIN() is not supported for different value types");
 
         assertThrows("SELECT MAX(oth) FROM t", 
UnsupportedOperationException.class,
-            "MAX() is not supported for type 'OTHER'.");
+            "MAX() is not supported for different value types");
 
         assertThrows("SELECT AVG(oth) from t", 
UnsupportedOperationException.class,
             "AVG() is not supported for type 'OTHER'.");
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/SelectByKeyFieldTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/SelectByKeyFieldTest.java
new file mode 100644
index 00000000000..9a3c56c98fe
--- /dev/null
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/SelectByKeyFieldTest.java
@@ -0,0 +1,429 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.calcite.integration;
+
+import java.util.List;
+import java.util.Objects;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.internal.binary.BinaryObjectImpl;
+import org.apache.ignite.internal.binary.BinaryUtils;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
+import org.apache.ignite.internal.util.tostring.GridToStringInclude;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.hamcrest.CoreMatchers;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static java.util.stream.Collectors.toList;
+import static 
org.apache.ignite.internal.processors.query.QueryUtils.KEY_FIELD_NAME;
+import static 
org.apache.ignite.internal.processors.query.QueryUtils.PRIMARY_KEY_INDEX;
+
+/**
+ * Checks that using {@link QueryUtils#KEY_FIELD_NAME} in condition will use
+ * {@link QueryUtils#PRIMARY_KEY_INDEX pk index}.
+ */
+public class SelectByKeyFieldTest extends AbstractBasicIntegrationTest {
+    /** {@inheritDoc} */
+    @Override protected int nodeCount() {
+        return 1;
+    }
+
+    /** */
+    @Test
+    public void testSimplePk() {
+        checkSimplePk(null);
+    }
+
+    /** */
+    @Test
+    public void testSimplePkAfterAddColumn() {
+        checkSimplePk(this::executeAlterTableAddColumn);
+    }
+
+    /** */
+    @Test
+    public void testSimplePkAfterDropColumn() {
+        checkSimplePk(this::executeAlterTableDropColumn);
+    }
+
+    /** */
+    @Test
+    public void testCompositePk() {
+        checkCompositePk(false, true, null);
+    }
+
+    /** */
+    @Test
+    public void testCompositePkSearchByPartOfKey() {
+        sql("create table PUBLIC.PERSON(id int, name varchar, surname varchar, 
age int, primary key(id, name))");
+
+        for (int i = 0; i < 10; i++) {
+            sql(
+                "insert into PUBLIC.PERSON(id, name, surname, age) values (?, 
?, ?, ?)",
+                i, "foo" + i, "bar" + i, 18 + i
+            );
+        }
+
+        List<List<?>> sqlRs = sql("select _key, id, name from PUBLIC.PERSON 
order by id");
+        BinaryObject _key = (BinaryObject)sqlRs.get(6).get(0);
+        int id = (Integer)sqlRs.get(6).get(1);
+        String name = (String)sqlRs.get(6).get(2);
+
+        assertQuery("select id, name, age, _key from PUBLIC.PERSON where id = 
?")
+            .withParams(id)
+            .matches(QueryChecker.containsIndexScan("PUBLIC", "PERSON", 
PRIMARY_KEY_INDEX))
+            .columnNames("ID", "NAME", "AGE", KEY_FIELD_NAME)
+            .returns(id, name, 24, _key)
+            .check();
+
+        assertQuery("select id, name, age, _key from PUBLIC.PERSON where name 
= ?")
+            .withParams(name)
+            .matches(QueryChecker.containsTableScan("PUBLIC", "PERSON"))
+            .columnNames("ID", "NAME", "AGE", KEY_FIELD_NAME)
+            .returns(id, name, 24, _key)
+            .check();
+
+        assertQuery("select name, age from PUBLIC.PERSON where name = ?")
+            .withParams(name)
+            .matches(CoreMatchers.not(QueryChecker.containsIndexScan("PUBLIC", 
"PERSON", PRIMARY_KEY_INDEX)))
+            .columnNames("NAME", "AGE")
+            .returns(name, 24)
+            .check();
+    }
+
+    /** */
+    @Test
+    public void testCompositePkAfterAddColumn() {
+        checkCompositePk(false, true, this::executeAlterTableAddColumn);
+    }
+
+    /** */
+    @Test
+    public void testCompositePkAfterDropColumn() {
+        checkCompositePk(false, true, this::executeAlterTableDropColumn);
+    }
+
+    /** */
+    @Test
+    public void testCompositePkWithKeyTypeAndBinaryObject() {
+        checkCompositePk(true, true, null);
+    }
+
+    /** */
+    @Test
+    @Ignore("https://issues.apache.org/jira/browse/IGNITE-28374";)
+    public void testCompositePkWithKeyTypeAndPersonCompositeKey() {
+        checkCompositePk(true, false, null);
+    }
+
+    /** */
+    @Test
+    public void testCompositePkWithOrderByKey() {
+        sql("create table PUBLIC.PERSON(id int, name varchar, surname varchar, 
age int, primary key(id, name))");
+
+        for (int i = 0; i < 10; i++) {
+            sql(
+                "insert into PUBLIC.PERSON(id, name, surname, age) values (?, 
?, ?, ?)",
+                i, "foo" + i, "bar" + i, 18 + i
+            );
+        }
+
+        List<List<?>> sqlRs = sql("select id, name, age, _key from 
PUBLIC.PERSON");
+        sqlRs.sort((o1, o2) -> binaryObjectCmpForDml(o1.get(3), o2.get(3)));
+
+        QueryChecker qryChecker = assertQuery("select id, name, age, _key from 
PUBLIC.PERSON order by _key")
+            .matches(QueryChecker.containsTableScan("PUBLIC", "PERSON"))
+            .columnNames("ID", "NAME", "AGE", KEY_FIELD_NAME);
+
+        sqlRs.forEach(objects -> 
qryChecker.returns(objects.toArray(Object[]::new)));
+
+        qryChecker.check();
+    }
+
+    /** */
+    @Test
+    public void testCompositePkWithDifferentCmpOperations() {
+        checkCompositePkWithDifferentCmpOperations(true);
+    }
+
+    /** */
+    @Test
+    @Ignore("https://issues.apache.org/jira/browse/IGNITE-28374";)
+    public void 
testCompositePkWithPersonCompositeKeyAndDifferentCmpOperations() {
+        checkCompositePkWithDifferentCmpOperations(false);
+    }
+
+    /** */
+    @Test
+    public void testCompositePkWithMinMaxByKey() {
+        sql("create table PUBLIC.PERSON(id int, name varchar, surname varchar, 
age int, primary key(id, name))");
+
+        for (int i = 0; i < 10; i++) {
+            sql(
+                "insert into PUBLIC.PERSON(id, name, surname, age) values (?, 
?, ?, ?)",
+                i, "foo" + i, "bar" + i, 18 + i
+            );
+        }
+
+        List<List<?>> sqlRs = sql("select _key from PUBLIC.PERSON order by 
id");
+        List<?> min = sqlRs.stream().min((o1, o2) -> 
binaryObjectCmpForDml(o1.get(0), o2.get(0))).orElseThrow();
+        List<?> max = sqlRs.stream().max((o1, o2) -> 
binaryObjectCmpForDml(o1.get(0), o2.get(0))).orElseThrow();
+
+        assertQuery("select min(_key) from PUBLIC.PERSON")
+            .matches(QueryChecker.containsTableScan("PUBLIC", "PERSON"))
+            .columnNames("MIN(_KEY)")
+            .returns(min.get(0))
+            .check();
+
+        assertQuery("select max(_key) from PUBLIC.PERSON")
+            .matches(QueryChecker.containsTableScan("PUBLIC", "PERSON"))
+            .columnNames("MAX(_KEY)")
+            .returns(max.get(0))
+            .check();
+    }
+
+    /** */
+    private void checkSimplePk(@Nullable Runnable executeBeforeChecks) {
+        sql("create table PUBLIC.PERSON(id int primary key, name varchar, 
surname varchar, age int)");
+
+        for (int i = 0; i < 10; i++) {
+            sql(
+                "insert into PUBLIC.PERSON(id, name, surname, age) values (?, 
?, ?, ?)",
+                i, "foo" + i, "bar" + i, 18 + i
+            );
+        }
+
+        List<List<?>> sqlRs = sql("select _key, id from PUBLIC.PERSON order by 
id");
+        int _key = (Integer)sqlRs.get(7).get(0);
+        int id = (Integer)sqlRs.get(7).get(1);
+
+        assertEquals(7, _key);
+        assertEquals(7, id);
+
+        if (executeBeforeChecks != null)
+            executeBeforeChecks.run();
+
+        assertQuery("select id, name, age, _key from PUBLIC.PERSON where _key 
= ?")
+            .withParams(_key)
+            .matches(QueryChecker.containsIndexScan("PUBLIC", "PERSON", 
PRIMARY_KEY_INDEX))
+            .columnNames("ID", "NAME", "AGE", KEY_FIELD_NAME)
+            .returns(id, "foo7", 25, _key)
+            .check();
+
+        // Let's check with a smaller number of columns.
+        assertQuery("select id, age, _key from PUBLIC.PERSON where _key = ?")
+            .withParams(_key)
+            .matches(QueryChecker.containsIndexScan("PUBLIC", "PERSON", 
PRIMARY_KEY_INDEX))
+            .columnNames("ID", "AGE", KEY_FIELD_NAME)
+            .returns(id, 25, _key)
+            .check();
+
+        // Let's just make sure that PK search is not broken.
+        assertQuery("select id, name, age, _key from PUBLIC.PERSON where id = 
?")
+            .withParams(id)
+            .matches(QueryChecker.containsIndexScan("PUBLIC", "PERSON", 
PRIMARY_KEY_INDEX))
+            .columnNames("ID", "NAME", "AGE", KEY_FIELD_NAME)
+            .returns(id, "foo7", 25, _key)
+            .check();
+    }
+
+    /** */
+    private void checkCompositePk(
+        boolean setKeyTypeToCreateTblDdl,
+        boolean useBinaryObject,
+        @Nullable Runnable executeBeforeChecks
+    ) {
+        if (setKeyTypeToCreateTblDdl) {
+            // Order of the primary key columns has been deliberately changed.
+            sql(String.format(
+                "create table PUBLIC.PERSON(id int, name varchar, surname 
varchar, age int, primary key(name, id)) " +
+                    "with \"key_type=%s\"",
+                PersonCompositeKey.class.getName()
+            ));
+        }
+        else
+            sql("create table PUBLIC.PERSON(id int, name varchar, surname 
varchar, age int, primary key(id, name))");
+
+        for (int i = 0; i < 10; i++) {
+            sql(
+                "insert into PUBLIC.PERSON(id, name, surname, age) values (?, 
?, ?, ?)",
+                i, "foo" + i, "bar" + i, 18 + i
+            );
+        }
+
+        List<List<?>> sqlRs = sql("select _key, id, name from PUBLIC.PERSON 
order by id");
+        BinaryObject _key = (BinaryObject)sqlRs.get(6).get(0);
+        int id = (Integer)sqlRs.get(6).get(1);
+        String name = (String)sqlRs.get(6).get(2);
+
+        assertEquals(6, id);
+        assertEquals("foo6", name);
+
+        if (executeBeforeChecks != null)
+            executeBeforeChecks.run();
+
+        assertQuery("select id, name, age, _key from PUBLIC.PERSON where _key 
= ?")
+            .withParams(useBinaryObject ? _key : _key.deserialize())
+            .matches(QueryChecker.containsIndexScan("PUBLIC", "PERSON", 
PRIMARY_KEY_INDEX + "_proxy"))
+            .columnNames("ID", "NAME", "AGE", KEY_FIELD_NAME)
+            .returns(id, name, 24, _key)
+            .check();
+
+        // Let's check with a smaller number of columns.
+        assertQuery("select id, age, _key from PUBLIC.PERSON where _key = ?")
+            .withParams(useBinaryObject ? _key : _key.deserialize())
+            .matches(QueryChecker.containsIndexScan("PUBLIC", "PERSON", 
PRIMARY_KEY_INDEX + "_proxy"))
+            .columnNames("ID", "AGE", KEY_FIELD_NAME)
+            .returns(id, 24, _key)
+            .check();
+
+        // Let's just make sure that PK search is not broken.
+        assertQuery("select id, name, age, _key from PUBLIC.PERSON where id = 
? and name = ?")
+            .withParams(id, name)
+            .matches(QueryChecker.containsIndexScan("PUBLIC", "PERSON", 
PRIMARY_KEY_INDEX))
+            .columnNames("ID", "NAME", "AGE", KEY_FIELD_NAME)
+            .returns(id, name, 24, _key)
+            .check();
+    }
+
+    /** */
+    private void checkCompositePkWithDifferentCmpOperations(boolean 
useBinaryObject) {
+        sql("create table PUBLIC.PERSON(id int, name varchar, surname varchar, 
age int, primary key(id, name))");
+
+        for (int i = 0; i < 10; i++) {
+            sql(
+                "insert into PUBLIC.PERSON(id, name, surname, age) values (?, 
?, ?, ?)",
+                i, "foo" + i, "bar" + i, 18 + i
+            );
+        }
+
+        List<List<?>> sqlRs = sql("select id, name, age, _key from 
PUBLIC.PERSON order by id");
+        BinaryObjectImpl _key8 = (BinaryObjectImpl)sqlRs.get(8).get(3);
+
+        for (CmpOp cmpOp : CmpOp.values()) {
+            if (cmpOp == CmpOp.EQ)
+                continue;
+
+            List<List<?>> expRows = sqlRs.stream()
+                .filter(objects -> 
cmpOp.expRowByKeyPred.test((BinaryObjectImpl)objects.get(3), _key8))
+                .collect(toList());
+
+            QueryChecker qryChecker = assertQuery(String.format(
+                "select id, name, age, _key from PUBLIC.PERSON where _key %s 
?", cmpOp.sql
+            ))
+                .withParams(useBinaryObject ? _key8 : _key8.deserialize())
+                .matches(QueryChecker.containsTableScan("PUBLIC", "PERSON"))
+                .columnNames("ID", "NAME", "AGE", KEY_FIELD_NAME);
+
+            expRows.forEach(objects -> 
qryChecker.returns(objects.toArray(Object[]::new)));
+
+            qryChecker.check();
+        }
+    }
+
+    /** */
+    public static class PersonCompositeKey {
+        /** */
+        @GridToStringInclude
+        public int id;
+
+        /** */
+        @GridToStringInclude
+        public String name;
+
+        /** {@inheritDoc} */
+        @Override public int hashCode() {
+            return Objects.hash(id, name);
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean equals(Object obj) {
+            if (obj == this)
+                return true;
+
+            if (!(obj instanceof PersonCompositeKey))
+                return false;
+
+            PersonCompositeKey that = (PersonCompositeKey)obj;
+
+            return id == that.id && name.equals(that.name);
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(PersonCompositeKey.class, this);
+        }
+    }
+
+    /** */
+    @FunctionalInterface
+    private interface ExpRowByKeyPredicate {
+        /** */
+        boolean test(BinaryObjectImpl rowKey, BinaryObjectImpl targetKey);
+    }
+
+    /** */
+    private enum CmpOp {
+        /** */
+        GE(">=", (rowKey, targetKey) -> binaryObjectCmpForDml(rowKey, 
targetKey) >= 0),
+
+        /** */
+        GT(">", (rowKey, targetKey) -> binaryObjectCmpForDml(rowKey, 
targetKey) > 0),
+
+        /** */
+        LE("<=", (rowKey, targetKey) -> binaryObjectCmpForDml(rowKey, 
targetKey) <= 0),
+
+        /** */
+        LT("<", (rowKey, targetKey) -> binaryObjectCmpForDml(rowKey, 
targetKey) < 0),
+
+        /** */
+        EQ("=", (rowKey, targetKey) -> binaryObjectCmpForDml(rowKey, 
targetKey) == 0),
+
+        /** */
+        NE("<>", (rowKey, targetKey) -> binaryObjectCmpForDml(rowKey, 
targetKey) != 0);
+
+        /** */
+        private final String sql;
+
+        /** */
+        private final ExpRowByKeyPredicate expRowByKeyPred;
+
+        /** */
+        CmpOp(String sql, ExpRowByKeyPredicate expRowByKeyPred) {
+            this.sql = sql;
+            this.expRowByKeyPred = expRowByKeyPred;
+        }
+    }
+
+    /** */
+    private void executeAlterTableAddColumn() {
+        sql("alter table PUBLIC.PERSON add email varchar");
+    }
+
+    /** */
+    private void executeAlterTableDropColumn() {
+        sql("alter table PUBLIC.PERSON drop column surname");
+    }
+
+    /** */
+    private static int binaryObjectCmpForDml(Object o1, Object o2) {
+        return BinaryUtils.binariesFactory.compareForDml(o1, o2);
+    }
+}
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
 
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
index ccba0615464..6558537af07 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
@@ -67,6 +67,7 @@ import 
org.apache.ignite.internal.processors.query.calcite.integration.QueryMeta
 import 
org.apache.ignite.internal.processors.query.calcite.integration.QueryWithPartitionsIntegrationTest;
 import 
org.apache.ignite.internal.processors.query.calcite.integration.RunningQueriesIntegrationTest;
 import 
org.apache.ignite.internal.processors.query.calcite.integration.ScalarInIntegrationTest;
+import 
org.apache.ignite.internal.processors.query.calcite.integration.SelectByKeyFieldTest;
 import 
org.apache.ignite.internal.processors.query.calcite.integration.ServerStatisticsIntegrationTest;
 import 
org.apache.ignite.internal.processors.query.calcite.integration.SetOpIntegrationTest;
 import 
org.apache.ignite.internal.processors.query.calcite.integration.SortAggregateIntegrationTest;
@@ -177,6 +178,8 @@ import org.junit.runners.Suite;
     MultiDcQueryMappingTest.class,
     TxWithExceptionalInterceptorTest.class,
     CacheWithInterceptorIntegrationTest.class,
+    TxWithExceptionalInterceptorTest.class,
+    SelectByKeyFieldTest.class,
 })
 public class IntegrationTestSuite {
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/management/IndexDescriptor.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/management/IndexDescriptor.java
index 8174b5c4ddc..01fc5a383dc 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/management/IndexDescriptor.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/management/IndexDescriptor.java
@@ -141,4 +141,9 @@ public class IndexDescriptor {
     public TableDescriptor table() {
         return tbl;
     }
+
+    /** */
+    public boolean isComposite() {
+        return keyDefs.size() > 1;
+    }
 }

Reply via email to