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;
+ }
+ }
+ }
}