You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ko...@apache.org on 2025/03/06 11:16:47 UTC

(ignite-3) branch main updated: IGNITE-24707 Sql. Fix type coercion for quantify operators (#5350)

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

korlov 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 92f9847124c IGNITE-24707 Sql. Fix type coercion for quantify operators (#5350)
92f9847124c is described below

commit 92f9847124c301b35894b905a4e99ec2f50d2fd2
Author: korlov42 <ko...@apache.org>
AuthorDate: Thu Mar 6 13:16:42 2025 +0200

    IGNITE-24707 Sql. Fix type coercion for quantify operators (#5350)
---
 .../sql/engine/prepare/IgniteTypeCoercion.java     | 111 ++-
 .../planner/datatypes/BaseTypeCoercionTest.java    |   4 +-
 .../NumericQuantifyOperatorTypeCoercionTest.java   | 777 +++++++++++++++++++++
 3 files changed, 888 insertions(+), 4 deletions(-)

diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java
index 1581d1b7dbc..f98b84d1dc8 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java
@@ -24,9 +24,11 @@ import static org.apache.calcite.util.Static.RESOURCE;
 import static org.apache.ignite.internal.sql.engine.util.TypeUtils.typeFamiliesAreCompatible;
 
 import java.nio.charset.Charset;
+import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 import org.apache.calcite.adapter.java.JavaTypeFactory;
 import org.apache.calcite.rel.type.DynamicRecordType;
 import org.apache.calcite.rel.type.RelDataType;
@@ -187,7 +189,7 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
     @Override
     public boolean inOperationCoercion(SqlCallBinding binding) {
         SqlOperator operator = binding.getOperator();
-        if (operator.getKind() != SqlKind.IN && operator.getKind() != SqlKind.NOT_IN) {
+        if (!operatorIsInOrQuantify(operator)) {
             return false;
         }
 
@@ -203,14 +205,119 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
                 typeFactory, type2, null
         );
 
-        //noinspection SimplifiableIfStatement
         if (!typeFamiliesAreCompatible(typeFactory, leftRowType, rightRowType)) {
             return false;
         }
 
+        if (operatorIsQuantify(operator)) {
+            return quantifyOperationCoercion(binding);
+        }
+
         return super.inOperationCoercion(binding);
     }
 
+    private static boolean operatorIsIn(SqlOperator operator) {
+        return operator.getKind().belongsTo(Set.of(SqlKind.IN, SqlKind.NOT_IN));
+    }
+
+    private static boolean operatorIsQuantify(SqlOperator operator) {
+        return operator.getKind().belongsTo(Set.of(SqlKind.SOME, SqlKind.ALL));
+    }
+
+    private static boolean operatorIsInOrQuantify(SqlOperator operator) {
+        return operatorIsIn(operator) || operatorIsQuantify(operator);
+    }
+
+    private boolean quantifyOperationCoercion(SqlCallBinding binding) {
+        // This method is a copy-paste of org.apache.calcite.sql.validate.implicit.TypeCoercionImpl.inOperationCoercion
+        // with stripped validation of operation kind.
+        assert operatorIsQuantify(binding.getOperator());
+        assert binding.getOperandCount() == 2;
+
+        RelDataType type1 = binding.getOperandType(0);
+        RelDataType type2 = binding.getOperandType(1);
+        SqlNode node1 = binding.operand(0);
+        SqlNode node2 = binding.operand(1);
+        SqlValidatorScope scope = binding.getScope();
+
+        if (type1.isStruct() && type2.isStruct() && type1.getFieldCount() != type2.getFieldCount()) {
+            return false;
+        }
+
+        int colCount = type1.isStruct() ? type1.getFieldCount() : 1;
+        RelDataType[] argTypes = new RelDataType[2];
+        argTypes[0] = type1;
+        argTypes[1] = type2;
+        boolean coerced = false;
+        List<RelDataType> widenTypes = new ArrayList<>();
+        for (int i = 0; i < colCount; i++) {
+            int i2 = i;
+            List<RelDataType> columnIthTypes = new AbstractList<>() {
+                @Override
+                public RelDataType get(int index) {
+                    return argTypes[index].isStruct() ? argTypes[index].getFieldList().get(i2).getType() : argTypes[index];
+                }
+
+                @Override
+                public int size() {
+                    return argTypes.length;
+                }
+            };
+
+            RelDataType widenType = commonTypeForBinaryComparison(columnIthTypes.get(0), columnIthTypes.get(1));
+            if (widenType == null) {
+                widenType = getTightestCommonType(columnIthTypes.get(0), columnIthTypes.get(1));
+            }
+            if (widenType == null) {
+                // Can not find any common type, just return early.
+                return false;
+            }
+            widenTypes.add(widenType);
+        }
+        // Find all the common type for RSH and LSH columns.
+        assert widenTypes.size() == colCount;
+        for (int i = 0; i < widenTypes.size(); i++) {
+            RelDataType desired = widenTypes.get(i);
+            // LSH maybe a row values or single node.
+            if (node1.getKind() == SqlKind.ROW) {
+                assert node1 instanceof SqlCall;
+                if (coerceOperandType(scope, (SqlCall) node1, i, desired)) {
+                    updateInferredColumnType(requireNonNull(scope, "scope"), node1, i, widenTypes.get(i));
+                    coerced = true;
+                }
+            } else {
+                coerced = coerceOperandType(scope, binding.getCall(), 0, desired) || coerced;
+            }
+            // RHS may be a row values expression or sub-query.
+            if (node2 instanceof SqlNodeList) {
+                final SqlNodeList node3 = (SqlNodeList) node2;
+                boolean listCoerced = false;
+                if (type2.isStruct()) {
+                    for (SqlNode node : (SqlNodeList) node2) {
+                        assert node instanceof SqlCall;
+                        listCoerced = coerceOperandType(scope, (SqlCall) node, i, desired) || listCoerced;
+                    }
+                    if (listCoerced) {
+                        updateInferredColumnType(requireNonNull(scope, "scope"), node2, i, desired);
+                    }
+                } else {
+                    for (int j = 0; j < ((SqlNodeList) node2).size(); j++) {
+                        listCoerced = coerceColumnType(scope, node3, j, desired) || listCoerced;
+                    }
+                    if (listCoerced) {
+                        updateInferredType(node2, desired);
+                    }
+                }
+                coerced = coerced || listCoerced;
+            } else {
+                // Another sub-query.
+                SqlValidatorScope scope1 = node2 instanceof SqlSelect ? validator.getSelectScope((SqlSelect) node2) : scope;
+                coerced = rowTypeCoercion(scope1, node2, i, desired) || coerced;
+            }
+        }
+        return coerced;
+    }
+
     /** {@inheritDoc} */
     @Override
     public boolean caseWhenCoercion(SqlCallBinding callBinding) {
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/datatypes/BaseTypeCoercionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/datatypes/BaseTypeCoercionTest.java
index fac10b9f0bb..a801a26abd1 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/datatypes/BaseTypeCoercionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/datatypes/BaseTypeCoercionTest.java
@@ -257,7 +257,7 @@ public class BaseTypeCoercionTest extends AbstractPlannerTest {
         };
     }
 
-    private static Matcher<RexCall> ofType(NativeType type) {
+    static Matcher<RexCall> ofType(NativeType type) {
         return new BaseMatcher<>() {
             RelDataType expectedType;
             RelDataType relRowType;
@@ -266,7 +266,7 @@ public class BaseTypeCoercionTest extends AbstractPlannerTest {
             public boolean matches(Object item) {
                 assert item != null;
 
-                RexCall rexCall = (RexCall) item;
+                RexNode rexCall = (RexNode) item;
 
                 relRowType = rexCall.getType();
                 expectedType = native2relationalType(Commons.typeFactory(), type);
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/datatypes/NumericQuantifyOperatorTypeCoercionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/datatypes/NumericQuantifyOperatorTypeCoercionTest.java
new file mode 100644
index 00000000000..821593f929a
--- /dev/null
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/datatypes/NumericQuantifyOperatorTypeCoercionTest.java
@@ -0,0 +1,777 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.planner.datatypes;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.util.Util;
+import org.apache.ignite.internal.sql.engine.planner.datatypes.utils.NumericPair;
+import org.apache.ignite.internal.sql.engine.planner.datatypes.utils.TypePair;
+import org.apache.ignite.internal.sql.engine.planner.datatypes.utils.Types;
+import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
+import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
+import org.apache.ignite.internal.type.NativeTypeSpec;
+import org.apache.ignite.internal.type.NativeTypes;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * A set of tests to verify behavior of type coercion for quantify operators (ALL, SOME), when operands belongs to the NUMERIC type family.
+ *
+ * <p>This tests aim to help to understand in which cases implicit cast will be added to which operand.
+ */
+public class NumericQuantifyOperatorTypeCoercionTest extends BaseTypeCoercionTest {
+    private static Stream<Arguments> args() {
+        return Stream.of(
+                forTypePair(NumericPair.TINYINT_TINYINT)
+                        .firstOpBeSame()
+                        .secondOpBeSame(),
+
+                forTypePair(NumericPair.TINYINT_SMALLINT)
+                        .firstOpMatches(ofType(NativeTypes.INT16))
+                        .secondOpBeSame(),
+
+                forTypePair(NumericPair.TINYINT_INT)
+                        .firstOpMatches(ofType(NativeTypes.INT32))
+                        .secondOpBeSame(),
+
+                forTypePair(NumericPair.TINYINT_BIGINT)
+                        .firstOpMatches(ofType(NativeTypes.INT64))
+                        .secondOpBeSame(),
+
+                forTypePair(NumericPair.TINYINT_DECIMAL_1_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_3_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_3_0)),
+
+                forTypePair(NumericPair.TINYINT_DECIMAL_2_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_4_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_4_1)),
+
+                forTypePair(NumericPair.TINYINT_DECIMAL_4_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_3)),
+
+                forTypePair(NumericPair.TINYINT_DECIMAL_2_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_3_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_3_0)),
+
+                forTypePair(NumericPair.TINYINT_DECIMAL_3_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_4_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_4_1)),
+
+                forTypePair(NumericPair.TINYINT_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_3)),
+
+                forTypePair(NumericPair.TINYINT_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_0)),
+
+                forTypePair(NumericPair.TINYINT_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.TINYINT_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.TINYINT_REAL)
+                        .firstOpMatches(ofType(NativeTypes.FLOAT))
+                        .secondOpMatches(ofType(NativeTypes.FLOAT)),
+
+                forTypePair(NumericPair.TINYINT_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+
+                forTypePair(NumericPair.SMALLINT_SMALLINT)
+                        .firstOpMatches(ofType(NativeTypes.INT16))
+                        .secondOpMatches(ofType(NativeTypes.INT16)),
+
+                forTypePair(NumericPair.SMALLINT_INT)
+                        .firstOpMatches(ofType(NativeTypes.INT32))
+                        .secondOpMatches(ofType(NativeTypes.INT32)),
+
+                forTypePair(NumericPair.SMALLINT_BIGINT)
+                        .firstOpMatches(ofType(NativeTypes.INT64))
+                        .secondOpMatches(ofType(NativeTypes.INT64)),
+
+                forTypePair(NumericPair.SMALLINT_DECIMAL_1_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_0)),
+
+                forTypePair(NumericPair.SMALLINT_DECIMAL_2_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.SMALLINT_DECIMAL_4_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.SMALLINT_DECIMAL_2_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_0)),
+
+                forTypePair(NumericPair.SMALLINT_DECIMAL_3_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.SMALLINT_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.SMALLINT_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_0)),
+
+                forTypePair(NumericPair.SMALLINT_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.SMALLINT_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.SMALLINT_REAL)
+                        .firstOpMatches(ofType(NativeTypes.FLOAT))
+                        .secondOpMatches(ofType(NativeTypes.FLOAT)),
+
+                forTypePair(NumericPair.SMALLINT_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+
+                forTypePair(NumericPair.INT_INT)
+                        .firstOpMatches(ofType(NativeTypes.INT32))
+                        .secondOpMatches(ofType(NativeTypes.INT32)),
+
+                forTypePair(NumericPair.INT_BIGINT)
+                        .firstOpMatches(ofType(NativeTypes.INT64))
+                        .secondOpMatches(ofType(NativeTypes.INT64)),
+
+                forTypePair(NumericPair.INT_DECIMAL_1_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_10_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_10_0)),
+
+                forTypePair(NumericPair.INT_DECIMAL_2_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_11_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_11_1)),
+
+                forTypePair(NumericPair.INT_DECIMAL_4_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_13_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_13_3)),
+
+                forTypePair(NumericPair.INT_DECIMAL_2_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_10_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_10_0)),
+
+                forTypePair(NumericPair.INT_DECIMAL_3_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_11_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_11_1)),
+
+                forTypePair(NumericPair.INT_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_13_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_13_3)),
+
+                forTypePair(NumericPair.INT_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_10_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_10_0)),
+
+                forTypePair(NumericPair.INT_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_11_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_11_1)),
+
+                forTypePair(NumericPair.INT_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_13_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_13_3)),
+
+                forTypePair(NumericPair.INT_REAL)
+                        .firstOpMatches(ofType(NativeTypes.FLOAT))
+                        .secondOpMatches(ofType(NativeTypes.FLOAT)),
+
+                forTypePair(NumericPair.INT_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+
+                forTypePair(NumericPair.BIGINT_BIGINT)
+                        .firstOpBeSame()
+                        .secondOpBeSame(),
+
+                forTypePair(NumericPair.BIGINT_DECIMAL_1_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_19_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_19_0)),
+
+                forTypePair(NumericPair.BIGINT_DECIMAL_2_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_20_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_20_1)),
+
+                forTypePair(NumericPair.BIGINT_DECIMAL_4_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_22_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_22_3)),
+
+                forTypePair(NumericPair.BIGINT_DECIMAL_2_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_19_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_19_0)),
+
+                forTypePair(NumericPair.BIGINT_DECIMAL_3_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_20_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_20_1)),
+
+                forTypePair(NumericPair.BIGINT_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_22_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_22_3)),
+
+                forTypePair(NumericPair.BIGINT_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_19_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_19_0)),
+
+                forTypePair(NumericPair.BIGINT_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_20_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_20_1)),
+
+                forTypePair(NumericPair.BIGINT_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_22_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_22_3)),
+
+                forTypePair(NumericPair.BIGINT_REAL)
+                        .firstOpMatches(ofType(NativeTypes.FLOAT))
+                        .secondOpMatches(ofType(NativeTypes.FLOAT)),
+
+                forTypePair(NumericPair.BIGINT_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DECIMAL_1_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_1_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_1_0)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DECIMAL_2_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_2_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_2_1)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DECIMAL_4_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_4_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_4_3)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DECIMAL_2_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_2_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_2_0)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DECIMAL_3_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_3_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_3_1)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_3)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_0)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_REAL)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+                forTypePair(NumericPair.DECIMAL_1_0_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+
+                forTypePair(NumericPair.DECIMAL_2_1_DECIMAL_2_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_2_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_2_1)),
+
+                forTypePair(NumericPair.DECIMAL_2_1_DECIMAL_4_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_4_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_4_3)),
+
+                forTypePair(NumericPair.DECIMAL_2_1_DECIMAL_2_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_3_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_3_1)),
+
+                forTypePair(NumericPair.DECIMAL_2_1_DECIMAL_3_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_3_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_3_1)),
+
+                forTypePair(NumericPair.DECIMAL_2_1_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_3)),
+
+                forTypePair(NumericPair.DECIMAL_2_1_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.DECIMAL_2_1_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.DECIMAL_2_1_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_2_1_REAL)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+                forTypePair(NumericPair.DECIMAL_2_1_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+
+                forTypePair(NumericPair.DECIMAL_4_3_DECIMAL_4_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_4_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_4_3)),
+
+                forTypePair(NumericPair.DECIMAL_4_3_DECIMAL_2_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_3)),
+
+                forTypePair(NumericPair.DECIMAL_4_3_DECIMAL_3_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_3)),
+
+                forTypePair(NumericPair.DECIMAL_4_3_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_3)),
+
+                forTypePair(NumericPair.DECIMAL_4_3_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_4_3_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_4_3_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_4_3_REAL)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+                forTypePair(NumericPair.DECIMAL_4_3_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+
+                forTypePair(NumericPair.DECIMAL_2_0_DECIMAL_2_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_2_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_2_0)),
+
+                forTypePair(NumericPair.DECIMAL_2_0_DECIMAL_3_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_3_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_3_1)),
+
+                forTypePair(NumericPair.DECIMAL_2_0_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_3)),
+
+                forTypePair(NumericPair.DECIMAL_2_0_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_0)),
+
+                forTypePair(NumericPair.DECIMAL_2_0_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.DECIMAL_2_0_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_2_0_REAL)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+                forTypePair(NumericPair.DECIMAL_2_0_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+
+                forTypePair(NumericPair.DECIMAL_3_1_DECIMAL_3_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_3_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_3_1)),
+
+                forTypePair(NumericPair.DECIMAL_3_1_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_3)),
+
+                forTypePair(NumericPair.DECIMAL_3_1_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.DECIMAL_3_1_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.DECIMAL_3_1_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_3_1_REAL)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+                forTypePair(NumericPair.DECIMAL_3_1_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+
+                forTypePair(NumericPair.DECIMAL_5_3_DECIMAL_5_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_3)),
+
+                forTypePair(NumericPair.DECIMAL_5_3_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_5_3_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_5_3_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_5_3_REAL)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+                forTypePair(NumericPair.DECIMAL_5_3_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+
+                forTypePair(NumericPair.DECIMAL_5_0_DECIMAL_5_0)
+                        .firstOpMatches(ofType(Types.DECIMAL_5_0))
+                        .secondOpMatches(ofType(Types.DECIMAL_5_0)),
+
+                forTypePair(NumericPair.DECIMAL_5_0_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.DECIMAL_5_0_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_5_0_REAL)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+                forTypePair(NumericPair.DECIMAL_5_0_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+
+                forTypePair(NumericPair.DECIMAL_6_1_DECIMAL_6_1)
+                        .firstOpMatches(ofType(Types.DECIMAL_6_1))
+                        .secondOpMatches(ofType(Types.DECIMAL_6_1)),
+
+                forTypePair(NumericPair.DECIMAL_6_1_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_6_1_REAL)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+                forTypePair(NumericPair.DECIMAL_6_1_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+
+                forTypePair(NumericPair.DECIMAL_8_3_DECIMAL_8_3)
+                        .firstOpMatches(ofType(Types.DECIMAL_8_3))
+                        .secondOpMatches(ofType(Types.DECIMAL_8_3)),
+
+                forTypePair(NumericPair.DECIMAL_8_3_REAL)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+                forTypePair(NumericPair.DECIMAL_8_3_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpBeSame(),
+
+
+                forTypePair(NumericPair.REAL_REAL)
+                        .firstOpMatches(ofType(NativeTypes.FLOAT))
+                        .secondOpMatches(ofType(NativeTypes.FLOAT)),
+
+                forTypePair(NumericPair.REAL_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE)),
+
+
+                forTypePair(NumericPair.DOUBLE_DOUBLE)
+                        .firstOpMatches(ofType(NativeTypes.DOUBLE))
+                        .secondOpMatches(ofType(NativeTypes.DOUBLE))
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("args")
+    public void equalsTo(
+            TypePair typePair,
+            Matcher<RexNode> firstOperandMatcher,
+            Matcher<RexNode> secondOperandMatcher
+    ) throws Exception {
+        // TODO: IGNITE-24706 remove this assumption
+        assumeFalse(typePair.first().spec() == NativeTypeSpec.DECIMAL || typePair.second().spec() == NativeTypeSpec.DECIMAL);
+
+        IgniteSchema schema = createSchemaWithTwoColumnTable(typePair.first(), typePair.second());
+
+        assertPlan("SELECT c1 = SOME(SELECT c2 FROM t) FROM t", schema,
+                equalsToSomeOperatorMatcher(firstOperandMatcher, secondOperandMatcher)::matches, List.of());
+        assertPlan("SELECT c2 = SOME(SELECT c1 FROM t) FROM t", schema,
+                equalsToSomeOperatorMatcher(secondOperandMatcher, firstOperandMatcher)::matches, List.of());
+        assertPlan("SELECT c1 = ALL(SELECT c2 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 3)::matches, List.of());
+        assertPlan("SELECT c2 = ALL(SELECT c1 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 3)::matches, List.of());
+    }
+
+    @ParameterizedTest
+    @MethodSource("args")
+    public void lessThan(
+            TypePair typePair,
+            Matcher<RexNode> firstOperandMatcher,
+            Matcher<RexNode> secondOperandMatcher
+    ) throws Exception {
+        // TODO: IGNITE-24706 remove this assumption
+        assumeFalse(typePair.first().spec() == NativeTypeSpec.DECIMAL || typePair.second().spec() == NativeTypeSpec.DECIMAL);
+
+        IgniteSchema schema = createSchemaWithTwoColumnTable(typePair.first(), typePair.second());
+
+        assertPlan("SELECT c1 < SOME(SELECT c2 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c2 < SOME(SELECT c1 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(secondOperandMatcher, firstOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c1 < ALL(SELECT c2 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c2 < ALL(SELECT c1 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+    }
+
+    @ParameterizedTest
+    @MethodSource("args")
+    public void lessThanOrEqual(
+            TypePair typePair,
+            Matcher<RexNode> firstOperandMatcher,
+            Matcher<RexNode> secondOperandMatcher
+    ) throws Exception {
+        // TODO: IGNITE-24706 remove this assumption
+        assumeFalse(typePair.first().spec() == NativeTypeSpec.DECIMAL || typePair.second().spec() == NativeTypeSpec.DECIMAL);
+
+        IgniteSchema schema = createSchemaWithTwoColumnTable(typePair.first(), typePair.second());
+
+        assertPlan("SELECT c1 <= SOME(SELECT c2 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c2 <= SOME(SELECT c1 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(secondOperandMatcher, firstOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c1 <= ALL(SELECT c2 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c2 <= ALL(SELECT c1 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+    }
+
+    @ParameterizedTest
+    @MethodSource("args")
+    public void greaterThan(
+            TypePair typePair,
+            Matcher<RexNode> firstOperandMatcher,
+            Matcher<RexNode> secondOperandMatcher
+    ) throws Exception {
+        // TODO: IGNITE-24706 remove this assumption
+        assumeFalse(typePair.first().spec() == NativeTypeSpec.DECIMAL || typePair.second().spec() == NativeTypeSpec.DECIMAL);
+
+        IgniteSchema schema = createSchemaWithTwoColumnTable(typePair.first(), typePair.second());
+
+        assertPlan("SELECT c1 > SOME(SELECT c2 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c2 > SOME(SELECT c1 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(secondOperandMatcher, firstOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c1 > ALL(SELECT c2 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c2 > ALL(SELECT c1 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+    }
+
+    @ParameterizedTest
+    @MethodSource("args")
+    public void greaterThanOrEqual(
+            TypePair typePair,
+            Matcher<RexNode> firstOperandMatcher,
+            Matcher<RexNode> secondOperandMatcher
+    ) throws Exception {
+        // TODO: IGNITE-24706 remove this assumption
+        assumeFalse(typePair.first().spec() == NativeTypeSpec.DECIMAL || typePair.second().spec() == NativeTypeSpec.DECIMAL);
+
+        IgniteSchema schema = createSchemaWithTwoColumnTable(typePair.first(), typePair.second());
+
+        assertPlan("SELECT c1 >= SOME(SELECT c2 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c2 >= SOME(SELECT c1 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(secondOperandMatcher, firstOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c1 >= ALL(SELECT c2 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+        assertPlan("SELECT c2 >= ALL(SELECT c1 FROM t) FROM t", schema,
+                quantifyOperatorMatcher(firstOperandMatcher, secondOperandMatcher, 0, 1)::matches, List.of());
+    }
+
+    /**
+     * This test ensures that {@link #args()} doesn't miss any type pair from {@link NumericPair}.
+     */
+    @Test
+    void argsIncludesAllTypePairs() {
+        checkIncludesAllNumericTypePairs(args());
+    }
+
+    private static Matcher<IgniteRel> equalsToSomeOperatorMatcher(
+            Matcher<RexNode> first, Matcher<RexNode> second
+    ) {
+        return new BaseMatcher<>() {
+            @Override
+            public boolean matches(Object actual) {
+                RelNode projectRel = (RelNode) actual;
+                Join joinRel = (Join) projectRel.getInput(0);
+
+                RexNode comparison = joinRel.getCondition();
+
+                assertThat(comparison, instanceOf(RexCall.class));
+
+                RexCall comparisonCall = (RexCall) comparison;
+
+                RexNode leftOperand = comparisonCall.getOperands().get(0);
+                RexNode rightOperand = comparisonCall.getOperands().get(1);
+
+                assertThat(leftOperand, first);
+                assertThat(rightOperand, second);
+
+                return true;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+
+            }
+        };
+    }
+
+    private static Matcher<IgniteRel> quantifyOperatorMatcher(
+            Matcher<RexNode> first, Matcher<RexNode> second, int firstOpIdx, int secondOpIdx
+    ) {
+        return new BaseMatcher<>() {
+            @Override
+            public boolean matches(Object actual) {
+                RexNode expression = ((Project) actual).getProjects().get(0);
+
+                List<RexNode> comparisonToVerify = findComparison(expression, firstOpIdx, secondOpIdx);
+
+                assertThat(comparisonToVerify, not(Matchers.empty()));
+
+                for (RexNode comparison : comparisonToVerify) {
+                    assertThat(comparison, instanceOf(RexCall.class));
+
+                    RexCall comparisonCall = (RexCall) comparison;
+
+                    RexNode leftOperand = comparisonCall.getOperands().get(0);
+                    RexNode rightOperand = comparisonCall.getOperands().get(1);
+
+                    assertThat(leftOperand, first);
+                    assertThat(rightOperand, second);
+                }
+
+                return true;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+
+            }
+        };
+    }
+
+    /** Finds a comparison call comparing input references with provided indexes. */
+    private static List<RexNode> findComparison(RexNode expression, int firstOpIdx, int secondOpIdx) {
+        List<RexNode> result = new ArrayList<>();
+        new RexShuttle() {
+            @Override
+            public RexNode visitCall(RexCall call) {
+                if (call.getKind().belongsTo(SqlKind.BINARY_COMPARISON)) {
+                    result.add(call);
+
+                    return call;
+                }
+
+                return super.visitCall(call);
+            }
+        }.apply(expression);
+
+        result.removeIf(cmp -> {
+            try {
+                new RexShuttle() {
+                    @Override
+                    public RexNode visitInputRef(RexInputRef inputRef) {
+                        int column = inputRef.getIndex();
+                        if (column != firstOpIdx && column != secondOpIdx) {
+                            throw Util.FoundOne.NULL;
+                        }
+
+                        return inputRef;
+                    }
+                }.apply(cmp);
+            } catch (Util.FoundOne ignored) {
+                return true;
+            }
+
+            return false;
+        });
+
+        return result;
+    }
+}