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 871915f54b IGNITE-18966: Sql. Custom data types. Fix least restrictive 
type and nullability (#1777)
871915f54b is described below

commit 871915f54b403afe221fda5c1e1775d98ff8b2ea
Author: Max Zhuravkov <[email protected]>
AuthorDate: Thu Mar 16 11:32:37 2023 +0400

    IGNITE-18966: Sql. Custom data types. Fix least restrictive type and 
nullability (#1777)
---
 .../internal/sql/engine/ItImplicitCastsTest.java   | 100 ++++++++++--
 .../ignite/internal/sql/engine/ItUuidTest.java     |  30 ++++
 .../internal/sql/engine/externalize/RelJson.java   |   5 +-
 .../sql/engine/prepare/IgniteSqlValidator.java     |  14 +-
 .../sql/engine/prepare/IgniteTypeCoercion.java     |  30 ++++
 .../sql/engine/type/IgniteTypeFactory.java         |  47 +++---
 .../util/SafeCustomTypeInternalConversion.java     |   2 +-
 .../sql/engine/planner/ImplicitCastsTest.java      | 180 +++++++++++++--------
 .../engine/prepare/LeastRestrictiveTypesTest.java  | 123 ++++++++++++--
 .../sql/engine/prepare/TypeCoercionTest.java       |  39 +++++
 10 files changed, 445 insertions(+), 125 deletions(-)

diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItImplicitCastsTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItImplicitCastsTest.java
index 3697d841b8..135d53cd67 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItImplicitCastsTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItImplicitCastsTest.java
@@ -17,13 +17,22 @@
 
 package org.apache.ignite.internal.sql.engine;
 
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
 import java.util.stream.Stream;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.runtime.CalciteContextException;
 import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeUtil;
+import org.apache.ignite.internal.sql.engine.type.IgniteCustomType;
+import 
org.apache.ignite.internal.sql.engine.type.IgniteCustomTypeCoercionRules;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.apache.ignite.internal.sql.engine.type.UuidType;
 import org.apache.ignite.tx.Transaction;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
@@ -44,15 +53,21 @@ public class ItImplicitCastsTest extends 
ClusterPerClassIntegrationTest {
     @ParameterizedTest
     @MethodSource("columnPairs")
     public void testFilter(ColumnPair columnPair) {
-        prepareTables(columnPair);
+        sql(format("CREATE TABLE T11 (c1 int primary key, c2 {})", 
columnPair.lhs));
+        sql(format("CREATE TABLE T12 (c1 int primary key, c2 {})", 
columnPair.rhs));
 
-        assertQuery("SELECT T11.c2 FROM T11 WHERE T11.c2 > 1.0").check();
+        String value = columnPair.lhsLiteral(0);
+        // Implicit casts are added to the left hand side of the expression.
+        String query = format("SELECT T11.c2 FROM T11 WHERE T11.c2 > CAST({} 
AS {})", value, columnPair.rhs);
+
+        assertQuery(query).check();
     }
 
     @ParameterizedTest
     @MethodSource("columnPairs")
     public void testMergeSort(ColumnPair columnPair) {
-        prepareTables(columnPair);
+        sql(format("CREATE TABLE T11 (c1 int primary key, c2 {})", 
columnPair.lhs));
+        sql(format("CREATE TABLE T12 (c1 int primary key, c2 {})", 
columnPair.rhs));
 
         assertQuery("SELECT T11.c2, T12.c2 FROM T11, T12 WHERE T11.c2 = 
T12.c2").check();
         assertQuery("SELECT T11.c2, T12.c2 FROM T11, T12 WHERE T11.c2 IS NOT 
DISTINCT FROM T12.c2").check();
@@ -61,7 +76,8 @@ public class ItImplicitCastsTest extends 
ClusterPerClassIntegrationTest {
     @ParameterizedTest
     @MethodSource("columnPairs")
     public void testNestedLoopJoin(ColumnPair columnPair) {
-        prepareTables(columnPair);
+        sql(format("CREATE TABLE T11 (c1 int primary key, c2 {})", 
columnPair.lhs));
+        sql(format("CREATE TABLE T12 (c1 int primary key, c2 {})", 
columnPair.rhs));
 
         assertQuery("SELECT T11.c2, T12.c2 FROM T11, T12 WHERE T11.c2 != 
T12.c2").check();
         assertQuery("SELECT T11.c2, T12.c2 FROM T11, T12 WHERE T11.c2 IS 
DISTINCT FROM T12.c2").check();
@@ -77,26 +93,52 @@ public class ItImplicitCastsTest extends 
ClusterPerClassIntegrationTest {
 
     private static Stream<ColumnPair> columnPairs() {
         IgniteTypeFactory typeFactory = new IgniteTypeFactory();
+        List<ColumnPair> columnPairs = new ArrayList<>();
+
+        columnPairs.add(new 
ColumnPair(typeFactory.createSqlType(SqlTypeName.INTEGER), 
typeFactory.createSqlType(SqlTypeName.FLOAT)));
+        columnPairs.add(new 
ColumnPair(typeFactory.createSqlType(SqlTypeName.DOUBLE), 
typeFactory.createSqlType(SqlTypeName.BIGINT)));
+
+        // IgniteCustomType: test cases for custom data types in join and 
filter conditions.
+        // Implicit casts must be added to the types a custom data type can be 
converted from.
+        IgniteCustomTypeCoercionRules customTypeCoercionRules = 
typeFactory.getCustomTypeCoercionRules();
+        for (String typeName : typeFactory.getCustomTypeSpecs().keySet()) {
+            IgniteCustomType customType = 
typeFactory.createCustomType(typeName);
+
+            for (SqlTypeName sourceTypeName : 
customTypeCoercionRules.canCastFrom(typeName)) {
+
+                RelDataType sourceType;
+                if (sourceTypeName == SqlTypeName.CHAR) {
+                    // Generate sample value to use its length as precision 
for CHAR type is order to avoid data truncation.
+                    String sampleValue = ColumnPair.generateValue(customType, 
0, false);
+                    sourceType = typeFactory.createSqlType(SqlTypeName.CHAR, 
sampleValue.length());
+                } else {
+                    sourceType = typeFactory.createSqlType(sourceTypeName);
+                }
+
+                ColumnPair columnPair = new ColumnPair(customType, sourceType);
+                columnPairs.add(columnPair);
+            }
+        }
 
-        return Stream.of(
-                new ColumnPair(typeFactory.createSqlType(SqlTypeName.INTEGER), 
typeFactory.createSqlType(SqlTypeName.FLOAT)),
-                new ColumnPair(typeFactory.createSqlType(SqlTypeName.DOUBLE), 
typeFactory.createSqlType(SqlTypeName.BIGINT))
-        );
-    }
+        List<ColumnPair> result = new ArrayList<>(columnPairs);
+        Collections.reverse(columnPairs);
 
-    private static void prepareTables(ColumnPair columnPair) {
-        sql(String.format("CREATE TABLE T11 (c1 int primary key, c2 %s)", 
columnPair.lhs));
-        sql(String.format("CREATE TABLE T12 (c1 decimal primary key, c2 %s)", 
columnPair.rhs));
+        columnPairs.stream().map(p -> new ColumnPair(p.rhs, 
p.lhs)).forEach(result::add);
 
+        return result.stream();
+    }
+
+    private static void initData(ColumnPair columnPair) {
         Transaction tx = CLUSTER_NODES.get(0).transactions().begin();
-        sql(tx, "INSERT INTO T11 VALUES(1, 2)");
-        sql(tx, "INSERT INTO T11 VALUES(2, 3)");
-        sql(tx, "INSERT INTO T12 VALUES(1, 2)");
-        sql(tx, "INSERT INTO T12 VALUES(2, 4)");
+        sql(tx, format("INSERT INTO T11 VALUES(1, CAST({} AS {}))", 
columnPair.lhsLiteral(1), columnPair.lhs));
+        sql(tx, format("INSERT INTO T11 VALUES(2, CAST({} AS {}))", 
columnPair.lhsLiteral(3), columnPair.lhs));
+        sql(tx, format("INSERT INTO T12 VALUES(1, CAST({} AS {}))", 
columnPair.lhsLiteral(2), columnPair.rhs));
+        sql(tx, format("INSERT INTO T12 VALUES(2, CAST({} AS {}))", 
columnPair.lhsLiteral(4), columnPair.rhs));
         tx.commit();
     }
 
     private static final class ColumnPair {
+
         private final RelDataType lhs;
 
         private final RelDataType rhs;
@@ -110,5 +152,31 @@ public class ItImplicitCastsTest extends 
ClusterPerClassIntegrationTest {
         public String toString() {
             return lhs + " " + rhs;
         }
+
+        String lhsLiteral(int idx) {
+            return generateValue(lhs, idx, true);
+        }
+
+        static String generateValue(RelDataType type, int i, boolean literal) {
+            if (SqlTypeUtil.isNumeric(type)) {
+                return Integer.toString(i);
+            } else if (type instanceof UuidType
+                    || type.getSqlTypeName() == SqlTypeName.CHAR
+                    || type.getSqlTypeName() == SqlTypeName.VARCHAR) {
+                // We need to generate valid UUID string so cast operations 
won't fail at runtime.
+                return generateUuid(i, literal);
+            } else {
+                throw new IllegalArgumentException("Unsupported type: " + 
type);
+            }
+        }
+
+        private static String generateUuid(int i, boolean literal) {
+            UUID val = new UUID(i, i);
+            if (!literal) {
+                return val.toString();
+            } else {
+                return format("'{}'", val);
+            }
+        }
     }
 }
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItUuidTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItUuidTest.java
index 4bd6965f99..a80ccf87d2 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItUuidTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItUuidTest.java
@@ -22,6 +22,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsString;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.UUID;
 import java.util.stream.Stream;
 import org.apache.calcite.runtime.CalciteContextException;
@@ -33,6 +35,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 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;
 
 /**
@@ -282,4 +285,31 @@ public class ItUuidTest extends 
ClusterPerClassIntegrationTest {
         assertQuery("SELECT RAND_UUID() = RAND_UUID()").returns(false).check();
         assertQuery("SELECT RAND_UUID() != RAND_UUID()").returns(true).check();
     }
+
+    @Test
+    public void testDisallowMismatchTypesOnInsert() {
+        var query = format("INSERT INTO t (id, uuid_key) VALUES (10, null), 
(20, '{}')", UUID_1);
+        var t = assertThrows(CalciteContextException.class, () -> sql(query));
+        assertThat(t.getMessage(), containsString("Values passed to VALUES 
operator must have compatible types"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getOps")
+    public void testDisallowMismatchTypesSetOp(String setOp) {
+        sql("INSERT INTO t (id, uuid_key) VALUES (1, ?)", UUID_1);
+        sql("INSERT INTO t (id, uuid_key) VALUES (2, ?)", UUID_2);
+
+        var query = format("SELECT uuid_key FROM t {} SELECT CAST(uuid_key AS 
VARCHAR) FROM t", setOp);
+        var t = assertThrows(CalciteContextException.class, () ->  sql(query));
+        assertThat(t.getMessage(), containsString(format("Type mismatch in 
column 1 of {}", setOp)));
+    }
+
+    private static Stream<Arguments> getOps() {
+        List<Arguments> result = new ArrayList<>();
+
+        SqlKind.SET_QUERY.stream().map(SqlKind::name).forEach(e -> 
result.add(Arguments.of(e)));
+        SqlKind.SET_QUERY.stream().map(op -> op.name() + " ALL").forEach(e -> 
result.add(Arguments.of(e)));
+
+        return result.stream();
+    }
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJson.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJson.java
index 3464c2cac6..d02bb72eb6 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJson.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJson.java
@@ -696,11 +696,12 @@ class RelJson {
         } else if (o instanceof Map) {
             Map<String, Object> map = (Map<String, Object>) o;
             String clazz = (String) map.get("class");
+            boolean nullable = Boolean.TRUE == map.get("nullable");
 
             if (clazz != null) {
                 RelDataType type = 
typeFactory.createJavaType(classForName(clazz, false));
 
-                if (Boolean.TRUE == map.get("nullable")) {
+                if (nullable) {
                     type = typeFactory.createTypeWithNullability(type, true);
                 }
 
@@ -741,7 +742,7 @@ class RelJson {
                     type = typeFactory.createSqlType(sqlTypeName, precision, 
scale);
                 }
 
-                if (Boolean.TRUE == map.get("nullable")) {
+                if (nullable) {
                     type = typeFactory.createTypeWithNullability(type, true);
                 }
 
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java
index 3d6579e211..62874427a1 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java
@@ -636,21 +636,27 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
         // We must check the number of dynamic parameters unless
         // https://issues.apache.org/jira/browse/IGNITE-18653
         // is resolved.
+
+        RelDataType parameterType;
+
         if (dynamicParam.getIndex() < parameters.length) {
             Object param = parameters[dynamicParam.getIndex()];
             // IgniteCustomType: first we must check whether dynamic parameter 
is a custom data type.
             // If so call createCustomType with appropriate arguments.
             if (param instanceof UUID) {
-                return typeFactory().createCustomType(UuidType.NAME);
+                parameterType =  typeFactory().createCustomType(UuidType.NAME);
             } else if (param != null) {
-                return 
typeFactory().toSql(typeFactory().createType(param.getClass()));
+                parameterType = 
typeFactory().toSql(typeFactory().createType(param.getClass()));
             } else {
-                return typeFactory().createSqlType(SqlTypeName.NULL);
+                parameterType = typeFactory().createSqlType(SqlTypeName.NULL);
             }
         } else {
             // This query will be rejected since the number of dynamic 
parameters
             // is not valid.
-            return typeFactory().createSqlType(SqlTypeName.NULL);
+            parameterType = typeFactory().createSqlType(SqlTypeName.NULL);
         }
+
+        // Dynamic parameters are nullable.
+        return typeFactory().createTypeWithNullability(parameterType, true);
     }
 }
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 738671a28e..8202255cc8 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
@@ -291,6 +291,36 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
         return syncedType;
     }
 
+    /** {@inheritDoc} **/
+    @Override
+    public @Nullable RelDataType commonTypeForBinaryComparison(@Nullable 
RelDataType type1, @Nullable RelDataType type2) {
+        if (type1 == null || type2 == null) {
+            return null;
+        }
+
+        // IgniteCustomType: If one of the arguments is a custom data type,
+        // check whether it is possible to convert another type to it.
+        // Returns not null to indicate that a CAST operation can be added
+        // to convert another type to this custom data type.
+        if (type1 instanceof IgniteCustomType) {
+            IgniteCustomType to = (IgniteCustomType) type1;
+            return tryCustomTypeCoercionRules(type2, to);
+        } else if (type2 instanceof IgniteCustomType) {
+            IgniteCustomType to = (IgniteCustomType) type2;
+            return tryCustomTypeCoercionRules(type1, to);
+        } else {
+            return super.commonTypeForBinaryComparison(type1, type2);
+        }
+    }
+
+    private @Nullable RelDataType tryCustomTypeCoercionRules(RelDataType from, 
IgniteCustomType to) {
+        if (typeCoercionRules.needToCast(from, to)) {
+            return to;
+        } else {
+            return null;
+        }
+    }
+
     private static SqlNode castTo(SqlNode node, RelDataType type) {
         SqlDataTypeSpec targetDataType;
         if (type instanceof IgniteCustomType) {
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
index 01c95275e4..fa1d26dc72 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
@@ -48,9 +48,7 @@ import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.BasicSqlType;
 import org.apache.calcite.sql.type.IntervalSqlType;
-import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeName;
-import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.ignite.internal.schema.NativeType;
 import org.apache.ignite.internal.schema.NativeTypes;
 import org.apache.ignite.internal.sql.engine.util.Commons;
@@ -375,28 +373,36 @@ public class IgniteTypeFactory extends 
JavaTypeFactoryImpl {
             assert resultType instanceof BasicSqlType : "leastRestrictive is 
expected to return a new instance of a type: " + resultType;
 
             IgniteCustomType firstCustomType = null;
-            SqlTypeFamily sqlTypeFamily = null;
+            boolean hasAnyType = false;
+            boolean hasBuiltInType = false;
 
             for (var type : types) {
                 if (type instanceof IgniteCustomType) {
-                    var customType = (IgniteCustomType) type;
-
                     if (firstCustomType == null) {
                         firstCustomType = (IgniteCustomType) type;
-                    } else if 
(!Objects.equals(firstCustomType.getCustomTypeName(), 
customType.getCustomTypeName())) {
-                        // IgniteCustomType: Conversion between custom data 
types is not supported.
-                        return null;
+                    } else {
+                        IgniteCustomType customType = (IgniteCustomType) type;
+                        if 
(!Objects.equals(firstCustomType.getCustomTypeName(), 
customType.getCustomTypeName())) {
+                            // IgniteCustomType: Conversion between custom 
data types is not supported.
+                            return null;
+                        }
                     }
-                } else if (SqlTypeUtil.isCharacter(type)) {
-                    sqlTypeFamily = type.getSqlTypeName().getFamily();
+                } else if (type.getSqlTypeName() == SqlTypeName.ANY) {
+                    hasAnyType = true;
+                } else if (type.getSqlTypeName() != SqlTypeName.ANY) {
+                    hasBuiltInType = true;
                 }
             }
 
-            if (firstCustomType != null && sqlTypeFamily != null) {
-                // IgniteCustomType: we allow implicit casts from VARCHAR to 
custom data types.
-                return firstCustomType;
-            } else {
+            if (hasAnyType && hasBuiltInType && firstCustomType != null) {
+                // There is no least restrictive type between ANY, built-in 
type, and a custom data type.
+                return null;
+            } else if ((hasAnyType && hasBuiltInType) || (hasAnyType && 
firstCustomType != null)) {
+                // When at least one of arguments have sqlTypeName = ANY,
+                // return it in order to be consistent with default 
implementation.
                 return resultType;
+            } else {
+                return null;
             }
         } else {
             return resultType;
@@ -464,7 +470,7 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
      * @param precision Precision if supported.
      * @return A custom data type.
      */
-    public RelDataType createCustomType(String typeName, int precision) {
+    public IgniteCustomType createCustomType(String typeName, int precision) {
         IgniteCustomTypeFactory customTypeFactory = 
customDataTypes.typeFactories.get(typeName);
         if (customTypeFactory == null) {
             throw new IllegalArgumentException("Unexpected custom data type: " 
+ typeName);
@@ -472,12 +478,9 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl 
{
 
         // By default a type must not be nullable.
         // See SqlTypeFactory::createSqlType.
-        //
-        // TODO workaround for 
https://issues.apache.org/jira/browse/IGNITE-18752
-        //  Set nullable to false and uncomment the assertion after upgrading 
to calcite 1.33.
-        IgniteCustomType customType = customTypeFactory.newType(true, 
precision);
-        // assert !customType.isNullable() : "makeCustomType must not return a 
nullable type: " + typeName + " " + customType;
-        return canonize(customType);
+        IgniteCustomType customType = customTypeFactory.newType(false, 
precision);
+        assert !customType.isNullable() : "makeCustomType must not return a 
nullable type: " + typeName + " " + customType;
+        return (IgniteCustomType) canonize(customType);
     }
 
     /**
@@ -488,7 +491,7 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
      * @param typeName Type name.
      * @return A custom data type.
      */
-    public RelDataType createCustomType(String typeName) {
+    public IgniteCustomType createCustomType(String typeName) {
         return createCustomType(typeName, PRECISION_NOT_SPECIFIED);
     }
 
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/SafeCustomTypeInternalConversion.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/SafeCustomTypeInternalConversion.java
index 2c77a6c44f..ebf0642974 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/SafeCustomTypeInternalConversion.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/SafeCustomTypeInternalConversion.java
@@ -74,6 +74,6 @@ final class SafeCustomTypeInternalConversion {
     }
 
     private static String storageTypeMismatch(Object value, Class<?> type) {
-        return String.format("storageType is %s value must also be %s but it 
was not: %s", type, type, value);
+        return String.format("storageType is %s value must also be %s but it 
was: %s", type, type, value);
     }
 }
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ImplicitCastsTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ImplicitCastsTest.java
index 4e8c3e8478..b0a7b826c6 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ImplicitCastsTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/ImplicitCastsTest.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.sql.engine.planner;
 
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -35,6 +37,8 @@ import 
org.apache.ignite.internal.sql.engine.rel.IgniteNestedLoopJoin;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTableScan;
 import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
+import org.apache.ignite.internal.sql.engine.type.IgniteCustomType;
+import 
org.apache.ignite.internal.sql.engine.type.IgniteCustomTypeCoercionRules;
 import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
@@ -45,11 +49,6 @@ import org.junit.jupiter.params.provider.MethodSource;
  */
 public class ImplicitCastsTest extends AbstractPlannerTest {
 
-    private static final RelDataType INTEGER = 
TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER);
-
-    private static final RelDataType FLOAT = 
TYPE_FACTORY.createSqlType(SqlTypeName.FLOAT);
-
-
     /** MergeSort join - casts are pushed down to children. **/
     @ParameterizedTest
     @MethodSource("joinColumnTypes")
@@ -82,62 +81,31 @@ public class ImplicitCastsTest extends AbstractPlannerTest {
     /** Filter clause - casts are added to condition operands. **/
     @ParameterizedTest
     @MethodSource("filterTypes")
-    public void testFilter(RelDataType lhs, ExpectedTypes expected) throws 
Exception {
+    public void testFilter(RelDataType lhs, RelDataType rhs, ExpectedTypes 
expected) throws Exception {
         IgniteSchema igniteSchema = new IgniteSchema("PUBLIC");
 
         addTable(igniteSchema, "A1", "COL1", lhs);
 
-        assertPlan("SELECT * FROM A1 WHERE COL1 > 1", igniteSchema, 
isInstanceOf(IgniteTableScan.class)
+        // Parameter types are not checked during the validation phase.
+        List<Object> params = List.of("anything");
+
+        String query = format("SELECT * FROM A1 WHERE COL1 > CAST(? AS {})", 
rhs);
+        assertPlan(query, igniteSchema, isInstanceOf(IgniteTableScan.class)
                 .and(node -> {
                     String actualPredicate = node.condition().toString();
-                    String expectedPredicate;
 
-                    if (expected.lhs == null) {
-                        expectedPredicate = ">($t1, 1)";
-                    } else {
-                        expectedPredicate = String.format(">(CAST($t1):%s NOT 
NULL, 1)", lhs);
-                    }
+                    // lhs is not null, rhs may be null.
+                    String castedLhs = castedExprNotNullable("$t1", 
expected.lhs);
+                    String castedRhs =  castedExpr("?0", expected.rhs);
+                    String expectedPredicate = format(">({}, {})",  castedLhs, 
castedRhs);
 
-                    return expectedPredicate.equals(actualPredicate);
-                }));
-    }
-
-    private static Stream<Arguments> joinColumnTypes() {
-
-        List<RelDataType> numericTypes = 
SqlTypeName.NUMERIC_TYPES.stream().map(t -> {
-            if (t == SqlTypeName.DECIMAL) {
-                return TYPE_FACTORY.createSqlType(t, 10, 2);
-            } else {
-                return TYPE_FACTORY.createSqlType(t);
-            }
-        }).collect(Collectors.toList());
-
-        List<Arguments> arguments = new ArrayList<>();
-
-        for (RelDataType lhs : numericTypes) {
-            for (RelDataType rhs : numericTypes) {
-                ExpectedTypes expectedTypes;
-                if (lhs.equals(rhs)) {
-                    expectedTypes = new ExpectedTypes(null, null);
-                } else {
-                    RelDataType t = 
TYPE_FACTORY.leastRestrictive(Arrays.asList(lhs, rhs));
-                    expectedTypes = new ExpectedTypes(t.equals(lhs) ? null : 
t, t.equals(rhs) ? null : t);
-                }
-                arguments.add(Arguments.of(lhs, rhs, expectedTypes));
-            }
-        }
-
-        return arguments.stream();
-    }
-
-    private static Stream<Arguments> filterTypes() {
-        return Stream.of(
-                Arguments.arguments(INTEGER, new ExpectedTypes(null, null)),
-                Arguments.arguments(FLOAT, new ExpectedTypes(null, null))
-        );
+                    return Objects.equals(expectedPredicate, actualPredicate);
+                }), params);
     }
 
     private static final class ExpectedTypes {
+
+        // null means no conversion is necessary.
         final RelDataType lhs;
 
         final RelDataType rhs;
@@ -172,7 +140,7 @@ public class ImplicitCastsTest extends AbstractPlannerTest {
             if (expected == null) {
                 return scan.projects() == null;
             } else {
-                String expectedProjections = String.format("[$t0, $t1, 
CAST($t1):%s NOT NULL]", expected);
+                String expectedProjections = format("[$t0, $t1, {}]", 
castedExpr("$t1", expected));
                 String actualProjections;
 
                 if (scan.projects() == null) {
@@ -197,23 +165,11 @@ public class ImplicitCastsTest extends 
AbstractPlannerTest {
         @Override
         public boolean test(IgniteNestedLoopJoin node) {
             String actualCondition = node.getCondition().toString();
-            RelDataType expected1 = expected.lhs;
-            RelDataType expected2 = expected.rhs;
             SqlOperator opToUse = SqlStdOperatorTable.NOT_EQUALS;
-            String expectedCondition;
 
-            if (expected1 != null && expected2 != null) {
-                expectedCondition = String.format(
-                        "%s(CAST($1):%s NOT NULL, CAST($3):%s NOT NULL)",
-                        opToUse.getName(), expected1, expected2);
-
-            } else if (expected1 == null && expected2 == null) {
-                expectedCondition = String.format("%s($1, $3)", 
opToUse.getName());
-            } else if (expected1 != null) {
-                expectedCondition = String.format("%s(CAST($1):%s NOT NULL, 
$3)", opToUse.getName(), expected1);
-            } else {
-                expectedCondition = String.format("%s($1, CAST($3):%s NOT 
NULL)", opToUse.getName(), expected2);
-            }
+            String castedLhs = castedExpr("$1", expected.lhs);
+            String castedRhs = castedExpr("$3", expected.rhs);
+            String expectedCondition = format("{}({}, {})", opToUse.getName(), 
castedLhs, castedRhs);
 
             return Objects.equals(actualCondition, expectedCondition);
         }
@@ -227,4 +183,96 @@ public class ImplicitCastsTest extends AbstractPlannerTest 
{
 
         createTable(igniteSchema, tableName, tableType, 
IgniteDistributions.single());
     }
+
+    private static String castedExpr(String idx, @Nullable RelDataType type) {
+        if (type == null) {
+            return idx;
+        } else {
+            return format("CAST({}):{}", idx, type.isNullable() ? 
type.toString() : type + " NOT NULL");
+        }
+    }
+
+    private static String castedExprNotNullable(String idx, @Nullable 
RelDataType type) {
+        if (type != null) {
+            return castedExpr(idx, 
TYPE_FACTORY.createTypeWithNullability(type, false));
+        } else {
+            return castedExpr(idx, null);
+        }
+    }
+
+    private static Stream<Arguments> filterTypes() {
+        return joinColumnTypes().map(args -> {
+            Object[] values = args.get();
+            ExpectedTypes expectedTypes = (ExpectedTypes) values[2];
+            // We use dynamic parameter in conditional expression and dynamic 
parameters has nullable types
+            // So we need to add nullable flag to types fully match.
+            if (expectedTypes.rhs != null && !expectedTypes.rhs.isNullable()) {
+                RelDataType nullableRhs = 
TYPE_FACTORY.createTypeWithNullability(expectedTypes.rhs, true);
+                expectedTypes = new ExpectedTypes(expectedTypes.lhs, 
nullableRhs);
+            }
+
+            return Arguments.of(values[0], values[1], expectedTypes);
+        });
+    }
+
+    private static Stream<Arguments> joinColumnTypes() {
+
+        List<RelDataType> numericTypes = 
SqlTypeName.NUMERIC_TYPES.stream().map(t -> {
+            if (t == SqlTypeName.DECIMAL) {
+                return TYPE_FACTORY.createSqlType(t, 10, 2);
+            } else {
+                return TYPE_FACTORY.createSqlType(t);
+            }
+        }).collect(Collectors.toList());
+
+        List<Arguments> arguments = new ArrayList<>();
+
+        for (RelDataType lhs : numericTypes) {
+            for (RelDataType rhs : numericTypes) {
+                ExpectedTypes expectedTypes;
+                if (lhs.equals(rhs)) {
+                    expectedTypes = new ExpectedTypes(null, null);
+                } else {
+                    List<RelDataType> types = Arrays.asList(lhs, rhs);
+                    RelDataType t = TYPE_FACTORY.leastRestrictive(types);
+                    if (t == null) {
+                        String error = format(
+                                "No least restrictive types between {}. This 
case requires special additional hand coding", types
+                        );
+                        throw new IllegalArgumentException(error);
+                    }
+                    expectedTypes = new ExpectedTypes(t.equals(lhs) ? null : 
t, t.equals(rhs) ? null : t);
+                }
+                arguments.add(Arguments.of(lhs, rhs, expectedTypes));
+            }
+        }
+
+        // IgniteCustomType: test cases for custom data types in join and 
filter conditions.
+        // Implicit casts must be added to the types a custom data type can be 
converted from.
+        IgniteCustomTypeCoercionRules customTypeCoercionRules = 
TYPE_FACTORY.getCustomTypeCoercionRules();
+
+        for (String customTypeName : 
TYPE_FACTORY.getCustomTypeSpecs().keySet()) {
+            IgniteCustomType customType = 
TYPE_FACTORY.createCustomType(customTypeName);
+
+            for (SqlTypeName sourceTypeName : 
customTypeCoercionRules.canCastFrom(customTypeName)) {
+                RelDataType sourceType = 
TYPE_FACTORY.createSqlType(sourceTypeName);
+
+                arguments.add(Arguments.of(sourceType, customType, new 
ExpectedTypes(customType, null)));
+                arguments.add(Arguments.of(customType, sourceType, new 
ExpectedTypes(null, customType)));
+            }
+        }
+
+        List<Arguments> result = new ArrayList<>(arguments);
+
+        arguments.stream().map(args -> {
+            Object[] argsVals = args.get();
+            Object lhs = argsVals[1];
+            Object rhs = argsVals[0];
+            ExpectedTypes expected = (ExpectedTypes) argsVals[2];
+
+            return Arguments.of(lhs, rhs, new ExpectedTypes(expected.rhs, 
expected.lhs));
+        });
+
+        return result.stream();
+    }
 }
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/LeastRestrictiveTypesTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/LeastRestrictiveTypesTest.java
index ada01dcce1..e7f615aa42 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/LeastRestrictiveTypesTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/LeastRestrictiveTypesTest.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.sql.engine.prepare;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -25,8 +26,12 @@ import java.util.List;
 import java.util.stream.Stream;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.ignite.internal.schema.NativeTypes;
+import org.apache.ignite.internal.sql.engine.type.IgniteCustomType;
+import org.apache.ignite.internal.sql.engine.type.IgniteCustomTypeSpec;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
-import org.apache.ignite.internal.sql.engine.type.UuidType;
+import org.apache.ignite.sql.ColumnType;
+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;
@@ -54,10 +59,10 @@ public class LeastRestrictiveTypesTest {
 
     private static final RelDataType DECIMAL = 
TYPE_FACTORY.createSqlType(SqlTypeName.DECIMAL, 1000, 10);
 
-    private static final RelDataType UUID = 
TYPE_FACTORY.createCustomType(UuidType.NAME);
-
     private static final RelDataType VARCHAR = 
TYPE_FACTORY.createSqlType(SqlTypeName.VARCHAR, 36);
 
+    private static final RelDataType CUSTOM_TYPE = new TestCustomType(false);
+
     // ANY produced by the default implementation of leastRestrictiveType has 
nullability = true
     private static final RelDataType ANY = 
TYPE_FACTORY.createTypeWithNullability(TYPE_FACTORY.createSqlType(SqlTypeName.ANY),
 true);
 
@@ -216,24 +221,85 @@ public class LeastRestrictiveTypesTest {
     }
 
     @ParameterizedTest
-    @MethodSource("uuidTests")
+    @MethodSource("customTypeTests")
     public void testUuid(RelDataType t1, RelDataType t2, LeastRestrictiveType 
leastRestrictiveType) {
         expectLeastRestrictiveType(t1, t2, leastRestrictiveType);
         expectLeastRestrictiveType(t2, t1, leastRestrictiveType);
     }
 
-    private static Stream<Arguments> uuidTests() {
+    private static Stream<Arguments> customTypeTests() {
+        List<Arguments> tests = new ArrayList<>();
+
+        tests.add(Arguments.arguments(CUSTOM_TYPE, TINYINT, 
LeastRestrictiveType.none()));
+        tests.add(Arguments.arguments(CUSTOM_TYPE, SMALLINT, 
LeastRestrictiveType.none()));
+        tests.add(Arguments.arguments(CUSTOM_TYPE, INTEGER, 
LeastRestrictiveType.none()));
+        tests.add(Arguments.arguments(CUSTOM_TYPE, FLOAT, 
LeastRestrictiveType.none()));
+        tests.add(Arguments.arguments(CUSTOM_TYPE, REAL, 
LeastRestrictiveType.none()));
+        tests.add(Arguments.arguments(CUSTOM_TYPE, DOUBLE, 
LeastRestrictiveType.none()));
+        tests.add(Arguments.arguments(CUSTOM_TYPE, DECIMAL, 
LeastRestrictiveType.none()));
+        tests.add(Arguments.arguments(CUSTOM_TYPE, BIGINT, 
LeastRestrictiveType.none()));
+        tests.add(Arguments.arguments(CUSTOM_TYPE, VARCHAR, 
LeastRestrictiveType.none()));
+        tests.add(Arguments.arguments(CUSTOM_TYPE, CUSTOM_TYPE, new 
LeastRestrictiveType(CUSTOM_TYPE)));
+
+        return tests.stream();
+    }
+
+    @ParameterizedTest
+    @MethodSource("anyTests")
+    public void testAny(RelDataType t1, RelDataType t2, LeastRestrictiveType 
leastRestrictiveType) {
+        expectLeastRestrictiveType(t1, t2, leastRestrictiveType);
+        expectLeastRestrictiveType(t2, t1, leastRestrictiveType);
+    }
+
+    private static Stream<Arguments> anyTests() {
+        List<Arguments> tests = new ArrayList<>();
+        LeastRestrictiveType anyType = new LeastRestrictiveType(ANY);
+
+        tests.add(Arguments.arguments(ANY, TINYINT, anyType));
+        tests.add(Arguments.arguments(ANY, SMALLINT, anyType));
+        tests.add(Arguments.arguments(ANY, INTEGER, anyType));
+        tests.add(Arguments.arguments(ANY, FLOAT, anyType));
+        tests.add(Arguments.arguments(ANY, REAL, anyType));
+        tests.add(Arguments.arguments(ANY, DOUBLE, anyType));
+        tests.add(Arguments.arguments(ANY, DECIMAL, anyType));
+        tests.add(Arguments.arguments(ANY, BIGINT, anyType));
+        tests.add(Arguments.arguments(ANY, VARCHAR, anyType));
+        tests.add(Arguments.arguments(ANY, CUSTOM_TYPE, anyType));
+
+        return tests.stream();
+    }
+
+    @Test
+    public void testCustomDataTypeLeastRestrictiveTypeForMoreThanTwoTypes() {
+        // no least restrictive type between ANY, a built-in type and a custom 
data type.
+        assertNull(TYPE_FACTORY.leastRestrictive(List.of(CUSTOM_TYPE, INTEGER, 
ANY)));
+        assertNull(TYPE_FACTORY.leastRestrictive(List.of(INTEGER, ANY, 
CUSTOM_TYPE)));
+        assertNull(TYPE_FACTORY.leastRestrictive(List.of(ANY, CUSTOM_TYPE, 
INTEGER)));
+    }
+
+    @ParameterizedTest
+    @MethodSource("types")
+    public void testLeastRestrictiveTypeForAnyAndMoreThanTwoTypes(RelDataType 
type) {
+        // Behaves the same as two argument version.
+        // Compatibility with default implementation.
+        assertEquals(ANY, TYPE_FACTORY.leastRestrictive(List.of(type, type, 
ANY)));
+        assertEquals(ANY, TYPE_FACTORY.leastRestrictive(List.of(type, ANY, 
type)));
+        assertEquals(ANY, TYPE_FACTORY.leastRestrictive(List.of(ANY, type, 
type)));
+    }
+
+    private static Stream<Arguments> types() {
         List<Arguments> tests = new ArrayList<>();
 
-        tests.add(Arguments.arguments(UUID, TINYINT, new 
LeastRestrictiveType(ANY)));
-        tests.add(Arguments.arguments(UUID, SMALLINT, new 
LeastRestrictiveType(ANY)));
-        tests.add(Arguments.arguments(UUID, INTEGER, new 
LeastRestrictiveType(ANY)));
-        tests.add(Arguments.arguments(UUID, FLOAT, new 
LeastRestrictiveType(ANY)));
-        tests.add(Arguments.arguments(UUID, REAL, new 
LeastRestrictiveType(ANY)));
-        tests.add(Arguments.arguments(UUID, DOUBLE, new 
LeastRestrictiveType(ANY)));
-        tests.add(Arguments.arguments(UUID, DECIMAL, new 
LeastRestrictiveType(ANY)));
-        tests.add(Arguments.arguments(UUID, BIGINT, new 
LeastRestrictiveType(ANY)));
-        tests.add(Arguments.arguments(UUID, VARCHAR, new 
LeastRestrictiveType(UUID)));
+        tests.add(Arguments.arguments(TINYINT));
+        tests.add(Arguments.arguments(SMALLINT));
+        tests.add(Arguments.arguments(INTEGER));
+        tests.add(Arguments.arguments(FLOAT));
+        tests.add(Arguments.arguments(REAL));
+        tests.add(Arguments.arguments(DOUBLE));
+        tests.add(Arguments.arguments(DECIMAL));
+        tests.add(Arguments.arguments(BIGINT));
+        tests.add(Arguments.arguments(VARCHAR));
+        tests.add(Arguments.arguments(CUSTOM_TYPE));
 
         return tests.stream();
     }
@@ -249,6 +315,10 @@ public class LeastRestrictiveTypesTest {
             this.relDataType = relDataType;
         }
 
+        private static LeastRestrictiveType none() {
+            return new LeastRestrictiveType((RelDataType) null);
+        }
+
         @Override
         public String toString() {
             return relDataType != null ? relDataType.toString() : "<none>";
@@ -270,4 +340,29 @@ public class LeastRestrictiveTypesTest {
         RelDataType actualType = 
TYPE_FACTORY.leastRestrictive(Arrays.asList(type1, type2));
         assertEquals(expectedType.relDataType, actualType, "leastRestrictive(" 
+ type1 + "," + type2 + ")");
     }
+
+    private static final class TestCustomType extends IgniteCustomType {
+
+        private static final IgniteCustomTypeSpec SPEC = new 
IgniteCustomTypeSpec("TestType",
+                NativeTypes.INT8, ColumnType.INT8, Byte.class,
+                IgniteCustomTypeSpec.getCastFunction(TestCustomType.class, 
"cast"));
+
+        private TestCustomType(boolean nullable) {
+            super(SPEC, nullable, -1);
+        }
+
+        @Override
+        public IgniteCustomType createWithNullability(boolean nullable) {
+            throw new AssertionError();
+        }
+
+        @Override
+        protected void generateTypeString(StringBuilder sb, boolean 
withDetail) {
+            sb.append("TestType");
+        }
+
+        public static byte cast(Object ignore) {
+            throw new AssertionError();
+        }
+    }
 }
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/TypeCoercionTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/TypeCoercionTest.java
index 8b2e0ae56b..27dd399b4e 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/TypeCoercionTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/TypeCoercionTest.java
@@ -44,9 +44,12 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParseException;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.ignite.internal.sql.engine.planner.AbstractPlannerTest;
 import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
+import org.apache.ignite.internal.sql.engine.type.IgniteCustomType;
+import 
org.apache.ignite.internal.sql.engine.type.IgniteCustomTypeCoercionRules;
 import org.apache.ignite.internal.sql.engine.type.UuidType;
 import org.apache.ignite.internal.tostring.S;
 import org.jetbrains.annotations.Nullable;
@@ -54,6 +57,7 @@ import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Disabled;
 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;
 import org.junit.jupiter.params.provider.ValueSource;
 
@@ -278,6 +282,41 @@ public class TypeCoercionTest extends AbstractPlannerTest {
         return rules.stream();
     }
 
+
+    @ParameterizedTest
+    @MethodSource("commonTypeForBinaryComparison")
+    public void 
testCommonTypeForBinaryComparisonForCustomDataTypes(RelDataType type1, 
RelDataType type2, RelDataType commonType) {
+        runTest("SELECT 1", (planner, ignore) -> {
+            SqlValidator validator = planner.validator();
+            IgniteTypeCoercion typeCoercion = new 
IgniteTypeCoercion(TYPE_FACTORY, validator);
+            RelDataType actualCommonType = 
typeCoercion.commonTypeForBinaryComparison(type1, type2);
+
+            assertEquals(commonType, actualCommonType);
+        });
+    }
+
+    private static Stream<Arguments> commonTypeForBinaryComparison() {
+        List<Arguments> arguments = new ArrayList<>();
+
+        // IgniteCustomType: test cases for common type in binary comparison 
between
+        // a custom data type and the types it can be converted from.
+
+        IgniteCustomTypeCoercionRules customTypeCoercionRules = 
TYPE_FACTORY.getCustomTypeCoercionRules();
+
+        for (String typeName : TYPE_FACTORY.getCustomTypeSpecs().keySet()) {
+            IgniteCustomType customType = 
TYPE_FACTORY.createCustomType(typeName);
+
+            for (SqlTypeName sourceTypeName : 
customTypeCoercionRules.canCastFrom(typeName)) {
+                RelDataType sourceType = 
TYPE_FACTORY.createSqlType(sourceTypeName);
+
+                arguments.add(Arguments.of(customType, sourceType, 
customType));
+                arguments.add(Arguments.of(sourceType, customType, 
customType));
+            }
+        }
+
+        return arguments.stream();
+    }
+
     private final class BinaryOpTypeCoercionTester {
 
         final TypeCoercionRule rule;


Reply via email to