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 77b98a3da2 IGNITE-18797: Sql. Provide information about available
custom data types to TypeUtils and ConverterUtils (#1694)
77b98a3da2 is described below
commit 77b98a3da27bf2ea8f1c486cd27dc5bec3e445d0
Author: Max Zhuravkov <[email protected]>
AuthorDate: Mon Feb 27 13:56:50 2023 +0400
IGNITE-18797: Sql. Provide information about available custom data types to
TypeUtils and ConverterUtils (#1694)
---
.../sql/engine/exec/exp/ConverterUtils.java | 17 +--
.../sql/engine/exec/exp/CustomTypesConversion.java | 93 ++++++++++++
.../sql/engine/exec/exp/RexToLixTranslator.java | 15 +-
.../sql/engine/prepare/IgniteSqlValidator.java | 84 ++++++-----
.../sql/engine/prepare/IgniteTypeCoercion.java | 16 +-
.../internal/sql/engine/type/IgniteCustomType.java | 95 +++---------
.../engine/type/IgniteCustomTypeCoercionRules.java | 125 +++++++++++++++
.../sql/engine/type/IgniteCustomTypeSpec.java | 167 +++++++++++++++++++++
.../sql/engine/type/IgniteTypeFactory.java | 80 +++++++---
.../internal/sql/engine/type/UuidFunctions.java | 75 ---------
.../ignite/internal/sql/engine/type/UuidType.java | 41 +++--
.../util/SafeCustomTypeInternalConversion.java | 79 ++++++++++
.../ignite/internal/sql/engine/util/TypeUtils.java | 52 ++++---
.../internal/sql/engine/util/TypeUtilsTest.java | 81 ++++++++++
14 files changed, 742 insertions(+), 278 deletions(-)
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ConverterUtils.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ConverterUtils.java
index 08117f657f..0402ea94e7 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ConverterUtils.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ConverterUtils.java
@@ -21,7 +21,6 @@ import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
-import java.util.UUID;
import org.apache.calcite.adapter.enumerable.RexImpTable;
import org.apache.calcite.linq4j.tree.ConstantExpression;
import org.apache.calcite.linq4j.tree.ConstantUntypedNull;
@@ -37,7 +36,6 @@ import org.apache.calcite.runtime.SqlFunctions;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.BuiltInMethod;
import org.apache.calcite.util.Util;
-import org.apache.ignite.internal.sql.engine.type.UuidFunctions;
/**
* ConverterUtils.
@@ -396,14 +394,15 @@ public class ConverterUtils {
}
return result;
}
- } else if (toType == UUID.class) {
- // IgniteCustomType: implement runtime conversion routines.
- // UUID can be created from a string/object type by a CAST
expression.
- // We get an object type here because type info is erased for
dynamic parameters.
- // See DataContextInputGetter in RexExecutorImpl.
- return UuidFunctions.cast(operand);
}
- return Expressions.convert_(operand, toType);
+
+ // IgniteCustomType: call runtime conversion routines.
+ var toCustomType = CustomTypesConversion.INSTANCE.tryConvert(operand,
toType);
+ if (toCustomType != null) {
+ return toCustomType;
+ } else {
+ return Expressions.convert_(operand, toType);
+ }
}
private static boolean isA(Type fromType, Primitive primitive) {
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/CustomTypesConversion.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/CustomTypesConversion.java
new file mode 100644
index 0000000000..f1dac99306
--- /dev/null
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/CustomTypesConversion.java
@@ -0,0 +1,93 @@
+/*
+ * 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.exec.exp;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.rel.type.RelDataType;
+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.util.Commons;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Provides cast expressions for {@link IgniteCustomType custom data types}.
+ */
+final class CustomTypesConversion {
+
+ static final CustomTypesConversion INSTANCE = new
CustomTypesConversion(Commons.typeFactory());
+
+ private final Map<Class<?>, Function<Expression, Expression>>
castByInternalType = new HashMap<>();
+
+ private final Map<String, Function<Expression, Expression>> castByTypeName
= new HashMap<>();
+
+ private CustomTypesConversion(IgniteTypeFactory typeFactory) {
+ // IgniteCustomType: Add cast function implementations.
+ var customTypeSpecs = typeFactory.getCustomTypeSpecs();
+
+ for (var spec : customTypeSpecs.values()) {
+ // We use Object as an argument because:
+ //
+ // 1. type info is erased for dynamic parameters. See
DataContextInputGetter in RexExecutorImpl.
+ // 2. so internal calls will throw a CastCastException instead of
a NoSuchMethodError in runtime
+ // (It would be even better if such casts were not necessary).
+
+ Function<Expression, Expression> castExpr =
+ (operand) -> Expressions.call(spec.castFunction(),
Expressions.convert_(operand, Object.class));
+ // ??? Should we add a hook here that can short-circuit when a
custom type can not be converted?
+
+ castByInternalType.put(spec.storageType(), castExpr);
+ castByTypeName.put(spec.typeName(), castExpr);
+ }
+ }
+
+ /**
+ * If the specified {@code internalType} represents is a custom data type,
+ * then this method returns expression that converts an operand to it.
Otherwise returns {@code null}.
+ * */
+ @Nullable
+ Expression tryConvert(Expression operand, Type internalType) {
+ // internal type is always a Class.
+ var cast = castByInternalType.get((Class<?>) internalType);
+ if (cast == null) {
+ return null;
+ }
+
+ return cast.apply(operand);
+ }
+
+ /**
+ * If the specified {@code targetType} represents is a custom data type,
+ * then this method returns expression that converts an operand to it.
Otherwise returns {@code null}.
+ * */
+ @Nullable
+ Expression tryConvert(Expression operand, RelDataType targetType) {
+ if (!(targetType instanceof IgniteCustomType)) {
+ return null;
+ }
+
+ var customType = (IgniteCustomType) targetType;
+ Function<Expression, Expression> cast =
castByTypeName.get(customType.getCustomTypeName());
+
+ return cast.apply(operand);
+ }
+}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
index a55c8cc822..7615dbfbe6 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
@@ -72,10 +72,7 @@ import org.apache.calcite.sql.validate.SqlConformance;
import org.apache.calcite.util.BuiltInMethod;
import org.apache.calcite.util.ControlFlowException;
import org.apache.calcite.util.Pair;
-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.UuidFunctions;
-import org.apache.ignite.internal.sql.engine.type.UuidType;
import org.apache.ignite.internal.sql.engine.util.IgniteMethod;
/**
@@ -219,15 +216,9 @@ public class RexToLixTranslator implements
RexVisitor<RexToLixTranslator.Result>
Expression convert = null;
switch (targetType.getSqlTypeName()) {
case ANY:
- // IgniteCustomType: conversion from some type to possibly a
custom data type.
- // Should we add a hook here that can short-circuit when a
custom type can not be converted?
- if (targetType instanceof UuidType) {
- // We need to convert an argument to an object so a call
will throw a CastCastException
- // instead of a NoSuchMethodError in runtime.
- // It would be even better if this cast were not necessary.
- return UuidFunctions.cast(Expressions.convert_(operand,
Object.class));
- } else if (targetType instanceof IgniteCustomType) {
- throw new AssertionError("IgniteCustomType: cast is not
implemented for " + targetType);
+ var toCustomType =
CustomTypesConversion.INSTANCE.tryConvert(operand, targetType);
+ if (toCustomType != null) {
+ convert = toCustomType;
} else {
convert = operand;
}
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 52c1f1c147..3d6579e211 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
@@ -67,6 +67,7 @@ import org.apache.calcite.sql.validate.SqlValidatorTable;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.ignite.internal.sql.engine.schema.IgniteTable;
import org.apache.ignite.internal.sql.engine.schema.TableDescriptor;
+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.UuidType;
import org.apache.ignite.internal.sql.engine.util.Commons;
@@ -325,51 +326,62 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
SqlKind sqlKind = expr.getKind();
// See the comments below.
+ // IgniteCustomType: at the moment we allow operations, involving
custom data types, that are binary comparison to fail at runtime.
if (!SqlKind.BINARY_COMPARISON.contains(sqlKind)) {
return dataType;
}
// Comparison and arithmetic operators are SqlCalls.
SqlCall sqlCall = (SqlCall) expr;
+ // IgniteCustomType: We only handle binary operations here.
+ var lhs = getValidatedNodeType(sqlCall.operand(0));
+ var rhs = getValidatedNodeType(sqlCall.operand(1));
- for (var operand : sqlCall.getOperandList()) {
- var operandType = getValidatedNodeType(operand);
- if (operandType.getSqlTypeName() == SqlTypeName.ANY) {
- // IgniteCustomType:
- //
- // The correct way to implement the validation error bellow
would be
- // 1) Implement the SqlOperandTypeChecker that prohibit
arithmetic operations
- // between types that neither support binary/unary operators
nor support type coercion.
- // 2) Specify that SqlOperandTypeChecker for every
binary/unary operator defined in
- // IgniteSqlOperatorTable
- //
- // This would allow to reject plans that contain type errors
- // at the validation stage.
- //
- // Similar approach can also be used to handle casts between
types that can not
- // be converted into one another.
- //
- // And if applied to dynamic parameters with combination of a
modified SqlOperandTypeChecker for
- // a cast function this would allow to reject plans with
invalid values w/o at validation stage.
- //
- var lhs = getValidatedNodeType(sqlCall.operand(0));
- var rhs = getValidatedNodeType(sqlCall.operand(1));
-
- if (lhs.getSqlTypeName() != rhs.getSqlTypeName()) {
- // IgniteCustomType: To enable implicit casts from varchar
types to a custom data type (for some expressions).
- // This check must also be moved to SqlOperandTypeChecker.
- // Also see IgniteTypeCoercion::needToCast. (It would be
better if code were located in one place).
- if (SqlTypeUtil.isCharacter(lhs) ||
SqlTypeUtil.isCharacter(rhs)) {
- return dataType;
- }
+ if (lhs.getSqlTypeName() != SqlTypeName.ANY && rhs.getSqlTypeName() !=
SqlTypeName.ANY) {
+ return dataType;
+ }
+
+ // IgniteCustomType:
+ //
+ // The correct way to implement the validation error bellow would be
to move these coercion rules to SqlOperandTypeChecker:
+ // 1) Implement the SqlOperandTypeChecker that prohibit arithmetic
operations
+ // between types that neither support binary/unary operators nor
support type coercion.
+ // 2) Specify that SqlOperandTypeChecker for every binary/unary
operator defined in
+ // IgniteSqlOperatorTable
+ //
+ // This would allow to reject plans that contain type errors
+ // at the validation stage.
+ //
+ // Similar approach can also be used to handle casts between types
that can not
+ // be converted into one another.
+ //
+ // And if applied to dynamic parameters with combination of a modified
SqlOperandTypeChecker for
+ // a cast function this would allow to reject plans with invalid
values w/o at validation stage.
+ //
+ var customTypeCoercionRules =
typeFactory().getCustomTypeCoercionRules();
+ boolean canConvert;
+
+ // IgniteCustomType: To enable implicit casts to a custom data type.
+ if (SqlTypeUtil.equalSansNullability(typeFactory(), lhs, rhs)) {
+ // We can always perform binary comparison operations between
instances of the same type.
+ canConvert = true;
+ } else if (lhs instanceof IgniteCustomType) {
+ canConvert = customTypeCoercionRules.needToCast(rhs,
(IgniteCustomType) lhs);
+ } else if (rhs instanceof IgniteCustomType) {
+ canConvert = customTypeCoercionRules.needToCast(lhs,
(IgniteCustomType) rhs);
+ } else {
+ // We should not get here because at least one operand type must
be a IgniteCustomType
+ // and only custom data types must use SqlTypeName::ANY.
+ throw new AssertionError("At least one operand must be a custom
data type: " + expr);
+ }
- var ex = RESOURCE.invalidTypesForComparison(
- lhs.getFullTypeString(), sqlKind.sql,
rhs.getFullTypeString());
+ if (!canConvert) {
+ var ex = RESOURCE.invalidTypesForComparison(
+ lhs.getFullTypeString(), sqlKind.sql,
rhs.getFullTypeString());
- throw
SqlUtil.newContextException(expr.getParserPosition(), ex);
- }
- }
+ throw SqlUtil.newContextException(expr.getParserPosition(), ex);
+ } else {
+ return dataType;
}
- return dataType;
}
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 5f93a1703c..738671a28e 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
@@ -47,7 +47,8 @@ import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.sql.validate.implicit.TypeCoercionImpl;
import org.apache.calcite.util.Util;
import org.apache.ignite.internal.sql.engine.type.IgniteCustomType;
-import org.apache.ignite.internal.sql.engine.type.UuidType;
+import
org.apache.ignite.internal.sql.engine.type.IgniteCustomTypeCoercionRules;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
import org.checkerframework.checker.nullness.qual.Nullable;
/** Implicit type cast implementation. */
@@ -56,8 +57,11 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
// We are using thread local here b/c TypeCoercion is expected to be
stateless.
private static final ThreadLocal<ContextStack> contextStack =
ThreadLocal.withInitial(ContextStack::new);
+ private final IgniteCustomTypeCoercionRules typeCoercionRules;
+
public IgniteTypeCoercion(RelDataTypeFactory typeFactory, SqlValidator
validator) {
super(typeFactory, validator);
+ this.typeCoercionRules = ((IgniteTypeFactory)
typeFactory).getCustomTypeCoercionRules();
}
/** {@inheritDoc} **/
@@ -157,14 +161,12 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
}
} else if (toType.getSqlTypeName() == SqlTypeName.ANY) {
RelDataType fromType = validator.deriveType(scope, node);
+
// IgniteCustomType: whether we need implicit cast from one type
to another.
- if (fromType instanceof IgniteCustomType && toType instanceof
IgniteCustomType) {
- IgniteCustomType from = (IgniteCustomType) fromType;
+ // We can get toType = ANY in e1, at least in case where e1 is
part of CASE <e1> WHERE ... END expression.
+ if (toType instanceof IgniteCustomType) {
IgniteCustomType to = (IgniteCustomType) toType;
- return !Objects.equals(from.getCustomTypeName(),
to.getCustomTypeName());
- } else if (SqlTypeUtil.isCharacter(fromType) && toType instanceof
UuidType) {
- // IgniteCustomType: implicit casts from character types to
UUID
- return true;
+ return typeCoercionRules.needToCast(fromType, to);
}
}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomType.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomType.java
index e9ece9d4ba..7ab9711dad 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomType.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomType.java
@@ -17,8 +17,6 @@
package org.apache.ignite.internal.sql.engine.type;
-import java.lang.reflect.Type;
-import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFamily;
import org.apache.calcite.rel.type.RelDataTypeImpl;
import org.apache.calcite.sql.SqlIdentifier;
@@ -28,48 +26,34 @@ import org.apache.calcite.sql.SqlTypeNameSpec;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.sql.type.SqlTypeName;
-import org.apache.ignite.internal.schema.NativeType;
-import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
-import org.apache.ignite.internal.sql.engine.exec.ExecutionServiceImpl;
-import org.apache.ignite.internal.sql.engine.exec.exp.ConverterUtils;
-import org.apache.ignite.internal.sql.engine.exec.exp.ExpressionFactoryImpl;
-import org.apache.ignite.internal.sql.engine.exec.exp.RexToLixTranslator;
import org.apache.ignite.internal.sql.engine.exec.exp.agg.Accumulators;
import org.apache.ignite.internal.sql.engine.prepare.IgniteSqlValidator;
import org.apache.ignite.internal.sql.engine.sql.IgniteSqlTypeNameSpec;
-import org.apache.ignite.internal.sql.engine.util.TypeUtils;
-import org.apache.ignite.sql.ColumnMetadata;
-import org.apache.ignite.sql.ColumnType;
/**
* A base class for custom data types.
*
* <p><b>Custom data type implementation check list.</b>
*
- * <p>Add a subclass that extends {@link IgniteCustomType}.
+ * <p>Create a subclass of {@link IgniteCustomType}.
+ *
+ * <p>Define {@link IgniteCustomTypeSpec type spec} for your type that
describes the following properties:
* <ul>
- * <li>Implement {@link IgniteCustomType#nativeType()}.</li>
- * <li>Implement {@link IgniteCustomType#columnType()}.</li>
- * <li>Implement {@link
IgniteCustomType#createWithNullability(boolean)}.</li>
+ * <li>{@link IgniteCustomTypeSpec#typeName() type name}.</li>
+ * <li>{@link IgniteCustomTypeSpec#nativeType() native type}.</li>
+ * <li>{@link IgniteCustomTypeSpec#columnType() column type}.</li>
+ * <li>{@link IgniteCustomTypeSpec#storageType() storage type}.</li>
+ * <li>{@link IgniteCustomTypeSpec#castFunction() cast function}.
+ * See {@link IgniteCustomTypeSpec#getCastFunction(Class, String)
getCastFunction}.
+ * </li>
* </ul>
*
* <p>Code base contains comments that start with {@code IgniteCustomType:} to
provide extra information.
*
- * <p>Update {@link IgniteTypeFactory}'s constructor to register your type.
+ * <p>Update {@link IgniteTypeFactory}'s constructor to register your type and
to specify type coercion rules.
*
* <p>Update type inference for dynamic parameters in {@link
IgniteSqlValidator}.
*
- * <p>Update {@link TypeUtils}:
- * <ul>
- * <li>Update {@link TypeUtils#toInternal(ExecutionContext, Object, Type)}
and
- * {@link TypeUtils#fromInternal(ExecutionContext, Object, Type)} to add
assertions that check
- * that a value has the same type as a {@link #storageType()}.</li>
- * </ul>
- *
- * <p>Update both {@link RexToLixTranslator RexToLitTranslator} and
- * {@link ConverterUtils} to implement runtime routines for conversion
- * of your type from other data types if necessary.
- *
* <p>Further steps:
* <ul>
* <li>Update an SQL parser generator code to support your type - see
{@code DataTypeEx()}.</li>
@@ -81,6 +65,8 @@ import org.apache.ignite.sql.ColumnType;
* when a custom data type is implemented.</li>
* </ul>
*
+ * <p>Type conversion is implemented in {@code CustomTypesConversion} and uses
rules defined for your custom type.
+ *
* <p>Client code/JDBC:
* <ul>
* <li>Update {@code JdbcDatabaseMetadata::getTypeInfo} to return
information about your type.</li>
@@ -89,66 +75,35 @@ import org.apache.ignite.sql.ColumnType;
*
* <p><b>Update this documentation when you are going to change this
procedure.</b>
*
- * @param <StorageT> A class representing value in a storage. Must implement
{@link Comparable}.
- * This is a requirement imposed by calcite's row-expressions
implementation
- * (see {@link org.apache.ignite.internal.sql.engine.rex.IgniteRexBuilder
IgniteRexBuilder}).
*/
-public abstract class IgniteCustomType<StorageT extends Comparable<StorageT>>
extends RelDataTypeImpl {
+public abstract class IgniteCustomType extends RelDataTypeImpl {
- private final Class<StorageT> storageType;
+ private final IgniteCustomTypeSpec spec;
private final boolean nullable;
private final int precision;
/** Constructor. */
- protected IgniteCustomType(Class<StorageT> storageType, boolean nullable,
int precision) {
- this.storageType = storageType;
+ protected IgniteCustomType(IgniteCustomTypeSpec spec, boolean nullable,
int precision) {
+ this.spec = spec;
this.nullable = nullable;
this.precision = precision;
computeDigest();
}
- /** Returns the name of this type. **/
- public abstract String getCustomTypeName();
-
- /**
- * Returns the storage type of this data type.
- *
- * <p>This method is called by {@link
IgniteTypeFactory#getJavaClass(RelDataType)}
- * to provide types for a expression interpreter. Execution engine also
relies on the fact that this
- * type is also used by {@link TypeUtils TypeUtils} in type conversions.
- *
- * @see ExpressionFactoryImpl
- * @see TypeUtils#toInternal(ExecutionContext, Object, Type)
- * @see TypeUtils#fromInternal(ExecutionContext, Object, Type)
- */
- public final Type storageType() {
- return storageType;
+ /** Returns the name of this type. A short handfor {@code
spec().typeName() }. **/
+ public final String getCustomTypeName() {
+ return spec.typeName();
}
/**
- * Returns the {@link NativeType} for this custom data type.
- *
- * <p>At the moment it serves the following purpose:
- * <ul>
- * <li>
- * Used by {@link
IgniteTypeFactory#relDataTypeToNative(RelDataType)} to retrieve underlying
- * {@link NativeType} for DDL queries.
- * </li>
- * <li>
- * To retrieve a java type to perform type conversions by
- * {@link ExecutionServiceImpl}.
- * </li>
- * </ul>
- */
- public abstract NativeType nativeType();
-
- /**
- * Returns the {@link ColumnType} of this data type. Provides type
information for {@link ColumnMetadata}.
+ * Returns the {@link IgniteCustomTypeSpec specification} of this type.
*/
- public abstract ColumnType columnType();
+ public final IgniteCustomTypeSpec spec() {
+ return spec;
+ }
/** {@inheritDoc} */
@Override public final boolean isNullable() {
@@ -172,7 +127,7 @@ public abstract class IgniteCustomType<StorageT extends
Comparable<StorageT>> ex
}
/** Creates an instance of this type with the specified nullability. **/
- public abstract IgniteCustomType<StorageT> createWithNullability(boolean
nullable);
+ public abstract IgniteCustomType createWithNullability(boolean nullable);
/**
* Creates an {@link SqlTypeNameSpec} for this custom data type, which is
used as an argument for the CAST function.
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomTypeCoercionRules.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomTypeCoercionRules.java
new file mode 100644
index 0000000000..bed6ac25ff
--- /dev/null
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomTypeCoercionRules.java
@@ -0,0 +1,125 @@
+/*
+ * 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.type;
+
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.validate.implicit.TypeCoercion;
+import org.apache.ignite.internal.sql.engine.prepare.IgniteTypeCoercion;
+
+/**
+ * Defines rules for coercing {@link SqlTypeName built-in SQL types} to {@link
IgniteCustomType custom data types}.
+ *
+ * @see IgniteTypeCoercion ignite type coercion.
+ * @see TypeCoercion calcite's TypeCoercion interface.
+ */
+public final class IgniteCustomTypeCoercionRules {
+
+ private final Map<String, Set<SqlTypeName>> canCastFrom;
+
+ IgniteCustomTypeCoercionRules(Map<String, IgniteCustomTypeSpec> typeSpecs,
Map<String, Set<SqlTypeName>> canCastFrom) {
+ for (var rule : canCastFrom.entrySet()) {
+ var typeName = rule.getKey();
+ if (!typeSpecs.containsKey(typeName)) {
+ var error = format("Unable to define type coercion rule. "
+ + "Unexpected custom type: {}. Rules: {}. Known types:
{}", typeName, rule.getValue(), typeSpecs.keySet()
+ );
+ throw new IllegalArgumentException(error);
+ }
+ }
+
+ this.canCastFrom = canCastFrom;
+ }
+
+ /**
+ * Checks whether the cast operation is needed to convert {@code fromType}
to the custom type {@code toType}.
+ *
+ * <p>NOTE: <b>This method returns {@code false}, when called with the
same argument as both parameters, because
+ * there is no need to add casts between the same types.</b>
+ *
+ * @param fromType Source data type.
+ * @param toType Target custom data type.
+ */
+ public boolean needToCast(RelDataType fromType, IgniteCustomType toType) {
+ // The implementation of this method must always use
::canCastFrom(typeName),
+ // because canCastFrom is can be used to generate rules for runtime
execution.
+ var rules = canCastFrom(toType.getCustomTypeName());
+
+ return rules.contains(fromType.getSqlTypeName());
+ }
+
+ /** Returns a set of built-in SQL types the given custom type can be
converted from. **/
+ public Set<SqlTypeName> canCastFrom(String typeName) {
+ return canCastFrom.getOrDefault(typeName, Collections.emptySet());
+ }
+
+ /** Creates a builder to define a table of type coercion rules. **/
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * A builder for {@link IgniteCustomTypeCoercionRules}.
+ */
+ public static class Builder {
+
+ private final Map<String, Set<SqlTypeName>> canCastFrom = new
HashMap<>();
+
+ Builder() {
+
+ }
+
+ /**
+ * Adds a rule that allows cast from the given custom type to the
specified built-in SQL type.
+ */
+ public Builder addRule(String typeName, SqlTypeName to) {
+ var rules = canCastFrom.computeIfAbsent(typeName, (k) ->
EnumSet.noneOf(SqlTypeName.class));
+ rules.add(to);
+ return this;
+ }
+
+ /**
+ * Adds rules that allow casts from the given custom type to the
specified built-in SQL types.
+ */
+ public Builder addRules(String typeName, Collection<SqlTypeName>
typeNames) {
+ var rules = canCastFrom.computeIfAbsent(typeName, (k) ->
EnumSet.noneOf(SqlTypeName.class));
+ rules.addAll(typeNames);
+ return this;
+ }
+
+ /**
+ * Builds a table of type coercion rules for the given custom data
types.
+ *
+ * @param typeSpecs A map of custom type specs.
+ * @return A table of type coercion rules.
+ *
+ * @throws IllegalArgumentException if a this builder contains rules
for custom types not present in {@code typeSpecs}.
+ */
+ public IgniteCustomTypeCoercionRules build(Map<String,
IgniteCustomTypeSpec> typeSpecs) {
+ return new IgniteCustomTypeCoercionRules(typeSpecs, canCastFrom);
+ }
+ }
+}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomTypeSpec.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomTypeSpec.java
new file mode 100644
index 0000000000..36b52a28fa
--- /dev/null
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteCustomTypeSpec.java
@@ -0,0 +1,167 @@
+/*
+ * 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.type;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Objects;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.ignite.internal.schema.NativeType;
+import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
+import org.apache.ignite.internal.sql.engine.exec.ExecutionServiceImpl;
+import org.apache.ignite.internal.sql.engine.exec.exp.ExpressionFactoryImpl;
+import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.apache.ignite.internal.tostring.S;
+import org.apache.ignite.sql.ColumnMetadata;
+import org.apache.ignite.sql.ColumnType;
+
+/**
+ * Specification that defines properties of a {@link IgniteCustomType custom
data type} that are shared by all instances of that type.
+ *
+ * @see IgniteCustomType custom data type.
+ */
+public final class IgniteCustomTypeSpec {
+
+ private final String typeName;
+
+ private final NativeType nativeType;
+
+ private final ColumnType columnType;
+
+ private final Class<?> storageType;
+
+ private final Method castFunction;
+
+ /**
+ * Creates a specification for a type that with the specified {@code
storage type}.
+ *
+ * @param typeName Type name.
+ * @param nativeType Native type.
+ * @param columnType Column type.
+ * @param storageType Storage type.
+ * @param castFunction Cast function.
+ */
+ public IgniteCustomTypeSpec(String typeName, NativeType nativeType,
ColumnType columnType,
+ Class<? extends Comparable<?>> storageType, Method castFunction) {
+
+ this.typeName = typeName;
+ this.nativeType = nativeType;
+ this.columnType = columnType;
+ this.storageType = storageType;
+ this.castFunction = castFunction;
+ }
+
+ /** Returns the name of the type. */
+ public String typeName() {
+ return typeName;
+ }
+
+ /**
+ * Returns the {@link NativeType} for this custom data type.
+ *
+ * <p>At the moment it serves the following purpose:
+ * <ul>
+ * <li>
+ * Used by {@link
IgniteTypeFactory#relDataTypeToNative(RelDataType)} to retrieve underlying
+ * {@link NativeType} for DDL queries.
+ * </li>
+ * <li>
+ * To retrieve a java type to perform type conversions by
+ * {@link ExecutionServiceImpl}.
+ * </li>
+ * </ul>
+ */
+ public NativeType nativeType() {
+ return nativeType;
+ }
+
+ /**
+ * Returns the {@link ColumnType} of this data type. Provides type
information for {@link ColumnMetadata}.
+ */
+ public ColumnType columnType() {
+ return columnType;
+ }
+
+ /**
+ * Returns the storage type of this data type.
+ *
+ * <p>This method is called by {@link
IgniteTypeFactory#getJavaClass(RelDataType)}
+ * to provide types for a expression interpreter. Execution engine also
relies on the fact that this type is also used by
+ * {@link TypeUtils TypeUtils} in type conversions.
+ *
+ * @see ExpressionFactoryImpl
+ * @see TypeUtils#toInternal(ExecutionContext, Object, Type)
+ * @see TypeUtils#fromInternal(ExecutionContext, Object, Type)
+ */
+ public Class<?> storageType() {
+ return storageType;
+ }
+
+ /** Implementation of a cast function for this type. */
+ public Method castFunction() {
+ return castFunction;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ IgniteCustomTypeSpec that = (IgniteCustomTypeSpec) o;
+ return typeName.equals(that.typeName) &&
nativeType.equals(that.nativeType) && storageType.equals(that.storageType)
+ && columnType == that.columnType;
+ }
+
+ /**
+ * Returns a method that implements a cast function for a custom data type.
+ *
+ * <p>Requirements:
+ * <ul>
+ * <li>It must be a {@code public static} method.</li>
+ * <li>It must have the following signature: {@code Object ->
typeSpec::nativeType.javaClass}.</li>
+ * <li>It must implement conversion from the type itself.</li>
+ * <li>It must implement conversion from String to {@code
typeSpec::nativeType.javaClass}.</li>
+ * </ul>
+ *
+ * @param clazz A class that defines a cast function.
+ * @param method A name of a method.
+ * @return A method that implements a cast function.
+ */
+ public static Method getCastFunction(Class<?> clazz, String method) {
+ try {
+ return clazz.getMethod(method, Object.class);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Incorrect cast function
method: " + method, e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(typeName, nativeType, storageType, columnType);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return S.toString(IgniteCustomTypeSpec.class, this);
+ }
+}
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 4eb570d94e..01c95275e4 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
@@ -30,8 +30,11 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
+import java.util.Collection;
+import java.util.EnumSet;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -95,8 +98,11 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
charset = StandardCharsets.UTF_8;
}
- // IgniteCustomType: all prototypes of custom types are registered
here.
- NewCustomType uuidType = new NewCustomType(UuidType.NAME,
UuidType.JAVA_TYPE, (nullable, precision) -> new UuidType(nullable));
+ // IgniteCustomType: all custom data types are registered here
+ NewCustomType uuidType = new NewCustomType(UuidType.SPEC, (nullable,
precision) -> new UuidType(nullable));
+ // UUID type can be converted from character types.
+ uuidType.addCoercionRules(SqlTypeName.CHAR_TYPES);
+
customDataTypes = new CustomDataTypes(Set.of(uuidType));
}
@@ -154,8 +160,8 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
return Enum.class;
case ANY:
if (type instanceof IgniteCustomType) {
- var customType = (IgniteCustomType<?>) type;
- return customType.storageType();
+ var customType = (IgniteCustomType) type;
+ return customType.spec().storageType();
}
// fallthrough
case OTHER:
@@ -252,8 +258,8 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
: NativeTypes.blobOf(relType.getPrecision());
case ANY:
if (relType instanceof IgniteCustomType) {
- var customType = (IgniteCustomType<?>) relType;
- return customType.nativeType();
+ var customType = (IgniteCustomType) relType;
+ return customType.spec().nativeType();
}
// fallthrough
default:
@@ -325,8 +331,8 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
return Enum.class;
case ANY:
if (type instanceof IgniteCustomType) {
- var customType = (IgniteCustomType<?>) type;
- var nativeType = customType.nativeType();
+ var customType = (IgniteCustomType) type;
+ var nativeType = customType.spec().nativeType();
return Commons.nativeTypeToClass(nativeType);
}
// fallthrough
@@ -368,15 +374,15 @@ public class IgniteTypeFactory extends
JavaTypeFactoryImpl {
// when at least one of its arguments have sqlTypeName = ANY.
assert resultType instanceof BasicSqlType : "leastRestrictive is
expected to return a new instance of a type: " + resultType;
- IgniteCustomType<?> firstCustomType = null;
+ IgniteCustomType firstCustomType = null;
SqlTypeFamily sqlTypeFamily = null;
for (var type : types) {
if (type instanceof IgniteCustomType) {
- var customType = (IgniteCustomType<?>) type;
+ var customType = (IgniteCustomType) type;
if (firstCustomType == null) {
- firstCustomType = (IgniteCustomType<?>) type;
+ firstCustomType = (IgniteCustomType) type;
} else if
(!Objects.equals(firstCustomType.getCustomTypeName(),
customType.getCustomTypeName())) {
// IgniteCustomType: Conversion between custom data
types is not supported.
return null;
@@ -423,7 +429,7 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
@Override
public RelDataType createTypeWithNullability(RelDataType type, boolean
nullable) {
if (type instanceof IgniteCustomType) {
- return canonize(((IgniteCustomType<?>)
type).createWithNullability(nullable));
+ return canonize(((IgniteCustomType)
type).createWithNullability(nullable));
} else {
return super.createTypeWithNullability(type, nullable);
}
@@ -469,7 +475,7 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
//
// 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);
+ IgniteCustomType customType = customTypeFactory.newType(true,
precision);
// assert !customType.isNullable() : "makeCustomType must not return a
nullable type: " + typeName + " " + customType;
return canonize(customType);
}
@@ -486,6 +492,16 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl
{
return createCustomType(typeName, PRECISION_NOT_SPECIFIED);
}
+ /** Returns {@link IgniteCustomTypeSpec type specifications} of registered
custom data types. */
+ public Map<String, IgniteCustomTypeSpec> getCustomTypeSpecs() {
+ return customDataTypes.typeSpecs;
+ }
+
+ /** Returns type coercion rules to custom data types. */
+ public IgniteCustomTypeCoercionRules getCustomTypeCoercionRules() {
+ return customDataTypes.typeCoercionRules;
+ }
+
private boolean allEquals(List<RelDataType> types) {
assert types.size() > 1;
@@ -515,32 +531,52 @@ public class IgniteTypeFactory extends
JavaTypeFactoryImpl {
*/
private final Map<String, IgniteCustomTypeFactory> typeFactories;
+ private final Map<String, IgniteCustomTypeSpec> typeSpecs;
+
+ private final IgniteCustomTypeCoercionRules typeCoercionRules;
+
CustomDataTypes(Set<NewCustomType> customDataTypes) {
this.javaTypes = customDataTypes.stream()
- .map(t -> t.storageType)
+ .map(t -> t.spec.storageType())
.collect(Collectors.toSet());
- this.typeFactories =
customDataTypes.stream().collect(Collectors.toMap((v) -> v.typeName, (v) ->
v.typeFactory));
+ this.typeSpecs = customDataTypes.stream()
+ .map(t -> Map.entry(t.spec.typeName(), t.spec))
+ .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
+
+ this.typeFactories =
customDataTypes.stream().collect(Collectors.toMap((v) -> v.spec.typeName(), (v)
-> v.typeFactory));
+
+ var builder = IgniteCustomTypeCoercionRules.builder();
+ for (var newType : customDataTypes) {
+ builder.addRules(newType.spec.typeName(),
newType.canBeCoercedTo);
+ }
+ this.typeCoercionRules = builder.build(typeSpecs);
}
}
private static final class NewCustomType {
- final String typeName;
-
- final Class<?> storageType;
+ final IgniteCustomTypeSpec spec;
final IgniteCustomTypeFactory typeFactory;
- NewCustomType(String typeName, Class<?> storageType,
IgniteCustomTypeFactory typeFactory) {
- this.typeName = typeName;
- this.storageType = storageType;
+ final Set<SqlTypeName> canBeCoercedTo =
EnumSet.noneOf(SqlTypeName.class);
+
+ NewCustomType(IgniteCustomTypeSpec spec, IgniteCustomTypeFactory
typeFactory) {
+ this.spec = spec;
this.typeFactory = typeFactory;
}
+
+ /**
+ * Adds a type coercion rule to from the given built-in SQL types to
this custom data type.
+ */
+ void addCoercionRules(Collection<SqlTypeName> types) {
+ canBeCoercedTo.addAll(types);
+ }
}
@FunctionalInterface
interface IgniteCustomTypeFactory {
- IgniteCustomType<?> newType(boolean nullable, int precision);
+ IgniteCustomType newType(boolean nullable, int precision);
}
/** {@inheritDoc} */
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/UuidFunctions.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/UuidFunctions.java
deleted file mode 100644
index 796c2ee355..0000000000
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/UuidFunctions.java
+++ /dev/null
@@ -1,75 +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.type;
-
-import java.lang.reflect.Method;
-import java.util.UUID;
-import org.apache.calcite.linq4j.tree.Expression;
-import org.apache.calcite.linq4j.tree.Expressions;
-
-/**
- * A set functions required by expression execution runtime to support of
{@code UUID} type.
- */
-public final class UuidFunctions {
-
- /**
- * Implementation of a CAST operator for {@link UuidType} used by
expression execution runtime.
- *
- * @see #cast(Object)
- **/
- private static final Method CAST;
-
- static {
- try {
- CAST = UuidFunctions.class.getMethod("cast", Object.class);
- } catch (NoSuchMethodException e) {
- throw new IllegalStateException("cast method is not defined", e);
- }
- }
-
- private UuidFunctions() {
-
- }
-
- /**
- * Creates a cast expression that convert the given operation into {@link
UuidType}.
- *
- * @param operand An operand.
- * @return A cast to UUID expression.
- */
- public static Expression cast(Expression operand) {
- return Expressions.call(CAST, operand);
- }
-
- /**
- * Performs casts from object to {@code UUID}. Accepts values that are
Strings, UUIDs.
- *
- * @param value A value.
- * @return An UUID.
- * @throws ClassCastException if type can not be converted to UUID.
- */
- public static UUID cast(Object value) {
- // It would be better to generate Expression tree that is equivalent
to the code below
- // from type checking rules for this type in order to avoid code
duplication.
- if (value instanceof String) {
- return UUID.fromString((String) value);
- } else {
- return (UUID) value;
- }
- }
-}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/UuidType.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/UuidType.java
index ce1196d3fd..f1cb5aafd9 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/UuidType.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/UuidType.java
@@ -18,22 +18,22 @@
package org.apache.ignite.internal.sql.engine.type;
import java.util.UUID;
-import org.apache.ignite.internal.schema.NativeType;
import org.apache.ignite.internal.schema.NativeTypes;
import org.apache.ignite.sql.ColumnType;
/** UUID SQL type. */
-public final class UuidType extends IgniteCustomType<UUID> {
+public final class UuidType extends IgniteCustomType {
/** A string name of this type: {@code UUID}. **/
public static final String NAME = "UUID";
- /** The storage type. **/
- public static final Class<UUID> JAVA_TYPE = UUID.class;
+ /** Type spec of this type. **/
+ public static final IgniteCustomTypeSpec SPEC = new
IgniteCustomTypeSpec(NAME, NativeTypes.UUID,
+ ColumnType.UUID, UUID.class,
IgniteCustomTypeSpec.getCastFunction(UuidType.class, "cast"));
/** Constructor. */
public UuidType(boolean nullable) {
- super(JAVA_TYPE, nullable, PRECISION_NOT_SPECIFIED);
+ super(SPEC, nullable, PRECISION_NOT_SPECIFIED);
}
/** {@inheritDoc} */
@@ -41,27 +41,22 @@ public final class UuidType extends IgniteCustomType<UUID> {
sb.append(NAME);
}
- /** {@inheritDoc} */
- @Override
- public String getCustomTypeName() {
- return NAME;
- }
-
- /** {@inheritDoc} */
- @Override
- public NativeType nativeType() {
- return NativeTypes.UUID;
- }
-
- /** {@inheritDoc} */
- @Override
- public ColumnType columnType() {
- return ColumnType.UUID;
- }
-
/** {@inheritDoc} */
@Override
public UuidType createWithNullability(boolean nullable) {
return new UuidType(nullable);
}
+
+ /**
+ * Implementation of a cast function for {@code UUID} data type.
+ */
+ public static UUID cast(Object value) {
+ // It would be better to generate Expression tree that is equivalent
to the code below
+ // from type checking rules for this type in order to avoid code
duplication.
+ if (value instanceof String) {
+ return UUID.fromString((String) value);
+ } else {
+ return (UUID) value;
+ }
+ }
}
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
new file mode 100644
index 0000000000..2c77a6c44f
--- /dev/null
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/SafeCustomTypeInternalConversion.java
@@ -0,0 +1,79 @@
+/*
+ * 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.util;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.schema.NativeTypeSpec;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * The sole purpose of this class is to provide safe-conversion to and from
internal types,
+ * preventing class cast exceptions inside when using {@link TypeUtils}
methods for to/from type conversion.
+ */
+final class SafeCustomTypeInternalConversion {
+
+ static final SafeCustomTypeInternalConversion INSTANCE = new
SafeCustomTypeInternalConversion(Commons.typeFactory());
+
+ private final Map<NativeTypeSpec, Class<?>> internalTypes;
+
+ private SafeCustomTypeInternalConversion(IgniteTypeFactory typeFactory) {
+ // IgniteCustomType: We can automatically compute val -> internal type
mapping
+ // by using type specs from the type factory.
+ var customTypes = typeFactory.getCustomTypeSpecs();
+
+ internalTypes = customTypes.values()
+ .stream()
+ .map(t -> Map.entry(t.nativeType().spec(), t.storageType()))
+ .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
+ }
+
+ @Nullable
+ Object tryConvertToInternal(Object val, NativeTypeSpec storageType) {
+ Class<?> internalType = internalTypes.get(storageType);
+ if (internalType == null) {
+ return null;
+ }
+
+ //TODO: This a quick fix of the issue
https://issues.apache.org/jira/browse/IGNITE-18831 and should be reworked.
+ if (storageType == NativeTypeSpec.UUID && val instanceof String) {
+ return UUID.fromString((String) val);
+ }
+
+ assert internalType.isInstance(val) : storageTypeMismatch(val,
internalType);
+ return val;
+ }
+
+ @Nullable
+ Object tryConvertFromInternal(Object val, NativeTypeSpec storageType) {
+ Class<?> internalType = internalTypes.get(storageType);
+ if (internalType == null) {
+ return null;
+ }
+
+ assert internalType.isInstance(val) : storageTypeMismatch(val,
internalType);
+ return val;
+ }
+
+ 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);
+ }
+}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
index 0fe3988738..45254bd4f0 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
@@ -32,7 +32,6 @@ import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -48,6 +47,7 @@ import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.ignite.internal.schema.DecimalNativeType;
import org.apache.ignite.internal.schema.NativeType;
+import org.apache.ignite.internal.schema.NativeTypeSpec;
import org.apache.ignite.internal.schema.NumberNativeType;
import org.apache.ignite.internal.schema.TemporalNativeType;
import org.apache.ignite.internal.schema.VarlenNativeType;
@@ -55,7 +55,6 @@ import
org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
import org.apache.ignite.internal.sql.engine.exec.RowHandler;
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.UuidFunctions;
import org.apache.ignite.internal.sql.engine.type.UuidType;
import org.apache.ignite.sql.ColumnType;
import org.jetbrains.annotations.NotNull;
@@ -239,9 +238,13 @@ public class TypeUtils {
}
/**
- * ToInternal.
- * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
+ * ToInternal. Converts the given value to its presentation used by the
execution engine.
+ *
+ * @deprecated The implementation of this method is incorrect because it
relies on the assumption that
+ * {@code val.getClass() == storageType(val)} is always true, which
sometimes is not the case.
+ * Use {@link #toInternal(ExecutionContext, Object, Type)} that
provides type information instead.
*/
+ @Deprecated
public static @Nullable Object toInternal(ExecutionContext<?> ectx, Object
val) {
return val == null ? null : toInternal(ectx, val, val.getClass());
}
@@ -281,17 +284,17 @@ public class TypeUtils {
Double.class.equals(storageType) || double.class.equals(storageType)
?
SqlFunctions.toDouble(num) :
BigDecimal.class.equals(storageType) ? SqlFunctions.toBigDecimal(num) : num;
- } else if (storageType == UUID.class) {
- //ToDo: This a quick fix of the issue
https://issues.apache.org/jira/browse/IGNITE-18831 and should be reworked.
- if (val instanceof String) {
- return UuidFunctions.cast(val);
- } else {
- assert val instanceof UUID : storageTypeMismatch(val,
UUID.class);
+ } else {
+ // TODO: https://issues.apache.org/jira/browse/IGNITE-17298 SQL:
Support BOOLEAN datatype.
+ // Fix this after BOOLEAN type supported is implemented.
+ if (storageType == Boolean.class) {
return val;
}
- } else {
- // IgniteCustomType: Add storageTypeMismatch assertion for your
type.
- return val;
+ var nativeTypeSpec = NativeTypeSpec.fromClass((Class<?>)
storageType);
+ assert nativeTypeSpec != null : "No native type spec for type: " +
storageType;
+
+ var customType =
SafeCustomTypeInternalConversion.INSTANCE.tryConvertToInternal(val,
nativeTypeSpec);
+ return customType != null ? customType : val;
}
}
@@ -299,7 +302,7 @@ public class TypeUtils {
* FromInternal.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
- public static Object fromInternal(ExecutionContext<?> ectx, Object val,
Type storageType) {
+ public static @Nullable Object fromInternal(ExecutionContext<?> ectx,
Object val, Type storageType) {
if (val == null) {
return null;
} else if (storageType == LocalDate.class && val instanceof Integer) {
@@ -315,12 +318,17 @@ public class TypeUtils {
return Period.of((Integer) val / 12, (Integer) val % 12, 0);
} else if (storageType == byte[].class && val instanceof ByteString) {
return ((ByteString) val).getBytes();
- } else if (storageType == UUID.class) {
- assert val instanceof UUID : storageTypeMismatch(val, UUID.class);
- return val;
} else {
- // IgniteCustomType: Add storageTypeMismatch assertion for your
type.
- return val;
+ // TODO: https://issues.apache.org/jira/browse/IGNITE-17298 SQL:
Support BOOLEAN datatype.
+ // Fix this after BOOLEAN type supported is implemented.
+ if (storageType == Boolean.class) {
+ return val;
+ }
+ var nativeTypeSpec = NativeTypeSpec.fromClass((Class<?>)
storageType);
+ assert nativeTypeSpec != null : "No native type spec for type: " +
storageType;
+
+ var customType =
SafeCustomTypeInternalConversion.INSTANCE.tryConvertFromInternal(val,
nativeTypeSpec);
+ return customType != null ? customType : val;
}
}
@@ -363,7 +371,7 @@ public class TypeUtils {
case ANY:
if (type instanceof IgniteCustomType) {
IgniteCustomType customType = (IgniteCustomType) type;
- return customType.columnType();
+ return customType.spec().columnType();
}
// fallthrough
case OTHER:
@@ -480,8 +488,4 @@ public class TypeUtils {
throw new IllegalStateException("Unexpected native type " +
nativeType);
}
}
-
- 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);
- }
}
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/TypeUtilsTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/TypeUtilsTest.java
new file mode 100644
index 0000000000..c318753af5
--- /dev/null
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/TypeUtilsTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.lang.reflect.Type;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.UUID;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Tests for {@link TypeUtils}.
+ */
+@ExtendWith(MockitoExtension.class)
+public class TypeUtilsTest {
+
+ @Mock
+ private ExecutionContext<?> ectx;
+
+ /**
+ * Checks that conversions to and from internal types is consistent.
+ *
+ * @see TypeUtils#toInternal(ExecutionContext, Object, Type) to internal.
+ * @see TypeUtils#fromInternal(ExecutionContext, Object, Type) from
internal.
+ */
+ @ParameterizedTest
+ @MethodSource("valueAndType")
+ public void testToFromInternalMatch(Object value, Class<?> type) {
+ Object internal = TypeUtils.toInternal(ectx, value, type);
+ assertNotNull(internal, "Conversion to internal has produced null");
+
+ Object original = TypeUtils.fromInternal(ectx, internal, type);
+ assertEquals(value, original, "toInternal -> fromInternal");
+ assertNotNull(original, "Conversion from internal has produced null");
+
+ Object internal2 = TypeUtils.toInternal(ectx, original);
+ assertEquals(internal, internal2, "toInternal w/o type parameter");
+ }
+
+ private static Stream<Arguments> valueAndType() {
+ return Stream.of(
+ Arguments.of((byte) 1, Byte.class),
+ Arguments.of((short) 1, Short.class),
+ Arguments.of(1, Integer.class),
+ Arguments.of(1L, Long.class),
+ Arguments.of(1.0F, Float.class),
+ Arguments.of(1.0D, Double.class),
+ Arguments.of("hello", String.class),
+ Arguments.of(LocalDate.of(1970, 1, 1), LocalDate.class),
+ Arguments.of(LocalDateTime.of(1970, 1, 1, 0, 0, 0, 0),
LocalDateTime.class),
+ Arguments.of(LocalTime.NOON, LocalTime.class),
+ Arguments.of(new UUID(1, 1), UUID.class)
+ );
+ }
+}