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