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

amashenkov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 1df2f576f7 IGNITE-18799 Sql. Custom types. Implement a basic set of 
test cases for a custom data type (#1849)
1df2f576f7 is described below

commit 1df2f576f798003abc51c7c38b62a1a37d8955d7
Author: Max Zhuravkov <[email protected]>
AuthorDate: Tue Apr 25 11:47:02 2023 +0300

    IGNITE-18799 Sql. Custom types. Implement a basic set of test cases for a 
custom data type (#1849)
---
 .../ignite/internal/sql/engine/ItUuidTest.java     | 322 ----------------
 .../engine/datatypes/CustomDataTypeTestSpecs.java  |  75 ++++
 .../tests/BaseAggregateCustomDataTypeTest.java     | 148 +++++++
 .../datatypes/tests/BaseCustomDataTypeTest.java    | 223 +++++++++++
 .../datatypes/tests/BaseDmlCustomDataTypeTest.java | 132 +++++++
 .../tests/BaseExpressionCustomDataTypeTest.java    | 351 +++++++++++++++++
 .../tests/BaseIndexCustomDataTypeTest.java         | 220 +++++++++++
 .../tests/BaseJoinCustomDataTypeTest.java          | 132 +++++++
 .../tests/BaseQueryCustomDataTypeTest.java         | 349 +++++++++++++++++
 .../tests/BaseSetOpCustomDataTypeTest.java         | 161 ++++++++
 .../datatypes/tests/CustomDataTypeTestSpec.java    |  88 +++++
 .../engine/datatypes/tests/TestDataSamples.java    | 172 +++++++++
 .../engine/datatypes/tests/TestTypeArguments.java  | 425 +++++++++++++++++++++
 .../datatypes/tests/TestTypeArgumentsTest.java     | 118 ++++++
 .../engine/datatypes/uuid/ItUuidAggregateTest.java |  36 ++
 .../sql/engine/datatypes/uuid/ItUuidDmlTest.java   |  36 ++
 .../datatypes/uuid/ItUuidExpressionTest.java       |  74 ++++
 .../sql/engine/datatypes/uuid/ItUuidIndexTest.java |  39 ++
 .../sql/engine/datatypes/uuid/ItUuidJoinTest.java  |  36 ++
 .../sql/engine/datatypes/uuid/ItUuidQueryTest.java |  57 +++
 .../sql/engine/datatypes/uuid/ItUuidSetOpTest.java |  36 ++
 .../internal/sql/engine/util/QueryChecker.java     | 148 ++++++-
 22 files changed, 3040 insertions(+), 338 deletions(-)

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
deleted file mode 100644
index 4a24d93058..0000000000
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItUuidTest.java
+++ /dev/null
@@ -1,322 +0,0 @@
-/*
- * 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;
-
-import static org.apache.ignite.lang.IgniteStringFormatter.format;
-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;
-import org.apache.calcite.sql.SqlKind;
-import org.apache.ignite.lang.IgniteException;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
-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;
-
-/**
- * Tests for {@link org.apache.ignite.internal.sql.engine.type.UuidType} data 
type.
- */
-public class ItUuidTest extends ClusterPerClassIntegrationTest {
-
-    // UUID1 > UUID2
-    private static final UUID UUID_1 = 
UUID.fromString("fd10556e-fc27-4a99-b5e4-89b8344cb3ce");
-
-    private static final UUID UUID_2 = 
UUID.fromString("c67d4baf-564e-4abe-aad5-fcf078d178bf");
-
-    /**
-     * Drops all created tables.
-     */
-    @AfterAll
-    public void dropTables() {
-        dropAllTables();
-    }
-
-    @BeforeAll
-    public void createTables() {
-        sql("CREATE TABLE t(id INTEGER PRIMARY KEY, uuid_key UUID, uuid_key2 
UUID)");
-    }
-
-    @BeforeEach
-    public void cleanTables() {
-        sql("DELETE FROM t");
-    }
-
-    @Test
-    public void testUuidLiterals() {
-        UUID uuid1 = UUID.randomUUID();
-
-        assertQuery(format("SELECT CAST('{}' as UUID)", 
uuid1)).columnTypes(UUID.class).returns(uuid1).check();
-        assertQuery(format("SELECT '{}'::UUID", 
uuid1)).columnTypes(UUID.class).returns(uuid1).check();
-        // from a dynamic parameter
-        assertQuery("SELECT 
?").withParams(uuid1).columnTypes(UUID.class).returns(uuid1).check();
-    }
-
-    @Test
-    public void testTypeOf() {
-        UUID uuid1 = UUID.randomUUID();
-
-        assertQuery("SELECT typeof(CAST(? as 
UUID))").withParams(uuid1.toString()).returns("UUID").check();
-        assertQuery("SELECT 
typeof(?)").withParams(uuid1).returns("UUID").check();
-
-        // https://issues.apache.org/jira/browse/IGNITE-18761
-        // TypeOf can short-circuit only when its argument is a constant 
expression.
-        // assertThrows(RuntimeException.class, () -> {
-        //   assertQuery("SELECT typeof(CAST('%s' as 
UUID))").returns("UUID1").check();
-        // });
-    }
-
-    @Test
-    public void testUuidTypeComparison() {
-        checkUuidComparison(UUID_1, UUID_2);
-    }
-
-    @Test
-    public void testUuidInUpdates() {
-        sql("INSERT INTO t (id, uuid_key) VALUES (1, ?)", UUID_1);
-
-        // Update column UUID with an UUID value
-        assertQuery("UPDATE t SET uuid_key = ? WHERE 
id=1").withParams(UUID_2).returns(1L).check();
-        assertQuery("SELECT uuid_key FROM t WHERE 
id=1").columnTypes(UUID.class).returns(UUID_2).check();
-
-        // Insert column UUID with a string value are possible to allow CASTs 
from STRING to UUID.
-        sql("INSERT INTO t (id, uuid_key) VALUES (3, ?)", UUID_1.toString());
-
-        // Update column UUID with a string value are possible to allow CASTs 
from STRING to UUID.
-        assertQuery("UPDATE t SET uuid_key = ? WHERE 
id=1").withParams(UUID_1.toString()).returns(1L).check();
-        assertQuery("SELECT uuid_key FROM t WHERE 
id=1").columnTypes(UUID.class).returns(UUID_1).check();
-    }
-
-    @Test
-    public void testUuidInQueries() {
-        // cast from string
-        sql(format("INSERT INTO t (id, uuid_key) VALUES (1, CAST('{}' as 
UUID))", UUID_1));
-
-        assertQuery("SELECT uuid_key FROM t WHERE 
id=1").columnTypes(UUID.class).returns(UUID_1).check();
-
-        // parameter in a query
-        sql("INSERT INTO t (id, uuid_key) VALUES (2, ?)", UUID_2);
-        assertQuery("SELECT uuid_key FROM t WHERE 
id=2").columnTypes(UUID.class).returns(UUID_2).check();
-
-        // null value
-        sql("INSERT INTO t (id, uuid_key) VALUES (3, NULL)");
-        assertQuery("SELECT uuid_key FROM t WHERE 
id=3").columnTypes(UUID.class).returns(null).check();
-
-        // uuid in predicate
-        assertQuery("SELECT id FROM t WHERE uuid_key=? ORDER BY 
id").withParams(UUID_1).returns(1).check();
-
-        // UUID works in comparison operators
-        assertQuery("SELECT id, uuid_key FROM t WHERE uuid_key < ? ORDER BY 
id").withParams(UUID_1)
-                .returns(2, UUID_2)
-                .check();
-
-        assertQuery("SELECT id, uuid_key FROM t WHERE uuid_key > ? ORDER BY 
id").withParams(UUID_2)
-                .returns(1, UUID_1)
-                .check();
-
-        // Works in IN operator
-        assertQuery("SELECT id, uuid_key FROM t WHERE uuid_key IN (?, ?) ORDER 
BY id")
-                .withParams(UUID_1, UUID_2)
-                .returns(1, UUID_1)
-                .returns(2, UUID_2)
-                .check();
-    }
-
-    @Test
-    public void testBasicAggregates() {
-        sql(format("INSERT INTO t (id, uuid_key) VALUES (1, CAST('{}' as 
UUID))", UUID_1));
-        sql(format("INSERT INTO t (id, uuid_key) VALUES (2, CAST('{}' as 
UUID))", UUID_2));
-
-        assertQuery("SELECT COUNT(uuid_key) FROM t").returns(2L).check();
-        assertQuery("SELECT ANY_VALUE(uuid_key) FROM t").check();
-
-        assertQuery("SELECT MIN(uuid_key) FROM t").returns(UUID_2).check();
-        assertQuery("SELECT MAX(uuid_key) FROM t").returns(UUID_1).check();
-    }
-
-    @Test
-    public void testUuidCaseExpression() {
-        UUID other = UUID.randomUUID();
-
-        sql("INSERT INTO t (id, uuid_key) VALUES (1, ?), (2, ?), (3, NULL)", 
UUID_1, UUID_2);
-
-        // CASE WHEN <condition> THEN .. WHEN <condition2> THEN ... END
-        assertQuery("SELECT id, CASE WHEN uuid_key = ? THEN uuid_key ELSE ? 
END FROM t ORDER BY id ASC ")
-                .withParams(UUID_1, other)
-                .returns(1, UUID_1)
-                .returns(2, other)
-                .returns(3, other)
-                .check();
-
-        // CASE <boolean> WHEN ... END
-
-        var query = "SELECT id, CASE uuid_key WHEN uuid_key = ? THEN uuid_key 
ELSE ? END FROM t;";
-        var t = assertThrows(CalciteContextException.class, () -> sql(query, 
UUID_1, UUID_1));
-        assertThat(t.getMessage(), containsString("Invalid types for 
comparison: UUID = BOOLEAN"));
-    }
-
-    @ParameterizedTest
-    @MethodSource("binaryComparisonOperators")
-    public void testUuidInvalidOperationsAreRejected(String op) {
-        var query = format("SELECT ? {} 1", op);
-        var t = assertThrows(IgniteException.class, () -> sql(query, UUID_1));
-        assertThat(t.getMessage(), containsString("class java.util.UUID cannot 
be cast to class java.lang.Integer"));
-    }
-
-    private static Stream<String> binaryComparisonOperators() {
-        return SqlKind.BINARY_COMPARISON.stream()
-                // to support IS DISTINCT FROM/IS NOT DISTINCT FROM
-                .map(o -> o.sql.replace("_", " "));
-    }
-
-    @ParameterizedTest
-    @MethodSource("binaryArithmeticOperators")
-    public void testUuidUnsupportedOperators(String op) {
-        var query = format("SELECT CAST('{}' as UUID) {} CAST('{}' as UUID)", 
UUID_1, op, UUID_1);
-        var t = assertThrows(IgniteException.class, () -> sql(query));
-        var errorMessage = format("Invalid types for arithmetic: class 
java.util.UUID {} class java.util.UUID", op);
-        assertThat(t.getMessage(), containsString(errorMessage));
-    }
-
-    private static Stream<String> binaryArithmeticOperators() {
-        return Stream.of("+", "-", "/", "*");
-    }
-
-    @Test
-    public void testUuidInvalidCasts() {
-        // invalid cast from integer to UUID an error from expression runtime.
-        // A call to UuidFunctions::cast is generates by 
RexToLitTranslator::translateCast
-        // org.apache.ignite.internal.sql.engine.type.UuidFunctions.cast(int)
-        {
-            var t = assertThrows(IgniteException.class, () -> sql("SELECT 
CAST(1 as UUID)"));
-            assertThat(t.getMessage(), containsString("class java.lang.Integer 
cannot be cast to class java.util.UUID"));
-        }
-
-        // A call to UuidFunctions::cast is generated by 
ConverterUtils::convert
-        // invalid cast from integer to UUID
-        {
-            var t = assertThrows(RuntimeException.class, () -> sql("SELECT 
CAST(? as UUID)", 1));
-            assertThat(t.getMessage(), containsString("class java.lang.Integer 
cannot be cast to class java.util.UUID"));
-        }
-    }
-
-    @Test
-    public void testUuidTypeCoercion() {
-        assertQuery(format("SELECT CAST('{}' as UUID) = '{}'", UUID_1, 
UUID_1)).returns(true).check();
-        assertQuery(format("SELECT '{}' = CAST('{}' as UUID)", UUID_1, 
UUID_1)).returns(true).check();
-
-        assertQuery(format("SELECT '{}'::UUID = '{}'", UUID_1, 
UUID_1)).returns(true).check();
-        assertQuery(format("SELECT '{}'= '{}'::UUID", UUID_1, 
UUID_1)).returns(true).check();
-
-        // UUID / String
-        checkUuidComparison(UUID_1.toString(), UUID_2);
-
-        // String / UUID
-        checkUuidComparison(UUID_1, UUID_2.toString());
-    }
-
-    @Test
-    public void testCoalesce() {
-        assertQuery(format("SELECT COALESCE(NULL, '{}'::UUID)", 
UUID_1)).returns(UUID_1).check();
-
-        assertQuery(format("SELECT COALESCE('{}'::UUID, NULL)", 
UUID_1)).returns(UUID_1).check();
-    }
-
-    @Test
-    public void testTypeCoercionInDml() {
-        sql("INSERT INTO t (id, uuid_key) VALUES (1, ?)",  UUID_1.toString());
-
-        sql("UPDATE t SET uuid_key=? WHERE id=1", UUID_2.toString());
-
-        assertQuery("SELECT id, uuid_key FROM t WHERE id = 1")
-                .returns(1,  UUID_2)
-                .check();
-    }
-
-    private void checkUuidComparison(Object uuid1, Object uuid2) {
-        assertQuery("SELECT ? = ?").withParams(uuid1, 
uuid1).returns(true).check();
-        assertQuery("SELECT ? != ?").withParams(uuid1, 
uuid1).returns(false).check();
-
-        assertQuery("SELECT ? = ?").withParams(uuid1, 
uuid2).returns(false).check();
-        assertQuery("SELECT ? != ?").withParams(uuid1, 
uuid2).returns(true).check();
-
-        assertQuery("SELECT ? > ?").withParams(uuid2, 
uuid1).returns(false).check();
-        assertQuery("SELECT ? >= ?").withParams(uuid2, 
uuid1).returns(false).check();
-        assertQuery("SELECT ? < ?").withParams(uuid2, 
uuid1).returns(true).check();
-        assertQuery("SELECT ? <= ?").withParams(uuid2, 
uuid1).returns(true).check();
-
-        assertQuery("SELECT ? > ?").withParams(uuid1, 
uuid2).returns(true).check();
-        assertQuery("SELECT ? >= ?").withParams(uuid1, 
uuid2).returns(true).check();
-        assertQuery("SELECT ? < ?").withParams(uuid1, 
uuid2).returns(false).check();
-        assertQuery("SELECT ? <= ?").withParams(uuid1, 
uuid2).returns(false).check();
-    }
-
-    @Test
-    public void testRandomUuid() {
-        assertQuery("INSERT INTO t SELECT x, RAND_UUID(), RAND_UUID() FROM 
TABLE(SYSTEM_RANGE(0, 99))").returns(100L).check();
-        assertQuery("SELECT uuid_key FROM t").columnTypes(UUID.class).check();
-        assertQuery("SELECT COUNT(DISTINCT uuid_key) FROM 
t").returns(100L).check();
-        assertQuery("SELECT COUNT(DISTINCT uuid_key2) FROM 
t").returns(100L).check();
-        assertQuery("SELECT COUNT(*) FROM t WHERE uuid_key = 
uuid_key2").returns(0L).check();
-        assertQuery("SELECT COUNT(*) FROM t WHERE uuid_key = 
RAND_UUID()").returns(0L).check();
-        assertQuery("SELECT COUNT(*) FROM t WHERE uuid_key != 
uuid_key2").returns(100L).check();
-    }
-
-    @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-18931";)
-    public void testRandomUuidComparison() {
-        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/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/CustomDataTypeTestSpecs.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/CustomDataTypeTestSpecs.java
new file mode 100644
index 0000000000..219a982096
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/CustomDataTypeTestSpecs.java
@@ -0,0 +1,75 @@
+/*
+ * 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.datatypes;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+
+import java.util.UUID;
+import org.apache.calcite.sql.type.SqlTypeName;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.CustomDataTypeTestSpec;
+import org.apache.ignite.internal.sql.engine.datatypes.tests.TestDataSamples;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.apache.ignite.internal.sql.engine.type.UuidType;
+import org.apache.ignite.sql.ColumnType;
+
+/**
+ * Defines {@link CustomDataTypeTestSpec test specs} for data types.
+ */
+public final class CustomDataTypeTestSpecs {
+
+    /**
+     * Test spec for {@link UuidType UUID} data type.
+     */
+    public static final CustomDataTypeTestSpec<UUID> UUID_TYPE = new 
CustomDataTypeTestSpec<>(
+            ColumnType.UUID, UuidType.NAME, UUID.class, new UUID[]{new UUID(1, 
1), new UUID(2, 1), new UUID(3, 1)}) {
+
+        @Override
+        public boolean hasLiterals() {
+            return false;
+        }
+
+        @Override
+        public String toLiteral(UUID value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String toValueExpr(UUID value) {
+            return format("'{}'::UUID", value);
+        }
+
+        @Override
+        public String toStringValue(UUID value) {
+            return value.toString();
+        }
+
+        @Override
+        public TestDataSamples<UUID> createSamples(IgniteTypeFactory 
typeFactory) {
+            TestDataSamples.Builder<UUID> samples = TestDataSamples.builder();
+
+            samples.add(values, SqlTypeName.VARCHAR, String::valueOf);
+            samples.add(values, SqlTypeName.CHAR, String::valueOf);
+
+            return samples.build();
+        }
+    };
+
+    private CustomDataTypeTestSpecs() {
+
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseAggregateCustomDataTypeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseAggregateCustomDataTypeTest.java
new file mode 100644
index 0000000000..dfe416497f
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseAggregateCustomDataTypeTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.datatypes.tests;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test cases for aggregate functions for custom data type.
+ *
+ * @param <T> A storage type for a custom data type.
+ */
+public abstract class BaseAggregateCustomDataTypeTest<T extends Comparable<T>> 
extends BaseCustomDataTypeTest<T> {
+
+    /** {@code COUNT} aggregate function. */
+    @Test
+    public void testCount() {
+        insertValues();
+
+        checkQuery("SELECT COUNT(test_key) FROM t")
+                .returns((long) values.size())
+                .check();
+    }
+
+    /** {@code MIN} aggregate function. */
+    @Test
+    public void testMin() {
+        T min = orderedValues.first();
+
+        insertValues();
+
+        checkQuery("SELECT MIN(test_key) FROM t").returns(min).check();
+    }
+
+    /** {@code MAX} aggregate function. */
+    @Test
+    public void testMax() {
+        T max = orderedValues.last();
+
+        insertValues();
+
+        checkQuery("SELECT MAX(test_key) FROM t").returns(max).check();
+    }
+
+    /** {@code GROUP BY}.*/
+    @Test
+    public void testGroupBy() {
+        T min = orderedValues.first();
+        T mid = orderedValues.higher(min);
+        T max = orderedValues.last();
+
+        insertValues();
+
+        checkQuery("SELECT ANY_VALUE(id) as o, test_key FROM t GROUP BY 
test_key ORDER BY o")
+                .returns(1, min)
+                .returns(2, mid)
+                .returns(3, max)
+                .check();
+    }
+
+    /** {@code GROUP BY} {@code HAVING}. */
+    @ParameterizedTest
+    @MethodSource("having")
+    public void testGroupByHaving(TestTypeArguments<T> arguments) {
+        insertValues();
+
+        String query = format("SELECT ANY_VALUE(id), test_key FROM t GROUP BY 
test_key HAVING test_key = {}",
+                arguments.valueExpr(0));
+
+        checkQuery(query)
+                .returns(1, orderedValues.first())
+                .check();
+    }
+
+    private Stream<TestTypeArguments<T>> having() {
+        T min = orderedValues.first();
+
+        return TestTypeArguments.unary(testTypeSpec, dataSamples, min);
+    }
+
+    /** {@code SOME} aggregate function. */
+    @ParameterizedTest
+    @MethodSource("some")
+    public void testSome(TestTypeArguments<T> arguments) {
+        T min = orderedValues.first();
+        T mid = orderedValues.higher(min);
+        T max = orderedValues.last();
+
+        insertValues();
+
+        String query = format(
+                "SELECT test_key, SOME(test_key = {}) FROM t GROUP BY test_key 
ORDER BY test_key",
+                arguments.valueExpr(0));
+
+        checkQuery(query)
+                .returns(min, false)
+                .returns(mid, false)
+                .returns(max, true)
+                .check();
+    }
+
+    private Stream<TestTypeArguments<T>> some() {
+        T max = orderedValues.last();
+
+        return TestTypeArguments.unary(testTypeSpec, dataSamples, max);
+    }
+
+    /** {@code ANY_VALUE} aggregate function. */
+    @Test
+    public void testAnyValue() {
+        insertValues();
+
+        List<List<Object>> rows = runSql("SELECT ANY_VALUE(test_key) FROM t");
+        assertEquals(1, rows.size());
+
+        List<Object> row = rows.get(0);
+        assertThat(values, hasItem((T) row.get(0)));
+    }
+
+    private void insertValues() {
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseCustomDataTypeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseCustomDataTypeTest.java
new file mode 100644
index 0000000000..fbe60d2dbf
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseCustomDataTypeTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.datatypes.tests;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import java.util.NavigableSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Stream;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.ignite.internal.app.IgniteImpl;
+import org.apache.ignite.internal.sql.engine.AsyncSqlCursor;
+import org.apache.ignite.internal.sql.engine.ClusterPerClassIntegrationTest;
+import org.apache.ignite.internal.sql.engine.QueryProcessor;
+import org.apache.ignite.internal.sql.engine.type.IgniteCustomType;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem;
+import org.apache.ignite.internal.sql.engine.util.QueryChecker;
+import org.apache.ignite.internal.sql.engine.util.QueryChecker.QueryTemplate;
+import org.apache.ignite.sql.ColumnMetadata;
+import org.apache.ignite.sql.ColumnType;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.provider.Arguments;
+
+/**
+ * Base class for test cases for {@link IgniteCustomType custom data type}.
+ *
+ * <p>Usage:
+ * <ul>
+ *     <li>{@link #runSql(String, Object...)} should be used instead of {@link 
#sql(String, Object...)} methods.</li>
+ *     <li>{@link #checkQuery(String)} should be used instead of {@link 
#assertQuery(String)}.</li>
+ *     <li>if result set includes a column with name {@code test_key} it 
expects it to be of a storage type.</li>
+ * </ul>
+ *
+ * <p>Query string support the following template variables:
+ * <ul>
+ *     <li>{@code <type>} - an SQL name of custom data type.</li>
+ *     <li>{@code $N} - the {@code N-th} value from sample values (0-based), 
converted to an SQL expression by
+ *     {@link CustomDataTypeTestSpec#toValueExpr(Comparable)} call 
(0-based)</li>
+ *     <li>{@code $N_lit} - the {@code N-th} value from sample values 
(0-based) in form of an SQL literal.</li>
+ * </ul>
+ *
+ * @param <T> A storage type for a custom data type.
+ */
+public abstract class BaseCustomDataTypeTest<T extends Comparable<T>> extends 
ClusterPerClassIntegrationTest {
+
+    protected CustomDataTypeTestSpec<T> testTypeSpec;
+
+    protected List<T> values;
+
+    protected NavigableSet<T> orderedValues;
+
+    protected Class<?> storageType;
+
+    protected TestDataSamples<T> dataSamples;
+
+    @BeforeAll
+    public void createTables() {
+        testTypeSpec = getTypeSpec();
+        storageType = testTypeSpec.storageType();
+
+        dataSamples = testTypeSpec.createSamples(new 
IgniteTypeFactory(IgniteTypeSystem.INSTANCE));
+        orderedValues = dataSamples.ordered();
+        values = dataSamples.values();
+
+        runSql("CREATE TABLE t(id INTEGER PRIMARY KEY, test_key <type>)");
+    }
+
+    @BeforeEach
+    public void cleanTables() {
+        sql("DELETE FROM t");
+    }
+
+    /** Creates a test type spec. **/
+    protected abstract CustomDataTypeTestSpec<T> getTypeSpec();
+
+    /**
+     * Use this method instead of {@link #assertQuery(String)} because it 
replaces template variables.
+     *
+     * @param query A query .
+     * @return A {@code QueryChecker}.
+     * @see #assertQuery(String)
+     */
+    protected final QueryChecker checkQuery(String query) {
+        QueryTemplate queryTemplate = createQueryTemplate(query);
+
+        return new QueryChecker(null, queryTemplate) {
+            @Override
+            protected QueryProcessor getEngine() {
+                return ((IgniteImpl) CLUSTER_NODES.get(0)).queryEngine();
+            }
+
+            @Override
+            protected void checkMetadata(AsyncSqlCursor<?> cursor) {
+                Optional<ColumnMetadata> testKey = cursor.metadata().columns()
+                        .stream()
+                        .filter(c -> "test_key".equalsIgnoreCase(c.name()))
+                        .findAny();
+
+                testKey.ifPresent((c) -> {
+                    ColumnType columnType = testTypeSpec.columnType();
+                    String error = format(
+                            "test_key should have type {}. This can happen if 
a query returned a column ", columnType
+                    );
+                    assertEquals(c.type(), columnType, error);
+                });
+            }
+        };
+    }
+
+    /**
+     * Use this method instead of {@link #sql(String, Object...)} because it 
replaces every {@code <type>} with the name of a custom data
+     * type under test, and {@code $N} with corresponding values, where {@code 
N} is 1-indexed.
+     *
+     * @param query A query.
+     * @return A {@code QueryChecker}.
+     * @see #sql(String, Object...)
+     */
+    protected final List<List<Object>> runSql(String query, Object... params) {
+        QueryTemplate queryTemplate = createQueryTemplate(query);
+        String queryString = queryTemplate.createQuery();
+
+        return sql(queryString, params);
+    }
+
+    /**
+     * Creates a query template.
+     * <ul>
+     *     <li>{@code <type>} are replaced with a value of type name of {@link 
CustomDataTypeTestSpec}.</li>
+     *     <li>{@code $N_lit} are replaced with corresponding literals, where 
{@code N} is 1-indexed</li>
+     *     <li>{@code $N} are replaced with corresponding values, where {@code 
N} is 1-indexed</li>
+     * </ul>
+     */
+    protected QueryTemplate createQueryTemplate(String query) {
+        QueryTemplate parameterProvidingTemplate = new 
ParameterReplacingTemplate<>(testTypeSpec, query, values);
+        return createQueryTemplate(parameterProvidingTemplate, 
testTypeSpec.typeName());
+    }
+
+    private static QueryTemplate createQueryTemplate(QueryTemplate template, 
String typeName) {
+        return new QueryTemplate() {
+            @Override
+            public String originalQueryString() {
+                return template.originalQueryString();
+            }
+
+            @Override
+            public String createQuery() {
+                String templateQuery = template.createQuery();
+                return templateQuery.replace("<type>", typeName);
+            }
+        };
+    }
+
+    private static class ParameterReplacingTemplate<T extends Comparable<T>> 
implements QueryTemplate {
+
+        private final CustomDataTypeTestSpec<T> testTypeSpec;
+
+        private final String query;
+
+        private final List<T> values;
+
+        ParameterReplacingTemplate(CustomDataTypeTestSpec<T> spec, String 
query, List<T> values) {
+            this.testTypeSpec = spec;
+            this.query = query;
+            this.values = values;
+        }
+
+        @Override
+        public String originalQueryString() {
+            return query;
+        }
+
+        @Override
+        public String createQuery() {
+            String q = query;
+
+            for (var i = 0; i < values.size(); i++) {
+                T value = values.get(i);
+
+                if (testTypeSpec.hasLiterals()) {
+                    String literalValue = testTypeSpec.toLiteral(value);
+                    q = q.replace("$" + i + "_lit", literalValue);
+                }
+
+                String placeHolderValue = testTypeSpec.toValueExpr(value);
+                q = q.replace("$" + i, placeHolderValue);
+            }
+
+            return q;
+        }
+    }
+
+    protected final Stream<TestTypeArguments<T>> convertedFrom() {
+        return TestTypeArguments.unary(testTypeSpec, dataSamples, 
dataSamples.min())
+                .filter(args -> !Objects.equals(args.typeName(0), 
testTypeSpec.typeName()));
+    }
+
+    /** Returns binary operators as sql, enum name pairs. */
+    protected static Stream<Arguments> binaryComparisonOperators() {
+        return SqlKind.BINARY_COMPARISON.stream()
+                // to support IS DISTINCT FROM/IS NOT DISTINCT FROM
+                .map(o -> Arguments.of(o.sql.replace("_", " "), o.sql));
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseDmlCustomDataTypeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseDmlCustomDataTypeTest.java
new file mode 100644
index 0000000000..4dd04ce6f1
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseDmlCustomDataTypeTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.datatypes.tests;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.stream.Stream;
+import org.apache.calcite.runtime.CalciteContextException;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * A set of test cases for DML operations.
+ *
+ * @param <T> A storage type for a custom data type.
+ */
+public abstract class BaseDmlCustomDataTypeTest<T extends Comparable<T>> 
extends BaseCustomDataTypeTest<T> {
+
+    /** {@code INSERT}. */
+    @ParameterizedTest
+    @MethodSource("dml")
+    public void testInsert(TestTypeArguments<T> arguments) {
+        runSql(format("INSERT INTO t VALUES (1, {})", arguments.valueExpr(0)));
+
+        checkQuery("SELECT test_key FROM t WHERE 
id=1").returns(arguments.value(0)).check();
+    }
+
+    /** {@code INSERT} with dynamic parameters.*/
+    @ParameterizedTest
+    @MethodSource("dml")
+    public void testInsertDynamicParameter(TestTypeArguments<T> arguments) {
+        T value1 = arguments.value(0);
+
+        runSql("INSERT INTO t VALUES (1, ?)", arguments.argValue(0));
+
+        checkQuery("SELECT test_key FROM t WHERE 
id=1").returns(value1).check();
+    }
+
+    /** {@code DELETE} by key. */
+    @Test
+    public void testDelete() {
+        T value1 = values.get(0);
+
+        runSql("INSERT INTO t VALUES (1, $0)");
+        runSql("INSERT INTO t VALUES (2, $1)");
+        runSql("INSERT INTO t VALUES (3, $2)");
+
+        runSql("DELETE FROM t WHERE test_key=?", value1);
+
+        checkQuery("SELECT id FROM t").returns(2).returns(3).check();
+    }
+
+    /** {@code UPDATE}. */
+    @ParameterizedTest
+    @MethodSource("dml")
+    public void testUpdate(TestTypeArguments<T> arguments) {
+        String insert = format("INSERT INTO t VALUES (1, {})", 
arguments.valueExpr(0));
+        runSql(insert);
+
+        checkQuery("UPDATE t SET test_key = ? WHERE id=1")
+                .withParams(arguments.argValue(0))
+                .returns(1L)
+                .check();
+
+        checkQuery("SELECT test_key FROM t WHERE id=1")
+                .returns(arguments.value(0))
+                .check();
+    }
+
+    /** {@code UPDATE} with dynamic parameter.*/
+    @ParameterizedTest
+    @MethodSource("dml")
+    public void testUpdateWithDynamicParameter(TestTypeArguments<T> arguments) 
{
+        String insert = format("INSERT INTO t VALUES (1, {})", 
arguments.valueExpr(0));
+        runSql(insert);
+
+        checkQuery("UPDATE t SET test_key = ? WHERE id=1")
+                .withParams(arguments.argValue(0))
+                .returns(1L)
+                .check();
+
+        checkQuery("SELECT test_key FROM t WHERE id=1")
+                .returns(arguments.value(0))
+                .check();
+    }
+
+    /** Type mismatch in {@code INSERT}s {@code VALUES}.*/
+    @ParameterizedTest
+    @MethodSource("convertedFrom")
+    public void testDisallowMismatchTypesOnInsert(TestTypeArguments<T> 
arguments) {
+        var query = format("INSERT INTO t (id, test_key) VALUES (10, null), 
(20, {})", arguments.valueExpr(0));
+        var t = assertThrows(CalciteContextException.class, () -> 
runSql(query));
+        assertThat(t.getMessage(), containsString("Values passed to VALUES 
operator must have compatible types"));
+    }
+
+    /**
+     * Type mismatch in {@code INSERT}s {@code VALUES} with dynamic parameters.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-18831";)
+    public void testDisallowMismatchTypesOnInsertDynamicParam() {
+        T value1 = values.get(0);
+
+        var query = "INSERT INTO t (id, test_key) VALUES (1, null), (2, ?)";
+        var t = assertThrows(CalciteContextException.class, () -> 
runSql(query, value1));
+        assertThat(t.getMessage(), containsString("Values passed to VALUES 
operator must have compatible types"));
+    }
+
+    private Stream<TestTypeArguments<T>> dml() {
+        return TestTypeArguments.unary(testTypeSpec, dataSamples, 
values.get(0));
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseExpressionCustomDataTypeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseExpressionCustomDataTypeTest.java
new file mode 100644
index 0000000000..61ea1e3749
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseExpressionCustomDataTypeTest.java
@@ -0,0 +1,351 @@
+/*
+ * 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.datatypes.tests;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Objects;
+import java.util.stream.Stream;
+import org.apache.calcite.runtime.CalciteContextException;
+import org.apache.ignite.internal.sql.engine.util.QueryChecker;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test cases for custom data type in expressions.
+ *
+ * @param <T> A storage type for a custom data type.
+ */
+public abstract class BaseExpressionCustomDataTypeTest<T extends 
Comparable<T>> extends BaseCustomDataTypeTest<T> {
+
+    /**
+     * Binary comparison operator tests.
+     */
+    @ParameterizedTest
+    @MethodSource("cmp")
+    public void testComparison(TestTypeArguments<T> arguments) {
+        checkComparison(arguments, 0, 1);
+    }
+
+    /**
+     * Binary comparison operator tests with dynamic parameters.
+     */
+    @ParameterizedTest
+    @MethodSource("cmp")
+    public void testComparisonDynamicParameters(TestTypeArguments<T> 
arguments) {
+        Comparable<?> lower = (Comparable<?>) arguments.argValue(0);
+        Comparable<?> higher = (Comparable<?>) arguments.argValue(1);
+
+        checkComparisonWithDynamicParameters(higher, lower);
+    }
+
+    private Stream<TestTypeArguments<T>> cmp() {
+        return TestTypeArguments.binary(testTypeSpec, dataSamples, 
dataSamples.min(), dataSamples.max());
+    }
+
+    /**
+     * {@code IN} operator.
+     */
+    @ParameterizedTest
+    @MethodSource("in")
+    public void testIn(TestTypeArguments<T> arguments) {
+        String lhs = arguments.valueExpr(0);
+        String value2 = arguments.valueExpr(1);
+        String value3 = arguments.valueExpr(2);
+
+        String query = format("SELECT {} IN ({}, {})", lhs, value2, value3);
+        checkQuery(query).returns(true).check();
+    }
+
+    private Stream<TestTypeArguments<T>> in() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+
+        return TestTypeArguments.nary(testTypeSpec, dataSamples, value1, 
value1, value2);
+    }
+
+    /**
+     * {@code NOT IN} operator.
+     */
+    @ParameterizedTest
+    @MethodSource("notIn")
+    public void testNotIn(TestTypeArguments<T> arguments) {
+        String lhs = arguments.valueExpr(0);
+        String value1 = arguments.valueExpr(1);
+        String value2 = arguments.valueExpr(2);
+
+        String query = format("SELECT {} NOT IN ({}, {})", lhs, value1, 
value2);
+        checkQuery(query).returns(true).check();
+    }
+
+    private Stream<TestTypeArguments<T>> notIn() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        return TestTypeArguments.nary(testTypeSpec, dataSamples, value3, 
value1, value2);
+    }
+
+    /**
+     * {@code COALESCE} operator.
+     */
+    @Test
+    public void testCoalesce() {
+        T value1 = values.get(0);
+
+        checkQuery("SELECT COALESCE($0, $1)").returns(value1).check();
+
+        // Checks that NULL type is properly handled by by 
typeFactory::leastRestrictiveType
+        checkQuery("SELECT COALESCE(NULL, $0)").returns(value1).check();
+
+        checkQuery("SELECT COALESCE($0, NULL)").returns(value1).check();
+    }
+
+    /**
+     * {@code COALESCE} operator does not allow different types in its 
arguments.
+     */
+    @ParameterizedTest
+    @MethodSource("convertedFrom")
+    public void testCoalesceMissingTypesIsIllegal(TestTypeArguments arguments) 
{
+        CalciteContextException t = 
assertThrows(CalciteContextException.class, () -> {
+            checkQuery(format("SELECT COALESCE($0, {})", 
arguments.valueExpr(0))).check();
+        });
+
+        assertThat(t.getMessage(), containsString("Illegal mixing of types in 
CASE or COALESCE statement"));
+    }
+
+    /**
+     * {@code NULLIF} operator.
+     */
+    @Test
+    public void testNullIf() {
+        T value1 = values.get(0);
+
+        checkQuery("SELECT NULLIF($0, $1)")
+                .returns(value1)
+                .check();
+
+        checkQuery("SELECT NULLIF($0, $0)")
+                .returns(new Object[]{null})
+                .check();
+    }
+
+    /**
+     * {@code CASE WHEN <expr_with_custom_type> THEN .. ELSE .. END}.
+     */
+    @Test
+    public void testCaseInWhen() {
+        T value1 = values.get(0);
+
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+
+        String query = format("SELECT CASE WHEN test_key = $0 THEN id ELSE 0 
END FROM t ORDER BY id ASC", value1);
+        checkQuery(query)
+                .returns(1)
+                .returns(0)
+                .returns(0)
+                .check();
+    }
+
+    /**
+     * {@code CASE WHEN .. THEN <expr_with_custom_type> ELSE .. END}.
+     */
+    @Test
+    public void testCaseInResult() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+
+        String query = format(
+                "SELECT CASE WHEN id = 1 THEN test_key WHEN id = 2 THEN 
test_key ELSE $2 END FROM t ORDER BY id ASC",
+                value3
+        );
+        checkQuery(query)
+                .returns(value1)
+                .returns(value2)
+                .returns(value3)
+                .check();
+    }
+
+    /**
+     * The same as {@link #testCaseInResult()} but for "simple" form of case 
expression {@code CASE <expr> WHEN .. END}.
+     */
+    @Test
+    public void testSimpleCase() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+
+        String query = format(
+                "SELECT CASE id > 0 WHEN id = 1 THEN test_key WHEN id = 2 THEN 
test_key ELSE $2 END FROM t ORDER BY id ASC"
+        );
+        checkQuery(query)
+                .returns(value1)
+                .returns(value2)
+                .returns(value3)
+                .check();
+    }
+
+    /** Custom data type from string. **/
+    @ParameterizedTest
+    @MethodSource("convertedFrom")
+    public void testCastFrom(TestTypeArguments<T> arguments) {
+        String stringValue = arguments.stringValue(0);
+        T value = arguments.value(0);
+
+        checkQuery(format("SELECT CAST('{}' AS <type>)", 
stringValue)).returns(value).check();
+
+        // PG style cast
+        checkQuery(format("SELECT '{}'::<type>", 
stringValue)).returns(value).check();
+    }
+
+    /**
+     * {@code BETWEEN} operator.
+     */
+    @ParameterizedTest
+    @MethodSource("between")
+    public void testBetweenWithDynamicParameters(TestTypeArguments<T> 
arguments) {
+        checkQuery("SELECT ?::<type> BETWEEN ? AND ?")
+                .withParams(arguments.value(0), arguments.value(1), 
arguments.value(2))
+                .returns(true)
+                .check();
+    }
+
+    /**
+     * {@code BETWEEN} operator.
+     */
+    @ParameterizedTest
+    @MethodSource("between")
+    public void testBetweenWithLiterals(TestTypeArguments<T> arguments) {
+        Assumptions.assumeTrue(testTypeSpec.hasLiterals(), "BETWEEN only works 
for types that has literals");
+
+        String query = format("SELECT ?::<type> BETWEEN {} AND {}", 
arguments.argLiteral(1), arguments.argLiteral(2));
+
+        checkQuery(query)
+                .withParams(arguments.value(0))
+                .returns(true)
+                .check();
+    }
+
+    private Stream<TestTypeArguments<T>> between() {
+        T value1 = orderedValues.first();
+        T value2 = orderedValues.higher(value1);
+        T value3 = orderedValues.last();
+
+        return TestTypeArguments.nary(testTypeSpec, dataSamples, value2, 
value1, value3);
+    }
+
+    /**
+     * {@code TYPE_OF} function.
+     */
+    @Test
+    public void testTypeOf() {
+        T value = values.get(0);
+        String typeName = testTypeSpec.typeName();
+
+        checkQuery("SELECT typeof(CAST(? as 
<type>))").withParams(value.toString()).returns(typeName).check();
+        checkQuery("SELECT 
typeof(?)").withParams(value).returns(typeName).check();
+
+        // https://issues.apache.org/jira/browse/IGNITE-18761
+        // TypeOf can short-circuit only when its argument is a constant 
expression.
+        // assertThrows(RuntimeException.class, () -> {
+        //   checkQuery("SELECT typeof(CAST('%s' as 
<type>))").returns(typeSpec.typeName()).check();
+        // });
+    }
+
+    /**
+     * Runs binary comparison checks with dynamic parameters.
+     */
+    protected final void checkComparisonWithDynamicParameters(Comparable<?> 
high, Comparable<?> low) {
+        if (Objects.equals(high, low)) {
+            throw new IllegalArgumentException(format("Values must not be 
equal. Value1: {}, value2: {}", high, low));
+        }
+
+        checkQuery("SELECT ? = ?").withParams(high, 
high).returns(true).check();
+        checkQuery("SELECT ? != ?").withParams(high, 
high).returns(false).check();
+        checkQuery("SELECT ? <> ?").withParams(high, 
high).returns(false).check();
+
+        checkQuery("SELECT ? IS NOT DISTINCT FROM ?").withParams(high, 
high).returns(true).check();
+        checkQuery("SELECT ? IS DISTINCT FROM ?").withParams(high, 
high).returns(false).check();
+
+        checkQuery("SELECT ? = ?").withParams(high, 
low).returns(false).check();
+        checkQuery("SELECT ? != ?").withParams(high, 
low).returns(true).check();
+        checkQuery("SELECT ? <> ?").withParams(high, 
low).returns(true).check();
+
+        checkQuery("SELECT ? > ?").withParams(low, 
high).returns(false).check();
+        checkQuery("SELECT ? >= ?").withParams(low, 
high).returns(false).check();
+        checkQuery("SELECT ? < ?").withParams(low, high).returns(true).check();
+        checkQuery("SELECT ? <= ?").withParams(low, 
high).returns(true).check();
+
+        checkQuery("SELECT ? > ?").withParams(high, low).returns(true).check();
+        checkQuery("SELECT ? >= ?").withParams(high, 
low).returns(true).check();
+        checkQuery("SELECT ? < ?").withParams(high, 
low).returns(false).check();
+        checkQuery("SELECT ? <= ?").withParams(high, 
low).returns(false).check();
+    }
+
+    /**
+     * Runs binary comparison checks for given arguments.
+     */
+    protected final void checkComparison(TestTypeArguments<?> arguments, int 
lowIdx, int highIdx) {
+        String low = arguments.valueExpr(lowIdx);
+        String high = arguments.valueExpr(highIdx);
+
+        checkComparisonQuery(high, "=", high).returns(true).check();
+        checkComparisonQuery(high, "!=", high).returns(false).check();
+        checkComparisonQuery(high, "<>", high).returns(false).check();
+
+        checkComparisonQuery(high, "IS NOT DISTINCT FROM", 
high).returns(true).check();
+        checkComparisonQuery(high, "IS DISTINCT FROM", 
high).returns(false).check();
+
+        checkComparisonQuery(high, "=", low).returns(false).check();
+        checkComparisonQuery(high, "!=", low).returns(true).check();
+        checkComparisonQuery(high, "<>", low).returns(true).check();
+
+        checkComparisonQuery(low, ">", high).returns(false).check();
+        checkComparisonQuery(low, ">=", high).returns(false).check();
+
+        checkComparisonQuery(low, "<", high).returns(true).check();
+        checkComparisonQuery(low, "<=", high).returns(true).check();
+
+        checkComparisonQuery(high, ">", low).returns(true).check();
+        checkComparisonQuery(high, ">=", low).returns(true).check();
+
+        checkComparisonQuery(high, "<", low).returns(false).check();
+        checkComparisonQuery(high, "<=", low).returns(false).check();
+    }
+
+    private QueryChecker checkComparisonQuery(String lhsExpr, String op, 
String rhsExpr) {
+        String query = format("SELECT {} {} {}", lhsExpr, op, rhsExpr);
+        return checkQuery(query);
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseIndexCustomDataTypeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseIndexCustomDataTypeTest.java
new file mode 100644
index 0000000000..7f3f974daa
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseIndexCustomDataTypeTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.datatypes.tests;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+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.MethodSource;
+
+/**
+ * Tests for indexes with custom data type columns.
+ *
+ * @param <T> A storage type for a custom data type.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-19128";)
+public abstract class BaseIndexCustomDataTypeTest<T extends Comparable<T>> 
extends BaseCustomDataTypeTest<T> {
+
+    @BeforeAll
+    public void addIndexSimpleIndex() throws InterruptedException {
+        runSql("create index t_test_key_idx on t (test_key)");
+
+        waitForIndex("t_test_key_idx");
+    }
+
+    @BeforeEach
+    public void insertData() {
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+    }
+
+    /**
+     * Key lookup.
+     */
+    @Test
+    public void testKeyLookUp() {
+        T value1 = values.get(0);
+
+        checkQuery("SELECT * FROM t WHERE test_key = $0")
+                .returns(1, value1)
+                .check();
+
+        checkQuery("SELECT * FROM t WHERE test_key iS NOT DISTINCT FROM $0")
+                .returns(1, value1)
+                .check();
+    }
+
+    /**
+     * {@code not EQ} lookup.
+     */
+    @Test
+    public void testNotEqLookUp() {
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        checkQuery("SELECT * FROM t WHERE test_key != $0 ORDER BY id")
+                .returns(2, value2)
+                .returns(3, value3)
+                .check();
+
+        checkQuery("SELECT * FROM t WHERE test_key IS DISTINCT FROM $0")
+                .returns(2, value2)
+                .returns(3, value3)
+                .check();
+    }
+
+    /**
+     * Range lookup with one bound.
+     */
+    @Test
+    public void testRangeLookUpSingleBound() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        checkQuery("SELECT * FROM t WHERE test_key > $0 ORDER BY id")
+                .returns(2, value2)
+                .returns(3, value3)
+                .check();
+
+        checkQuery("SELECT * FROM t WHERE test_key >= $1 ORDER BY id")
+                .returns(2, value2)
+                .returns(3, value3)
+                .check();
+
+        checkQuery("SELECT * FROM t WHERE test_key < $2 ORDER BY id")
+                .returns(1, value1)
+                .returns(2, value2)
+                .check();
+
+        checkQuery("SELECT * FROM t WHERE test_key <= $2 ORDER BY id")
+                .returns(1, value1)
+                .returns(2, value2)
+                .returns(3, value3)
+                .check();
+    }
+
+
+    /**
+     * Range lookup with two bounds.
+     */
+    @Test
+    public void testRangeLookUpTwoBounds() {
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        checkQuery("SELECT * FROM t WHERE test_key > $0 AND test_key < $2 
ORDER BY id")
+                .returns(2, value2)
+                .check();
+
+        checkQuery("SELECT * FROM t WHERE test_key >= $1 AND test_key <= $2 
ORDER BY id")
+                .returns(2, value2)
+                .returns(3, value3)
+                .check();
+    }
+
+    /**
+     * {@code BETWEEN} operator in index.
+     */
+    @Test
+    public void testRangeLookUpBetween() {
+        Assumptions.assumeTrue(testTypeSpec.hasLiterals(), "BETWEEN only works 
for types that has literals");
+
+        T value2 = values.get(1);
+
+        checkQuery("SELECT * FROM t WHERE test_key > BETWEEN $0_lit AND $2_lit 
ORDER BY id")
+                .returns(2, value2)
+                .check();
+    }
+
+    /**
+     * Out of range lookup.
+     */
+    @Test
+    public void testOutOfRangeLookUp() {
+        checkQuery("SELECT * FROM t WHERE test_key < $0")
+                .returnNothing()
+                .check();
+    }
+
+    /**
+     * {@code IN} operator.
+     */
+    @Test
+    public void testInLookUp() {
+        T value1 = values.get(0);
+        T value3 = values.get(2);
+
+        checkQuery("SELECT * FROM t WHERE test_key IN ($0, $2) ORDER BY id")
+                .returns(1, value1)
+                .returns(3, value3)
+                .check();
+    }
+
+    /**
+     * {@code NOT IN} operator.
+     */
+    @Test
+    public void testNotInLookUp() {
+        T value2 = values.get(1);
+
+        checkQuery("SELECT * FROM t WHERE test_key NOT IN ($0, $2) ORDER BY 
id")
+                .returns(2, value2)
+                .check();
+    }
+
+    /**
+     * Compound index: primary key + custom data type.
+     */
+    @ParameterizedTest
+    @MethodSource("compoundIndex")
+    public void testCompoundIndex(TestTypeArguments<T> arguments) throws 
InterruptedException {
+        sql("drop index if exists t_test_key_pk_idx");
+        sql("create index if not exists t_test_key_pk_idx on t (id, 
test_key)");
+
+        waitForIndex("t_test_key_pk_idx");
+
+        runSql("insert into t values(100, $0)");
+
+        String query = format("select id, test_key from t where test_key = {} 
and id = 100", arguments.valueExpr(0));
+        checkQuery(query)
+                .returns(100, arguments.value(0))
+                .check();
+    }
+
+    /**
+     * Dynamic parameter in index search.
+     */
+    @Test
+    public void testIndexDynParam() {
+        assertQuery("SELECT * FROM t WHERE test_key=?")
+                .withParams(values.get(0))
+                .check();
+    }
+
+    private Stream<TestTypeArguments<T>> compoundIndex() {
+        return TestTypeArguments.unary(testTypeSpec, dataSamples, 
values.get(0));
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseJoinCustomDataTypeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseJoinCustomDataTypeTest.java
new file mode 100644
index 0000000000..8a8e19f4ff
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseJoinCustomDataTypeTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.datatypes.tests;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test cases for {@code JOIN} operator for a custom data type.
+ *
+ * @param <T> A storage type for a custom data type.
+ */
+public abstract class BaseJoinCustomDataTypeTest<T extends Comparable<T>> 
extends BaseCustomDataTypeTest<T> {
+
+    /** Creates join tables. */
+    @BeforeAll
+    public void createJoinTables() {
+        Stream<TestTypeArguments<T>> args = 
TestTypeArguments.unary(testTypeSpec, dataSamples, dataSamples.min());
+        args.forEach(arg -> {
+            String typeName = arg.typeName(0);
+            String createTable = format("create table t_join_{}(id integer 
primary key, test_key {})", typeName, typeName);
+            runSql(createTable);
+        });
+    }
+
+    @AfterEach
+    public void cleanJoinTables() {
+        Stream<TestTypeArguments<T>> args = 
TestTypeArguments.unary(testTypeSpec, dataSamples, dataSamples.min());
+        args.forEach(arg -> {
+            String typeName = arg.typeName(0);
+            String delete = format("DELETE FROM t_join_{}", typeName);
+            runSql(delete);
+        });
+    }
+
+    /** Equijoins. Joins with equality predicate in condition. */
+    @ParameterizedTest
+    @MethodSource("equiJoin")
+    public void testEquiJoin(TestTypeArguments<T> arguments, String joinType) {
+        String insert = format("INSERT INTO t_join_{} (id, test_key) VALUES(1, 
{})", arguments.typeName(0), arguments.valueExpr(0));
+        runSql(insert);
+
+        String join = format("SELECT * FROM t {} JOIN t_join_{} ON t.test_key 
= t_join_{}.test_key",
+                joinType, arguments.typeName(0), arguments.typeName(0));
+        checkQuery(join).check();
+    }
+
+    private Stream<Arguments> equiJoin() {
+        Stream<TestTypeArguments<T>> unary = 
TestTypeArguments.unary(testTypeSpec, dataSamples, dataSamples.min());
+
+        return unary.flatMap(arg -> {
+            return Stream.of(
+                    // Empty
+                    Arguments.of(arg, " "),
+                    Arguments.of(arg, "INNER"),
+                    Arguments.of(arg, "LEFT"),
+                    Arguments.of(arg, "RIGHT"),
+                    Arguments.of(arg, "FULL"));
+        });
+    }
+
+    /** Non equijoins. Joins with non equality predicate in condition. */
+    @ParameterizedTest
+    @MethodSource("nonEquiJoin")
+    public void testNonEquijoin(TestTypeArguments<T> arguments, String 
joinExpr) {
+        String insert1 = format("INSERT INTO t (id, test_key) VALUES(1, {})", 
arguments.valueExpr(0));
+        runSql(insert1);
+
+        String insert2 = format("INSERT INTO t_join_{} (id, test_key) 
VALUES(2, {})", arguments.typeName(1), arguments.valueExpr(1));
+        runSql(insert2);
+
+        String insert3 = format("INSERT INTO t_join_{} (id, test_key) 
VALUES(3, {})", arguments.typeName(2), arguments.valueExpr(2));
+        runSql(insert3);
+
+        String joinCondition = format(joinExpr, arguments.typeName(2));
+        String join = format("SELECT * FROM t {} t.test_key > 
t_join_{}.test_key", joinCondition, arguments.typeName(2));
+        checkQuery(join).check();
+    }
+
+    /** Antijoin. Join with {@code not equals} in condition. */
+    @ParameterizedTest
+    @MethodSource("nonEquiJoin")
+    public void testAntiJoin(TestTypeArguments<T> arguments, String joinExpr) {
+        String insert1 = format("INSERT INTO t (id, test_key) VALUES(1, {})", 
arguments.valueExpr(0));
+        runSql(insert1);
+
+        String insert2 = format("INSERT INTO t_join_{} (id, test_key) 
VALUES(2, {})", arguments.typeName(1), arguments.valueExpr(1));
+        runSql(insert2);
+
+        String insert3 = format("INSERT INTO t_join_{} (id, test_key) 
VALUES(3, {})", arguments.typeName(2), arguments.valueExpr(2));
+        runSql(insert3);
+
+        String joinCondition = format(joinExpr, arguments.typeName(2));
+        String join = format("SELECT * FROM t {} t.test_key != 
t_join_{}.test_key", joinCondition, arguments.typeName(2));
+        checkQuery(join).check();
+    }
+
+    private Stream<Arguments> nonEquiJoin() {
+        Stream<TestTypeArguments<T>> args = 
TestTypeArguments.nary(testTypeSpec, dataSamples, values.get(0), values.get(1), 
values.get(2));
+
+        return args.map(arg -> arg.withLabel(arg.typeName(0) + " " + 
arg.typeName(1))).flatMap(arg -> {
+            return Stream.of(
+                    Arguments.of(arg, "JOIN t_join_{} ON "),
+                    Arguments.of(arg, "INNER JOIN t_join_{} ON "),
+                    Arguments.of(arg, "LEFT JOIN t_join_{} ON "),
+                    Arguments.of(arg, "RIGHT JOIN t_join_{} ON "),
+                    Arguments.of(arg, "FULL JOIN t_join_{} ON "),
+                    Arguments.of(arg, ", t_join_{} WHERE "));
+        });
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseQueryCustomDataTypeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseQueryCustomDataTypeTest.java
new file mode 100644
index 0000000000..df989b2fca
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseQueryCustomDataTypeTest.java
@@ -0,0 +1,349 @@
+/*
+ * 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.datatypes.tests;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test cases for custom data type in {@code WHERE} clause of a query.
+ */
+public abstract class BaseQueryCustomDataTypeTest<T extends Comparable<T>> 
extends BaseCustomDataTypeTest<T> {
+
+    /** No predicate. */
+    @Test
+    public void testSelect() {
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+
+        checkQuery("SELECT * FROM t")
+                .columnTypes(Integer.class, storageType)
+                .check();
+    }
+
+    /** Query against an empty table. */
+    @Test
+    public void testSelectFromEmpty() {
+        checkQuery("SELECT id, test_key FROM t")
+                .columnTypes(Integer.class, storageType)
+                .returnNothing()
+                .check();
+    }
+
+    /** Test for equality predicate. */
+    @ParameterizedTest
+    @MethodSource("eq")
+    public void testEqCondition(TestTypeArguments<T> arguments) {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+
+        runSql("INSERT INTO t VALUES(1, ?)", value1);
+        runSql("INSERT INTO t VALUES(2, ?)", value2);
+        runSql("INSERT INTO t VALUES(3, ?)", value1);
+
+        String query = format("SELECT id FROM t where test_key = {} ORDER BY 
id", arguments.valueExpr(0));
+        checkQuery(query)
+                .returns(1)
+                .returns(3)
+                .check();
+
+        String query2 = format("SELECT id FROM t where test_key != {} ORDER BY 
id", arguments.valueExpr(0));
+        checkQuery(query2)
+                .returns(2)
+                .check();
+    }
+
+    /** Test for equality predicate with dynamic parameter. */
+    @ParameterizedTest
+    @MethodSource("eq")
+    public void testEqConditionWithDynamicParameters(TestTypeArguments<T> 
arguments) {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+
+        runSql("INSERT INTO t VALUES(1, ?)", value1);
+        runSql("INSERT INTO t VALUES(2, ?)", value2);
+        runSql("INSERT INTO t VALUES(3, ?)", value1);
+
+        checkQuery("SELECT id FROM t where test_key = ? ORDER BY id")
+                .withParams(arguments.value(0))
+                .returns(1)
+                .returns(3)
+                .check();
+
+        checkQuery("SELECT id FROM t where test_key != ? ORDER BY id")
+                .withParams(arguments.value(0))
+                .returns(2)
+                .check();
+    }
+
+    private Stream<TestTypeArguments<T>> eq() {
+        return TestTypeArguments.unary(testTypeSpec, dataSamples, 
values.get(0));
+    }
+
+    /** {@code IN} expression. */
+    @Test
+    public void testIn() {
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+
+        checkQuery("SELECT id FROM t where test_key IN ($0, $1)")
+                .returns(1)
+                .returns(2)
+                .check();
+    }
+
+    /** {@code IN} expression with dynamic parameters. */
+    @Test
+    public void testInWithDynamicParamsCondition() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+
+        // We are using casts because dynamic parameters inside IN expression 
can cause type inference failures
+        // See https://issues.apache.org/jira/browse/IGNITE-18924
+        checkQuery("SELECT id FROM t where test_key IN (?::<type>, ?::<type>)")
+                .withParams(value1, value2)
+                .returns(1)
+                .returns(2)
+                .check();
+    }
+
+    /** {@code NOT IN} expression with dynamic parameters. */
+    @Test
+    public void testNotInWithDynamicParamsCondition() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+
+        // We are using casts because inside IN expression cause type 
inference failures
+        // See https://issues.apache.org/jira/browse/IGNITE-18924
+        checkQuery("SELECT id FROM t where test_key NOT IN (?::<type>, 
?::<type>)")
+                .withParams(value1, value2)
+                .returns(3)
+                .check();
+    }
+
+    /** {@code NOT IN} expression. */
+    @Test
+    public void testNotIn() {
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+
+        checkQuery("SELECT id FROM t where test_key NOT IN ($0, $1)")
+                .returns(3)
+                .check();
+    }
+
+    /** {@code IN} operator with dynamic parameters. */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-18924";)
+    public void testInWithDynamicParamsConditionNoCasts() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+
+        runSql("INSERT INTO t VALUES(1, $0)");
+        runSql("INSERT INTO t VALUES(2, $1)");
+        runSql("INSERT INTO t VALUES(3, $2)");
+
+        checkQuery("SELECT id FROM t where test_key IN (?, ?)")
+                .withParams(value1, value2)
+                .returns(1)
+                .returns(2)
+                .check();
+    }
+
+    /** {@code BETWEEN} operator. */
+    @ParameterizedTest
+    @MethodSource("between")
+    public void testBetweenCondition(TestTypeArguments<T> arguments) {
+        T min = orderedValues.first();
+        T mid = orderedValues.higher(min);
+        T max = orderedValues.last();
+
+        runSql("INSERT INTO t VALUES(1, ?)", min);
+        runSql("INSERT INTO t VALUES(2, ?)", mid);
+        runSql("INSERT INTO t VALUES(3, ?)", max);
+
+        checkQuery("SELECT id FROM t WHERE test_key BETWEEN ? AND ? ORDER BY 
id")
+                .withParams(arguments.value(0), arguments.value(1))
+                .returns(1)
+                .returns(2)
+                .check();
+
+        checkQuery("SELECT id FROM t WHERE test_key BETWEEN ? AND ? ORDER BY 
id")
+                .withParams(arguments.value(1), arguments.value(2))
+                .returns(2)
+                .returns(3)
+                .check();
+    }
+
+    private Stream<TestTypeArguments<T>> between() {
+        T value1 = orderedValues.first();
+        T value2 = orderedValues.higher(value1);
+        T value3 = orderedValues.last();
+
+        return TestTypeArguments.nary(testTypeSpec, dataSamples, value1, 
value2, value3);
+    }
+
+    /** {@code IS NOT DISTINCT FROM} predicate.*/
+    @ParameterizedTest
+    @MethodSource("distinctFrom")
+    public void testIsNotDistinctFrom(TestTypeArguments<T> arguments) {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        runSql("INSERT INTO t VALUES(1, ?)", value1);
+        runSql("INSERT INTO t VALUES(2, ?)", value2);
+        runSql("INSERT INTO t VALUES(3, ?)", value3);
+
+        String query = format("SELECT id FROM t where test_key IS NOT DISTINCT 
FROM {}", arguments.valueExpr(0));
+        checkQuery(query)
+                .returns(2)
+                .check();
+    }
+
+    /**
+     * {@code IS NOT DISTINCT FROM} predicate in {@code WHERE clause} with 
dynamic parameters.
+     */
+    @ParameterizedTest
+    @MethodSource("distinctFrom")
+    public void 
testIsNotDistinctFromWithDynamicParameters(TestTypeArguments<T> arguments) {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        runSql("INSERT INTO t VALUES(1, ?)", value1);
+        runSql("INSERT INTO t VALUES(2, ?)", value2);
+        runSql("INSERT INTO t VALUES(3, ?)", value3);
+
+        checkQuery("SELECT id FROM t where test_key IS NOT DISTINCT FROM ?")
+                .withParams(arguments.value(0))
+                .returns(2)
+                .check();
+    }
+
+    /** {@code IS DISTINCT FROM} in {@code WHERE clause}. */
+    @ParameterizedTest
+    @MethodSource("distinctFrom")
+    public void testIsDistinctFrom(TestTypeArguments<T> arguments) {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        runSql("INSERT INTO t VALUES(1, ?)", value1);
+        runSql("INSERT INTO t VALUES(2, ?)", value2);
+        runSql("INSERT INTO t VALUES(3, ?)", value3);
+
+        String query = format("SELECT id FROM t where test_key IS DISTINCT 
FROM {}", arguments.valueExpr(0));
+
+        checkQuery(query)
+                .returns(1)
+                .returns(3)
+                .check();
+    }
+
+    private Stream<TestTypeArguments<T>> distinctFrom() {
+        return TestTypeArguments.unary(testTypeSpec, dataSamples, 
values.get(1));
+    }
+
+    /** Ascending ordering.*/
+    @Test
+    public void testAscOrdering() {
+        T min = orderedValues.first();
+        T mid = orderedValues.higher(min);
+        T max = orderedValues.last();
+
+        runSql("INSERT INTO t VALUES(1, ?)", min);
+        runSql("INSERT INTO t VALUES(2, ?)", mid);
+        runSql("INSERT INTO t VALUES(3, ?)", max);
+
+        checkQuery("SELECT id FROM t ORDER BY test_key ASC")
+                .returns(1)
+                .returns(2)
+                .returns(3)
+                .check();
+    }
+
+    /** Descending ordering. */
+    @Test
+    public void testDescOrdering() {
+        T min = orderedValues.first();
+        T mid = orderedValues.higher(min);
+        T max = orderedValues.last();
+
+        runSql("INSERT INTO t VALUES(1, ?)", min);
+        runSql("INSERT INTO t VALUES(2, ?)", mid);
+        runSql("INSERT INTO t VALUES(3, ?)", max);
+
+        checkQuery("SELECT id FROM t ORDER BY test_key 
DESC").returns(3).returns(2).returns(1).check();
+    }
+
+    /** Predicate in {@code WHERE} clause. */
+    @ParameterizedTest
+    @MethodSource("filter")
+    public void testFilter(TestTypeArguments<T> arguments) {
+        String query = format("SELECT id FROM t WHERE t.test_key > {}", 
arguments.valueExpr(0));
+
+        T value1 = orderedValues.first();
+        T value2 = orderedValues.last();
+
+        runSql("INSERT INTO t VALUES(1, ?)", value1);
+        runSql("INSERT INTO t VALUES(2, ?)", value2);
+
+        checkQuery(query)
+                .returns(2)
+                .check();
+    }
+
+    /** Predicate in {@code WHERE} clause with dynamic parameter. */
+    @ParameterizedTest
+    @MethodSource("filter")
+    public void testFilterWithDynamicParameters(TestTypeArguments<T> 
arguments) {
+        String query = format("SELECT id FROM t WHERE t.test_key > ?");
+
+        T value1 = orderedValues.first();
+        T value2 = orderedValues.last();
+
+        runSql("INSERT INTO t VALUES(1, ?)", value1);
+        runSql("INSERT INTO t VALUES(2, ?)", value2);
+
+        checkQuery(query)
+                .withParams(arguments.value(0))
+                .returns(2)
+                .check();
+    }
+
+    private Stream<TestTypeArguments<T>> filter() {
+        return TestTypeArguments.unary(testTypeSpec, dataSamples, 
dataSamples.min());
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseSetOpCustomDataTypeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseSetOpCustomDataTypeTest.java
new file mode 100644
index 0000000000..2fb19531c2
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseSetOpCustomDataTypeTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.datatypes.tests;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test cases for set operators for custom data type.
+ *
+ * @param <T> A storage type for a custom data type.
+ */
+public abstract class BaseSetOpCustomDataTypeTest<T extends Comparable<T>> 
extends BaseCustomDataTypeTest<T> {
+
+    /**
+     * {@code UNION} operator.
+     */
+    @Test
+    public void testUnion() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        insertValues();
+
+        checkQuery("SELECT id, test_key FROM (SELECT id, test_key FROM t UNION 
SELECT id, test_key FROM t) tmp ORDER BY id ASC")
+                .returns(1, value1)
+                .returns(2, value2)
+                .returns(3, value3)
+                .check();
+    }
+
+    /**
+     * {@code UNION ALL} operator.
+     */
+    @Test
+    public void testUnionAll() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        insertValues();
+
+        checkQuery("SELECT id, test_key FROM (SELECT id, test_key FROM t UNION 
ALL SELECT id, test_key FROM t) tmp ORDER BY id ASC")
+                .returns(1, value1)
+                .returns(1, value1)
+                .returns(2, value2)
+                .returns(2, value2)
+                .returns(3, value3)
+                .returns(3, value3)
+                .check();
+    }
+
+    /**
+     * {@code INTERSECT} operator.
+     */
+    @Test
+    public void testIntersect() {
+        T value1 = values.get(0);
+        T value3 = values.get(2);
+
+        insertValues();
+
+        String intercept = "SELECT id, test_key FROM ("
+                + "SELECT id, test_key FROM t INTERSECT SELECT id, test_key 
FROM t WHERE id IN (1, 3)"
+                + ") tmp ORDER BY id ASC";
+
+        checkQuery(intercept)
+                .returns(1, value1)
+                .returns(3, value3)
+                .check();
+    }
+
+    /**
+     * {@code INTERSECT} and {@code INTERSECT ALL} operators.
+     */
+    @Test
+    public void testIntersectAll() {
+        T value1 = values.get(0);
+        T value3 = values.get(2);
+
+        insertValues();
+
+        String interceptAll = "SELECT id, test_key FROM ("
+                + "SELECT id, test_key FROM t INTERSECT ALL SELECT id, 
test_key FROM t WHERE id IN (1, 1, 3, 3)"
+                + ") tmp ORDER BY id ASC";
+
+        checkQuery(interceptAll)
+                .returns(1, value1)
+                .returns(3, value3)
+                .check();
+    }
+
+    /**
+     * {@code EXCEPT} operator.
+     */
+    @Test
+    public void testExcept() {
+        T value2 = values.get(1);
+
+        insertValues();
+
+        String except = "SELECT id, test_key FROM ("
+                + "SELECT id, test_key FROM t EXCEPT SELECT id, test_key FROM 
t WHERE id IN (1, 3)"
+                + ") tmp ORDER BY id ASC";
+
+        checkQuery(except)
+                .returns(2, value2)
+                .check();
+
+        String exceptAll = "SELECT id, test_key FROM ("
+                + "SELECT id, test_key FROM t EXCEPT ALL SELECT id, test_key 
FROM t WHERE id IN (1, 1, 3, 3)"
+                + ") tmp ORDER BY id ASC";
+
+        checkQuery(exceptAll)
+                .returns(2, value2)
+                .check();
+    }
+
+    /**
+     * {@code EXCEPT ALL} operator.
+     */
+    @Test
+    public void testExceptAll() {
+        T value2 = values.get(1);
+
+        insertValues();
+
+        String exceptAll = "SELECT id, test_key FROM ("
+                + "SELECT id, test_key FROM t EXCEPT ALL SELECT id, test_key 
FROM t WHERE id IN (1, 1, 3, 3)"
+                + ") tmp ORDER BY id ASC";
+
+        checkQuery(exceptAll)
+                .returns(2, value2)
+                .check();
+    }
+
+    private void insertValues() {
+        T value1 = values.get(0);
+        T value2 = values.get(1);
+        T value3 = values.get(2);
+
+        runSql("INSERT INTO t VALUES(1, ?)", value1);
+        runSql("INSERT INTO t VALUES(2, ?)", value2);
+        runSql("INSERT INTO t VALUES(3, ?)", value3);
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/CustomDataTypeTestSpec.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/CustomDataTypeTestSpec.java
new file mode 100644
index 0000000000..22226a1486
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/CustomDataTypeTestSpec.java
@@ -0,0 +1,88 @@
+/*
+ * 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.datatypes.tests;
+
+import java.util.List;
+import org.apache.ignite.internal.sql.engine.type.IgniteCustomTypeSpec;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.apache.ignite.sql.ColumnType;
+
+/**
+ * {@link IgniteCustomTypeSpec} + its values + convenient methods.
+ */
+public abstract class CustomDataTypeTestSpec<T extends Comparable<T>> {
+
+    private final ColumnType columnType;
+
+    private final String typeName;
+
+    protected final List<T> values;
+
+    private final Class<?> storageType;
+
+    /** Constructor. */
+    public CustomDataTypeTestSpec(ColumnType columnType, String typeName, 
Class<T> javaType, T[] values) {
+        this.columnType = columnType;
+        this.typeName = typeName;
+        this.values = List.of(values);
+        this.storageType = ColumnType.columnTypeToClass(columnType);
+    }
+
+    /** {@link ColumnType}. */
+    public final ColumnType columnType() {
+        return columnType;
+    }
+
+    /** SQL type name. */
+    public final String typeName() {
+        return typeName;
+    }
+
+    /** Storage type. */
+    public final Class<?> storageType() {
+        return storageType;
+    }
+
+    /**
+     * Returns {@code true} if there is SQL literal syntax for this type.
+     */
+    public abstract boolean hasLiterals();
+
+    /**
+     * Produces a SQL literal for the given value.
+     *
+     * @param value A value.
+     * @return An SQL literal for the given value.
+     * @throws UnsupportedOperationException if this type does not have 
literal syntax.
+     */
+    public abstract String toLiteral(T value);
+
+    /**
+     * Produces an expression that is used to replace placeholder values 
({@code $N}). If a type has its own SQL literals, implementation of
+     * this method must call {@link #toLiteral(T)}.
+     *
+     * @param value A value.
+     * @return an SQL expression.
+     */
+    public abstract String toValueExpr(T value);
+
+    public abstract String toStringValue(T value);
+
+    /** Creates {@link TestDataSamples test samples} for the given type. */
+    public abstract TestDataSamples<T> createSamples(IgniteTypeFactory 
typeFactory);
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/TestDataSamples.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/TestDataSamples.java
new file mode 100644
index 0000000000..dc3418e26c
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/TestDataSamples.java
@@ -0,0 +1,172 @@
+/*
+ * 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.datatypes.tests;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.function.Function;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+/**
+ * Values used in tests.
+ */
+public final class TestDataSamples<T extends Comparable<T>> {
+
+    private final List<T> values;
+
+    private final TreeSet<T> orderedValues;
+
+    private final Map<T, Map<SqlTypeName, Object>> valueReprs;
+
+    private TestDataSamples(List<T> values, Map<T, Map<SqlTypeName, Object>> 
valueReprs) {
+        this.values = values;
+        this.orderedValues = new TreeSet<>(values);
+        this.valueReprs = valueReprs;
+
+        if (orderedValues.size() < 3) {
+            throw new IllegalArgumentException("There should be at least 3 
distinct values but got " + values);
+        }
+    }
+
+    /**
+     * Returns the smallest value among these samples.
+     */
+    public T min() {
+        return orderedValues.first();
+    }
+
+    /**
+     * Returns the largest value among these samples.
+     */
+    public T max() {
+        return orderedValues.last();
+    }
+
+    /**
+     * Returns an ordered view of these samples.
+     */
+    public NavigableSet<T> ordered() {
+        return orderedValues;
+    }
+
+    /**
+     * Returns these samples as a list.
+     **/
+    public List<T> values() {
+        return values;
+    }
+
+    /**
+     * Returns all representations of the given sample.
+     *
+     * @throws IllegalArgumentException if there is no such sample.
+     */
+    public Map<SqlTypeName, Object> get(Object value) {
+        return getReprMap(value);
+    }
+
+    /**
+     * Returns a representation of the given sample of the specified type.
+     *
+     * @throws IllegalArgumentException if there is no such sample or the 
there is no representation of this sample of the specified
+     *                                  type.
+     */
+    public Object get(Object value, SqlTypeName typeName) {
+        Map<SqlTypeName, Object> map = getReprMap(value);
+
+        Object representation = map.get(typeName);
+        if (representation == null) {
+            String error = format("No representation of {} type: {}. Types: 
{}", value, typeName, map.keySet());
+            throw new IllegalArgumentException(error);
+        }
+        return representation;
+    }
+
+    private Map<SqlTypeName, Object> getReprMap(Object value) {
+        Map<SqlTypeName, Object> map = valueReprs.get((T) value);
+        if (map == null) {
+            String error = format("No representation of {}. Samples: {}", 
value, valueReprs.keySet());
+            throw new IllegalArgumentException(error);
+        }
+        return map;
+    }
+
+    /**
+     * Returns a builder of {@link TestDataSamples}.
+     */
+    public static <T extends Comparable<T>> Builder<T> builder() {
+        return new Builder<>();
+    }
+
+    /**
+     * A builder of {@link TestDataSamples}.
+     */
+    public static final class Builder<T extends Comparable<T>> {
+
+        private final Map<T, Map<SqlTypeName, Object>> alts = new HashMap<>();
+
+        private final List<T> values = new ArrayList<>();
+
+        private Builder() {
+
+        }
+
+        /**
+         * Add a sample with.
+         */
+        public Builder<T> add(T value, SqlTypeName typeName, Object typeValue) 
{
+            Map<SqlTypeName, Object> vals = alts.computeIfAbsent(value, (key) 
-> new TreeMap<>());
+            if (vals.put(typeName, typeValue) != null) {
+                String error = format("Duplicate values can not be present: {} 
type: {}. Values: {}", value, typeName, values);
+                throw new IllegalArgumentException(error);
+            }
+
+            if (!values.contains(value)) {
+                values.add(value);
+            }
+
+            return this;
+        }
+
+        /**
+         * Adds the given values.
+         */
+        public Builder<T> add(Collection<T> values, SqlTypeName typeName, 
Function<T, Object> mapping) {
+            for (T value : values) {
+                add(value, typeName, mapping.apply(value));
+            }
+
+            return this;
+        }
+
+        /**
+         * Creates an instance of {@link TestDataSamples}.
+         **/
+        public TestDataSamples<T> build() {
+            return new TestDataSamples<>(values, alts);
+        }
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/TestTypeArguments.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/TestTypeArguments.java
new file mode 100644
index 0000000000..47f50e5ac3
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/TestTypeArguments.java
@@ -0,0 +1,425 @@
+/*
+ * 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.datatypes.tests;
+
+import java.lang.reflect.Array;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Stream;
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Provides method for generating arguments for {@link BaseCustomDataTypeTest 
custom data type tests}.
+ *
+ * @param <T> A storage type for a custom data type.
+ */
+public final class TestTypeArguments<T extends Comparable<T>> {
+
+    private final List<Argument<T>> args;
+
+    private final String label;
+
+    TestTypeArguments(Argument<T>... args) {
+        this(List.of(args), null);
+    }
+
+    private TestTypeArguments(List<Argument<T>> args, @Nullable String label) {
+        this.args = args;
+
+        if (label == null) {
+            StringBuilder sb = new StringBuilder();
+
+            for (var arg : args) {
+                if (sb.length() > 0) {
+                    sb.append(" ");
+                }
+                sb.append(arg.typeName);
+            }
+
+            sb.append(":");
+
+            for (var arg : args) {
+                sb.append(" ");
+                sb.append(arg.value);
+            }
+
+            this.label = sb.toString();
+        } else {
+            this.label = label;
+        }
+    }
+
+
+    /**
+     * Returns an original value of the {@code i-th} argument.
+     */
+    public T value(int i) {
+        return getArg(i).value();
+    }
+
+    /**
+     * Returns a SQL type name of the {@code i-th} argument.
+     */
+    public String typeName(int i) {
+        Argument<T> arg = getArg(i);
+        return arg.typeName();
+    }
+
+    /**
+     * Returns a java value of the {@code i-th} argument.
+     */
+    public Object argValue(int i) {
+        Argument<T> arg = getArg(i);
+        return arg.argValue();
+    }
+
+    /**
+     * An SQL literal expression of the {@code i-th} argument.
+     *
+     * @throws UnsupportedOperationException if there is no literal for the 
{@code i-th} argument.
+     */
+    public String argLiteral(int i) {
+        Argument<T> arg = getArg(i);
+        return arg.argLiteral();
+    }
+
+    /**
+     * Returns an SQL expression that produces a value of i-th argument.
+     *
+     * <p>Default implementation returns: {@code CAST('value' AS <type>)}.
+     */
+    public String valueExpr(int i) {
+        Argument<T> arg = getArg(i);
+        return arg.valueExpr();
+    }
+
+    /** Returns string representation of the i-th argument. **/
+    public String stringValue(int i) {
+        Argument<T> arg = getArg(i);
+        return arg.stringValue();
+    }
+
+    /**
+     * Creates a copy of these arguments with the given label.
+     *
+     * @param label A value used by {@link #toString()} method.
+     * @return a copy of these arguments with a new label.
+     */
+    public TestTypeArguments<T> withLabel(String label) {
+        return new TestTypeArguments<>(args, label);
+    }
+
+    /** {@inheritDoc} **/
+    @Override
+    public String toString() {
+        return label;
+    }
+
+    private Argument<T> getArg(int index) {
+        return args.get(index);
+    }
+
+
+    /**
+     * Creates a stream of arguments for an unary operator.
+     *
+     * <p>For a sample value {@code V} of type {@code T} and its 
representations {@code (R1, R_type1)} and {@code (R2, R_type2)}.
+     *
+     * <p>This method produces the following results:
+     * <pre>
+     *     value | typeName | argument |
+     *     ------+----------+----------+
+     *     V     | T        | V        |
+     *     V     | R_type1  | R1       |
+     *     V     | R_type2  | R2       |
+     * </pre>
+     */
+    public static <T extends Comparable<T>> Stream<TestTypeArguments<T>> 
unary(CustomDataTypeTestSpec<T> testTypeSpec,
+            TestDataSamples<T> samples, T value) {
+
+        List<TestTypeArguments<T>> result = new ArrayList<>();
+        Argument<T> argument = createArgument(testTypeSpec, 
testTypeSpec.typeName(), value, value);
+        result.add(new TestTypeArguments<>(argument));
+
+        for (var entry : samples.get(value).entrySet()) {
+            String typeName = entry.getKey().getName();
+            Argument<T> anotherArgument = createArgument(testTypeSpec, 
typeName, value, entry.getValue());
+
+            result.add(new TestTypeArguments<>(anotherArgument));
+        }
+
+        return result.stream();
+    }
+
+    /**
+     * Creates a stream of arguments for a binary operator.
+     *
+     * <p>For sample values {@code V1}, and {@code V2} of type {@code T} and 
their representations:
+     * <ul>
+     *     <li>{@code V1}: {@code (R1_1, R_type1)} and {@code (R1_2, 
R_type2)}</li>
+     *     <li>{@code V2}: {@code (R2_1, R_type1)} and {@code (R2_2, 
R_type2)}</li>
+     * </ul>
+     *
+     * <p>This method produces the following results:
+     * <pre>
+     *     value(0) | typeName(0) | argument(0) |
+     *     ---------+-------------+-------------+
+     *     V1       | T           | V1          |
+     *     V1       | R_type1     | R1_1        |
+     *     V1       | R_type2     | R1_2        |
+     *     ---------+-------------+-------------+
+     *     value(1) | typeName(1) | argument(1) |
+     *     ---------+-------------+-------------+
+     *     V2       | T           | V2          |
+     *     V2       | R_type1     | R2_1        |
+     *     V2       | R_type2     | R2_2        |
+     * </pre>
+     */
+    public static <T extends Comparable<T>> Stream<TestTypeArguments<T>> 
binary(
+            CustomDataTypeTestSpec<T> typeSpec, TestDataSamples<T> samples, T 
lhs, T rhs) {
+
+        Map<SqlTypeName, Object> value1s = samples.get(lhs);
+        Map<SqlTypeName, Object> value2s = samples.get(rhs);
+
+        Argument<T> lhsArg = createArgument(typeSpec, typeSpec.typeName(), 
lhs, lhs);
+        Argument<T> rhsArg = createArgument(typeSpec, typeSpec.typeName(), 
rhs, rhs);
+
+        List<TestTypeArguments<T>> result = new ArrayList<>();
+        result.add(new TestTypeArguments<>(lhsArg, rhsArg));
+
+        for (var typeValue : value2s.entrySet()) {
+            Argument<T> rhs1 = createArgument(typeSpec, 
typeValue.getKey().toString(), rhs, typeValue.getValue());
+
+            result.add(new TestTypeArguments<>(lhsArg, rhs1));
+        }
+
+        for (var typeValue : value1s.entrySet()) {
+            Argument<T> lhs1 = createArgument(typeSpec, 
typeValue.getKey().toString(), lhs, typeValue.getValue());
+
+            result.add(new TestTypeArguments<>(lhs1, rhsArg));
+        }
+
+        return result.stream();
+    }
+
+    /**
+     * Creates a stream of arguments for a n-ary operator or an operator that 
have more than one argument for its right-hand side, eg.
+     * {@code IN}.
+     *
+     * <p>For sample values {@code V1}, {@code V2}, and {@code V3} of type 
{@code T} and their representations:
+     * <ul>
+     *     <li>{@code V1}: {@code (R1_1, R_type1)} and {@code (R1_2, 
R_type2)}</li>
+     *     <li>{@code V2}: {@code (R2_1, R_type1)} and {@code (R2_2, 
R_type2)}</li>
+     *     <li>{@code V3}: {@code (R3_1, R_type1)} and {@code (R3_2, 
R_type2)}</li>
+     * </ul>
+     *
+     * <p>This method produces the following results:
+     * <pre>
+     *     value(0) | typeName(0) | argument(0) |
+     *     ---------+-------------+-------------+
+     *     V1       | T           | V1          |
+     *     V1       | R_type1     | R1_1        |
+     *     V1       | R_type2     | R1_2        |
+     *     ---------+-------------+-------------+
+     *     value(1) | typeName(1) | argument(1) |
+     *     ---------+-------------+-------------+
+     *     V2       | T           | V2          |
+     *     V2       | R_type1     | R2_1        |
+     *     V2       | R_type2     | R2_2        |
+     *     ---------+-------------+-------------+
+     *     value(2) | typeName(2) | argument(1) |
+     *     ---------+-------------+-------------+
+     *     V3       | T           | V3          |
+     *     V3       | R_type1     | R3_1        |
+     *     V3       | R_type2     | R3_2        |
+     * </pre>
+     */
+    @SafeVarargs
+    public static <T extends Comparable<T>> Stream<TestTypeArguments<T>> 
nary(CustomDataTypeTestSpec<T> typeSpec,
+            TestDataSamples<T> samples, T lhs, T... rhs) {
+
+        if (rhs.length < 1) {
+            throw new IllegalArgumentException("Right-hand side must have at 
least one argument");
+        }
+
+        List<TestTypeArguments<T>> result = new ArrayList<>(rhs.length + 1);
+
+        Argument<T>[] first = (Argument<T>[]) 
Array.newInstance(Argument.class, rhs.length + 1);
+        first[0] = createArgument(typeSpec, typeSpec.typeName(), lhs, lhs);
+
+        for (int i = 1; i <= rhs.length; i++) {
+            T rhsValue = rhs[i - 1];
+            first[i] = createArgument(typeSpec, typeSpec.typeName(), rhsValue, 
rhsValue);
+        }
+        result.add(new TestTypeArguments<>(first));
+
+        for (var entry : samples.get(first[0].value()).entrySet()) {
+            Argument[] args = new Argument[rhs.length + 1];
+            args[0] = first[0];
+
+            for (int i = 1; i <= rhs.length; i++) {
+                T val = rhs[i - 1];
+                Object otherValue = samples.get(val, entry.getKey());
+                args[i] = createArgument(typeSpec, entry.getKey().getName(), 
val, otherValue);
+            }
+
+            result.add(new TestTypeArguments<T>(args));
+        }
+
+        return result.stream();
+    }
+
+    private static final class Argument<T extends Comparable<T>> implements 
Comparable<Argument<T>> {
+
+        private final String typeName;
+
+        private final T value;
+
+        private final Object argument;
+
+        private final String valueExpr;
+
+        private final String stringValue;
+
+        private final String literal;
+
+        Argument(String typeName, T value, Object argument, String valueExpr, 
String stringValue, @Nullable String literal) {
+            this.typeName = typeName;
+            this.value = value;
+            this.argument = argument;
+            this.valueExpr = valueExpr;
+            this.stringValue = stringValue;
+            this.literal = literal;
+        }
+
+        /** Original value. **/
+        T value() {
+            return value;
+        }
+
+        /** SQL type name of this argument. **/
+        String typeName() {
+            return typeName;
+        }
+
+        /** Java value of an argument. */
+        Object argValue() {
+            return argument;
+        }
+
+        /** Returns an SQL expression that produces a value of this argument. 
**/
+        String valueExpr() {
+            return valueExpr;
+        }
+
+        /** Returns a string representation of this argument. **/
+        String stringValue() {
+            return stringValue;
+        }
+
+        /**
+         * Returns this argument as SQL literal.
+         *
+         * @throws UnsupportedOperationException if there is no literal for 
this type.
+         */
+        public String argLiteral() {
+            if (literal == null) {
+                throw new UnsupportedOperationException("There is no literal 
expression for type {}. Use testType.hasLiteral()");
+            }
+            return literal;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int compareTo(Argument<T> o) {
+            return value.compareTo(o.value);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Argument<?> argument = (Argument<?>) o;
+            return value.equals(argument.value);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int hashCode() {
+            return Objects.hash(value);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String toString() {
+            return typeName + " " + value;
+        }
+    }
+
+
+    private static <T extends Comparable<T>> Argument<T> 
createArgument(CustomDataTypeTestSpec<T> testTypeSpec,
+            String typeName, T value, Object argument) {
+
+        String valueExpr;
+        String stringValue;
+
+        if (!typeName.equals(testTypeSpec.typeName())) {
+            SqlLiteral sqlLiteral;
+
+            if (argument instanceof Number) {
+                if (argument instanceof BigInteger
+                        || argument instanceof Long
+                        || argument instanceof Integer
+                        || argument instanceof Short
+                        || argument instanceof Byte) {
+
+                    sqlLiteral = 
SqlLiteral.createExactNumeric(argument.toString(), SqlParserPos.ZERO);
+                } else {
+                    sqlLiteral = 
SqlLiteral.createApproxNumeric(argument.toString(), SqlParserPos.ZERO);
+                }
+            } else if (argument instanceof String) {
+                sqlLiteral = SqlLiteral.createCharString(argument.toString(), 
"UTF-8", SqlParserPos.ZERO);
+            } else if (argument instanceof Boolean) {
+                sqlLiteral = SqlLiteral.createBoolean((Boolean) argument, 
SqlParserPos.ZERO);
+            } else {
+                throw new IllegalArgumentException("Unsupported literal: " + 
value);
+            }
+
+            valueExpr = sqlLiteral.toString();
+            stringValue = argument.toString();
+        } else {
+            valueExpr = testTypeSpec.toValueExpr(value);
+            stringValue = testTypeSpec.toStringValue(value);
+        }
+
+        String literal = testTypeSpec.hasLiterals() ? 
testTypeSpec.toLiteral(value) : null;
+
+        return new Argument<>(typeName, value, argument, valueExpr, 
stringValue, literal);
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/TestTypeArgumentsTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/TestTypeArgumentsTest.java
new file mode 100644
index 0000000000..569dc8b187
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/TestTypeArgumentsTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.datatypes.tests;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem;
+import org.apache.ignite.sql.ColumnType;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link TestTypeArguments}.
+ */
+public class TestTypeArgumentsTest {
+
+    private final TestType type = new TestType();
+
+    private final TestDataSamples<String> samples = type.createSamples(new 
IgniteTypeFactory(IgniteTypeSystem.INSTANCE));
+
+    /**
+     * Tests for {@link TestTypeArguments#unary(CustomDataTypeTestSpec, 
TestDataSamples, Comparable)}.
+     */
+    @Test
+    public void testUnary() {
+        List<String> args = TestTypeArguments.unary(type, samples, 
"1").map(TestTypeArguments::toString).collect(Collectors.toList());
+
+        List<String> expected = List.of("TestType: 1", "INTEGER: 1", "BIGINT: 
1");
+        assertEquals(expected, args);
+    }
+
+    /**
+     * Tests for {@link TestTypeArguments#binary(CustomDataTypeTestSpec, 
TestDataSamples, Comparable, Comparable)}.
+     */
+    @Test
+    public void testBinary() {
+        List<String> args = TestTypeArguments.binary(type, samples, "1", "2")
+                .map(TestTypeArguments::toString)
+                .collect(Collectors.toList());
+
+        List<String> expected = List.of("TestType TestType: 1 2",
+                "TestType INTEGER: 1 2", "TestType BIGINT: 1 2",
+                "INTEGER TestType: 1 2", "BIGINT TestType: 1 2");
+        assertEquals(expected, args);
+    }
+
+    /**
+     * Tests for {@link TestTypeArguments#nary(CustomDataTypeTestSpec, 
TestDataSamples, Comparable, Comparable[])}.
+     */
+    @Test
+    public void testNary() {
+        List<String> args = TestTypeArguments.nary(type, samples, "1", "2", 
"3")
+                .map(TestTypeArguments::toString)
+                .collect(Collectors.toList());
+
+        List<String> expected = List.of(
+                "TestType TestType TestType: 1 2 3",
+                "TestType INTEGER INTEGER: 1 2 3",
+                "TestType BIGINT BIGINT: 1 2 3");
+        assertEquals(expected, args);
+    }
+
+    private static final class TestType extends CustomDataTypeTestSpec<String> 
{
+
+        private static final String[] VALUES = {"1", "2", "3"};
+
+        TestType() {
+            super(ColumnType.INT8, "TestType", String.class, VALUES);
+        }
+
+        @Override
+        public boolean hasLiterals() {
+            return false;
+        }
+
+        @Override
+        public String toLiteral(String value) {
+            return format("'{}'", value);
+        }
+
+        @Override
+        public String toValueExpr(String value) {
+            return format("'{}'::VARCHAR");
+        }
+
+        @Override
+        public String toStringValue(String value) {
+            return value;
+        }
+
+        @Override
+        public TestDataSamples<String> createSamples(IgniteTypeFactory 
typeFactory) {
+            return TestDataSamples.<String>builder()
+                    .add(List.of(VALUES), SqlTypeName.INTEGER, String::valueOf)
+                    .add(List.of(VALUES), SqlTypeName.BIGINT, String::valueOf)
+                    .build();
+        }
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidAggregateTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidAggregateTest.java
new file mode 100644
index 0000000000..ed9b514b4c
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidAggregateTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.datatypes.uuid;
+
+import java.util.UUID;
+import org.apache.ignite.internal.sql.engine.datatypes.CustomDataTypeTestSpecs;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.BaseAggregateCustomDataTypeTest;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.CustomDataTypeTestSpec;
+import org.apache.ignite.internal.sql.engine.type.UuidType;
+
+/**
+ * Tests for aggregates for {@link UuidType UUID data type}.
+ */
+public class ItUuidAggregateTest extends BaseAggregateCustomDataTypeTest<UUID> 
{
+
+    /** {@inheritDoc} **/
+    @Override
+    protected CustomDataTypeTestSpec<UUID> getTypeSpec() {
+        return CustomDataTypeTestSpecs.UUID_TYPE;
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidDmlTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidDmlTest.java
new file mode 100644
index 0000000000..db2c060e8b
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidDmlTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.datatypes.uuid;
+
+import java.util.UUID;
+import org.apache.ignite.internal.sql.engine.datatypes.CustomDataTypeTestSpecs;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.BaseDmlCustomDataTypeTest;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.CustomDataTypeTestSpec;
+import org.apache.ignite.internal.sql.engine.type.UuidType;
+
+/**
+ * Tests for DML operators for {@link UuidType UUID data type}.
+ */
+public class ItUuidDmlTest extends BaseDmlCustomDataTypeTest<UUID> {
+
+    /** {@inheritDoc} **/
+    @Override
+    protected CustomDataTypeTestSpec<UUID> getTypeSpec() {
+        return CustomDataTypeTestSpecs.UUID_TYPE;
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidExpressionTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidExpressionTest.java
new file mode 100644
index 0000000000..5c713b27d8
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidExpressionTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.datatypes.uuid;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.UUID;
+import org.apache.ignite.internal.sql.engine.datatypes.CustomDataTypeTestSpecs;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.BaseExpressionCustomDataTypeTest;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.CustomDataTypeTestSpec;
+import org.apache.ignite.internal.sql.engine.type.UuidType;
+import org.apache.ignite.lang.IgniteException;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for expressions for {@link UuidType UUID data type}.
+ */
+public class ItUuidExpressionTest extends 
BaseExpressionCustomDataTypeTest<UUID> {
+
+    /** {@code RAND_UUID} function.*/
+    @Test
+    public void testRand() {
+        checkQuery("SELECT RAND_UUID()").columnTypes(UUID.class).check();
+    }
+
+    /**
+     * {@code RAND} function returns different results.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-18931";)
+    public void testRandomUuidComparison() {
+        assertQuery("SELECT RAND_UUID() = RAND_UUID()").returns(false).check();
+        assertQuery("SELECT RAND_UUID() != RAND_UUID()").returns(true).check();
+    }
+
+    /**
+     * {@code UUID} vs type that can not be casted to {@code UUID}.
+     */
+    @ParameterizedTest
+    @MethodSource("binaryComparisonOperators")
+    public void testUuidInvalidOperationsDynamicParams(String opSql, String 
op) {
+        String query = format("SELECT ? {} 1", opSql);
+        IgniteException t = assertThrows(IgniteException.class, () -> 
sql(query, dataSamples.min()));
+
+        assertThat(t.getMessage(), containsString("class java.util.UUID cannot 
be cast to class java.lang.Integer"));
+    }
+
+    /** {@inheritDoc} **/
+    @Override
+    protected CustomDataTypeTestSpec<UUID> getTypeSpec() {
+        return CustomDataTypeTestSpecs.UUID_TYPE;
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidIndexTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidIndexTest.java
new file mode 100644
index 0000000000..e54019422a
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidIndexTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.datatypes.uuid;
+
+import java.util.UUID;
+import org.apache.ignite.internal.sql.engine.datatypes.CustomDataTypeTestSpecs;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.BaseIndexCustomDataTypeTest;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.CustomDataTypeTestSpec;
+import org.apache.ignite.internal.sql.engine.type.UuidType;
+import org.junit.jupiter.api.Disabled;
+
+/**
+ * Tests for queries that use indexes with {@link UuidType UUID data type}.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-19128";)
+public class ItUuidIndexTest extends BaseIndexCustomDataTypeTest<UUID> {
+
+    /** {@inheritDoc} **/
+    @Override
+    protected CustomDataTypeTestSpec<UUID> getTypeSpec() {
+        return CustomDataTypeTestSpecs.UUID_TYPE;
+    }
+
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidJoinTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidJoinTest.java
new file mode 100644
index 0000000000..7517d385c0
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidJoinTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.datatypes.uuid;
+
+import java.util.UUID;
+import org.apache.ignite.internal.sql.engine.datatypes.CustomDataTypeTestSpecs;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.BaseJoinCustomDataTypeTest;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.CustomDataTypeTestSpec;
+import org.apache.ignite.internal.sql.engine.type.UuidType;
+
+/**
+ * Tests for {@code JOIN} operator for {@link UuidType UUID data type}.
+ */
+public class ItUuidJoinTest extends BaseJoinCustomDataTypeTest<UUID> {
+
+    /** {@inheritDoc} */
+    @Override
+    protected CustomDataTypeTestSpec<UUID> getTypeSpec() {
+        return CustomDataTypeTestSpecs.UUID_TYPE;
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidQueryTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidQueryTest.java
new file mode 100644
index 0000000000..775cbeb8e8
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidQueryTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.datatypes.uuid;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.UUID;
+import org.apache.calcite.runtime.CalciteContextException;
+import org.apache.ignite.internal.sql.engine.datatypes.CustomDataTypeTestSpecs;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.BaseQueryCustomDataTypeTest;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.CustomDataTypeTestSpec;
+import org.apache.ignite.internal.sql.engine.type.UuidType;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for {@code SELECT} operator for {@link UuidType UUID data type}.
+ */
+public class ItUuidQueryTest extends BaseQueryCustomDataTypeTest<UUID> {
+
+    /**
+     * {@code UUID} vs type that can not be casted to {@code UUID}.
+     */
+    @ParameterizedTest
+    @MethodSource("binaryComparisonOperators")
+    public void testUuidInvalidOperation(String opSql, String opName) {
+        String query = format("SELECT * FROM t WHERE test_key {} 1", opSql);
+
+        CalciteContextException t = 
assertThrows(CalciteContextException.class, () -> checkQuery(query).check());
+        String error = format("Invalid types for comparison: UUID {} INTEGER 
NOT NULL", opName);
+        assertThat(t.getMessage(), containsString(error));
+    }
+
+    /** {@inheritDoc} **/
+    @Override
+    protected CustomDataTypeTestSpec<UUID> getTypeSpec() {
+        return CustomDataTypeTestSpecs.UUID_TYPE;
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidSetOpTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidSetOpTest.java
new file mode 100644
index 0000000000..56085c491f
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/uuid/ItUuidSetOpTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.datatypes.uuid;
+
+import java.util.UUID;
+import org.apache.ignite.internal.sql.engine.datatypes.CustomDataTypeTestSpecs;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.BaseSetOpCustomDataTypeTest;
+import 
org.apache.ignite.internal.sql.engine.datatypes.tests.CustomDataTypeTestSpec;
+import org.apache.ignite.internal.sql.engine.type.UuidType;
+
+/**
+ * Tests for set operators for {@link UuidType UUID data type}.
+ */
+public class ItUuidSetOpTest extends BaseSetOpCustomDataTypeTest<UUID> {
+
+    /** {@inheritDoc} */
+    @Override
+    protected CustomDataTypeTestSpec<UUID> getTypeSpec() {
+        return CustomDataTypeTestSpecs.UUID_TYPE;
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
index 9867b04311..4edc219a42 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
@@ -26,6 +26,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.fail;
 
+import java.lang.reflect.Array;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -240,7 +241,36 @@ public abstract class QueryChecker {
         );
     }
 
-    private final String originalQuery;
+    /**
+     * Allows to parameterize an SQL query string.
+     */
+    public interface QueryTemplate {
+
+        /** Template that always returns original query. **/
+        static QueryTemplate returnOriginalQuery(String query) {
+            return new QueryTemplate() {
+                @Override
+                public String originalQueryString() {
+                    return query;
+                }
+
+                @Override
+                public String createQuery() {
+                    return query;
+                }
+            };
+        }
+
+        /** Returns the original query string. **/
+        String originalQueryString();
+
+        /**
+         * Produces an SQL query from the original query string.
+         */
+        String createQuery();
+    }
+
+    private final QueryTemplate queryTemplate;
 
     private final ArrayList<Matcher<String>> planMatchers = new ArrayList<>();
 
@@ -269,8 +299,18 @@ public abstract class QueryChecker {
      * @param qry Query.
      */
     public QueryChecker(Transaction tx, String qry) {
+        this(tx, QueryTemplate.returnOriginalQuery(qry));
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param tx Transaction.
+     * @param queryTemplate A query template.
+     */
+    public QueryChecker(Transaction tx, QueryTemplate queryTemplate) {
         this.tx = tx;
-        this.originalQuery = qry;
+        this.queryTemplate = new AddDisabledRulesTemplate(queryTemplate, 
disabledRules);
     }
 
     /**
@@ -289,6 +329,7 @@ public abstract class QueryChecker {
      *
      * @return This.
      */
+    @SuppressWarnings("rawtypes")
     public QueryChecker withParams(Object... params) {
         // let's interpret null array as simple single null.
         if (params == null) {
@@ -415,14 +456,8 @@ public abstract class QueryChecker {
 
         QueryContext context = QueryContext.create(SqlQueryType.ALL, tx);
 
-        String qry = originalQuery;
+        String qry = queryTemplate.createQuery();
 
-        if (!disabledRules.isEmpty()) {
-            assert qry.matches("(?i)^select .*") : "SELECT query was expected: 
" + originalQuery;
-
-            qry = qry.replaceAll("(?i)^select", "select "
-                    + disabledRules.stream().collect(Collectors.joining("','", 
"/*+ DISABLE_RULE('", "') */")));
-        }
         try {
 
             if (!CollectionUtils.nullOrEmpty(planMatchers) || exactPlan != 
null) {
@@ -443,12 +478,13 @@ public abstract class QueryChecker {
                 if (exactPlan != null) {
                     assertEquals(exactPlan, actualPlan);
                 }
-
             }
             // Check result.
             CompletableFuture<AsyncSqlCursor<List<Object>>> cursors = 
qryProc.querySingleAsync(sessionId, context, qry, params);
             AsyncSqlCursor<List<Object>> cur = await(cursors);
 
+            checkMetadata(cur);
+
             if (expectedColumnNames != null) {
                 List<String> colNames = cur.metadata().columns().stream()
                         .map(ColumnMetadata::name)
@@ -506,6 +542,10 @@ public abstract class QueryChecker {
 
     protected abstract QueryProcessor getEngine();
 
+    protected void checkMetadata(AsyncSqlCursor<?> cursor) {
+
+    }
+
     /**
      * Check collections equals (ordered).
      *
@@ -521,19 +561,59 @@ public abstract class QueryChecker {
         int idx = 0;
 
         while (it1.hasNext()) {
-            Object item1 = it1.next();
-            Object item2 = it2.next();
+            Object expItem = it1.next();
+            Object actualItem = it2.next();
+
+            if (expItem instanceof Collection && actualItem instanceof 
Collection) {
+                assertEqualsCollections((Collection<?>) expItem, 
(Collection<?>) actualItem);
+            } else if (!Objects.deepEquals(expItem, actualItem)) {
+                String expectedStr = displayValue(expItem, true);
+                String actualStr = displayValue(actualItem, true);
 
-            if (item1 instanceof Collection && item2 instanceof Collection) {
-                assertEqualsCollections((Collection<?>) item1, (Collection<?>) 
item2);
-            } else if (!Objects.deepEquals(item1, item2)) {
-                fail("Collections are not equal (position " + idx + 
"):\nExpected: " + exp + "\nActual:   " + act);
+                fail("Collections are not equal (position " + idx + 
"):\nExpected: " + expectedStr + "\nActual:   " + actualStr);
             }
 
             idx++;
         }
     }
 
+    /**
+     * Converts the given value to a test-output friendly representation that 
includes type information.
+     */
+    private static String displayValue(Object value, boolean includeType) {
+        if (value == null) {
+            return "<null>";
+        } else if (value.getClass().isArray()) {
+            Class<?> componentType = value.getClass().getComponentType();
+            StringBuilder sb = new StringBuilder();
+
+            // Always include array element type in the output.
+            sb.append(componentType);
+            sb.append("[");
+
+            // Include element type for Object[] arrays.
+            boolean includeElementType = componentType == Object.class;
+
+            int length = Array.getLength(value);
+            for (int i = 0; i < length; i++) {
+                Object item = Array.get(value, i);
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                sb.append(displayValue(item, includeElementType));
+            }
+
+            sb.append("]");
+            return sb.toString();
+        } else {
+            if (includeType) {
+                return value + " <" + value.getClass() + ">";
+            } else {
+                return value.toString();
+            }
+        }
+    }
+
     /**
      * List comparator.
      */
@@ -582,4 +662,40 @@ public abstract class QueryChecker {
             return 0;
         }
     }
+
+    /**
+     * Updates an SQL query string to include hints for the optimizer to 
disable certain rules.
+     */
+    private static final class AddDisabledRulesTemplate implements 
QueryTemplate {
+
+        private final QueryTemplate input;
+
+        private final List<String> disabledRules;
+
+        private AddDisabledRulesTemplate(QueryTemplate input, List<String> 
disabledRules) {
+            this.input = input;
+            this.disabledRules = disabledRules;
+        }
+
+        @Override
+        public String originalQueryString() {
+            return input.originalQueryString();
+        }
+
+        @Override
+        public String createQuery() {
+            String qry = input.createQuery();
+
+            if (!disabledRules.isEmpty()) {
+                String originalQuery = input.originalQueryString();
+
+                assert qry.matches("(?i)^select .*") : "SELECT query was 
expected: " + originalQuery + ". Updated: " + qry;
+
+                return qry.replaceAll("(?i)^select", "select "
+                        + 
disabledRules.stream().collect(Collectors.joining("','", "/*+ DISABLE_RULE('", 
"') */")));
+            } else {
+                return qry;
+            }
+        }
+    }
 }


Reply via email to