This is an automated email from the ASF dual-hosted git repository.
tkalkirill pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new 0a3288a114a IGNITE-29045 SQL Calcite: Support Java temporal types in
UDF and UDTF parameters and results (#13569)
0a3288a114a is described below
commit 0a3288a114a8682ec832bed7bd05ab51af4ad9eb
Author: Kirill Tkalenko <[email protected]>
AuthorDate: Fri Sep 25 17:47:49 2026 +0300
IGNITE-29045 SQL Calcite: Support Java temporal types in UDF and UDTF
parameters and results (#13569)
---
.../query/calcite/exec/LogicalRelImplementor.java | 4 +-
.../query/calcite/exec/TableFunctionScan.java | 59 +-
.../query/calcite/exec/exp/ConverterUtils.java | 55 +-
.../calcite/exec/exp/IgniteFunctionParameter.java | 54 ++
.../exec/exp/IgniteReflectiveFunctionBase.java | 14 +
.../calcite/exec/exp/IgniteScalarFunction.java | 5 +-
.../exec/exp/ReflectiveCallNotNullImplementor.java | 8 +-
.../query/calcite/prepare/IgniteTypeCoercion.java | 31 +
.../processors/query/calcite/type/OtherType.java | 14 +-
.../processors/query/calcite/util/TypeUtils.java | 67 +-
.../query/calcite/integration/DataTypesTest.java | 20 +
.../UserDefinedFunctionsIntegrationTest.java | 677 +++++++++++++++++++++
.../query/calcite/type/OtherTypeTest.java | 68 +++
.../query/calcite/util/TypeUtilsTest.java | 141 +++++
.../apache/ignite/testsuites/UtilTestSuite.java | 4 +
15 files changed, 1188 insertions(+), 33 deletions(-)
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
index 7b2172c33ac..ac39e7c6a4b 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
@@ -784,9 +784,7 @@ public class LogicalRelImplementor<Row> implements
IgniteRelVisitor<Node<Row>> {
RelDataType rowType = rel.getRowType();
- RowFactory<Row> rowFactory =
ctx.rowHandler().factory(ctx.getTypeFactory(), rowType);
-
- return new ScanNode<>(ctx, rowType, new TableFunctionScan<>(rowType,
dataSupplier, rowFactory));
+ return new ScanNode<>(ctx, rowType, new TableFunctionScan<>(ctx,
rowType, dataSupplier));
}
/** {@inheritDoc} */
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/TableFunctionScan.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/TableFunctionScan.java
index b29f91d6a7f..74d7e2cd298 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/TableFunctionScan.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/TableFunctionScan.java
@@ -17,16 +17,26 @@
package org.apache.ignite.internal.processors.query.calcite.exec;
+import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Iterator;
import java.util.function.Supplier;
+import org.apache.calcite.linq4j.tree.Primitive;
+import org.apache.calcite.linq4j.tree.Types;
import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.ignite.internal.processors.query.IgniteSQLException;
import
org.apache.ignite.internal.processors.query.calcite.exec.RowHandler.RowFactory;
+import org.apache.ignite.internal.processors.query.calcite.type.OtherType;
+import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils;
import org.apache.ignite.internal.util.typedef.F;
+import org.jetbrains.annotations.Nullable;
/** */
public class TableFunctionScan<Row> implements Iterable<Row> {
+ /** */
+ private final ExecutionContext<Row> ctx;
+
/** */
private final RelDataType rowType;
@@ -36,15 +46,22 @@ public class TableFunctionScan<Row> implements
Iterable<Row> {
/** */
private final RowFactory<Row> rowFactory;
+ /** */
+ private final boolean hasConvertableFields;
+
/** */
public TableFunctionScan(
+ ExecutionContext<Row> ctx,
RelDataType rowType,
- Supplier<Iterable<?>> dataSupplier,
- RowFactory<Row> rowFactory
+ Supplier<Iterable<?>> dataSupplier
) {
+ this.ctx = ctx;
this.rowType = rowType;
this.dataSupplier = dataSupplier;
- this.rowFactory = rowFactory;
+
+ rowFactory = ctx.rowHandler().factory(ctx.getTypeFactory(), rowType);
+
+ hasConvertableFields = hasConvertableFields(ctx, rowType);
}
/** {@inheritDoc} */
@@ -66,6 +83,42 @@ public class TableFunctionScan<Row> implements Iterable<Row>
{
+ "] doesn't match defined columns number [" +
rowType.getFieldCount() + "].");
}
+ if (hasConvertableFields) {
+ if (rowContainer.getClass() == Object[].class)
+ rowArr = rowArr.clone();
+
+ for (int i = 0; i < rowArr.length; i++)
+ rowArr[i] = convertToInternal(rowArr[i],
rowType.getFieldList().get(i).getType());
+ }
+
return rowFactory.create(rowArr);
}
+
+ /** */
+ private @Nullable Object convertToInternal(@Nullable Object val,
RelDataType type) {
+ // Preserve objects for both Ignite's custom OTHER type and Calcite's
SQL OTHER type.
+ if (val == null || type instanceof OtherType || type.getSqlTypeName()
== SqlTypeName.OTHER)
+ return val;
+
+ Type storageType = ctx.getTypeFactory().getResultClass(type);
+
+ if (!TypeUtils.isConvertableType(storageType))
+ return val;
+
+ // SQL table functions can already return values in the internal
representation.
+ if
(Types.isAssignableFrom(Primitive.box(ctx.getTypeFactory().getJavaClass(type)),
val.getClass()))
+ return val;
+
+ return TypeUtils.toInternal(ctx, val, storageType);
+ }
+
+ /** */
+ private static boolean hasConvertableFields(ExecutionContext<?> ctx,
RelDataType rowType) {
+ return rowType.getFieldList().stream().anyMatch(field -> {
+ RelDataType type = field.getType();
+
+ return !(type instanceof OtherType) && type.getSqlTypeName() !=
SqlTypeName.OTHER
+ &&
TypeUtils.isConvertableType(ctx.getTypeFactory().getResultClass(type));
+ });
+ }
}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ConverterUtils.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ConverterUtils.java
index 3d61ad80048..8dc7224dcea 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ConverterUtils.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ConverterUtils.java
@@ -38,6 +38,8 @@ import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.util.BuiltInMethod;
import org.apache.calcite.util.Util;
import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils;
+import org.jetbrains.annotations.Nullable;
/** */
public class ConverterUtils {
@@ -53,6 +55,25 @@ public class ConverterUtils {
return toInternal(operand, operand.getType(), targetType);
}
+ /** Converts a user-defined function result to the internal representation
using the execution context. */
+ static Expression toInternal(Expression root, Expression operand, Type
targetType) {
+ Type fromType = operand.getType();
+
+ if (!TypeUtils.isConvertableType(fromType))
+ return operand;
+
+ // Preserve Calcite's calendar conversion for JDBC dates and
timestamps.
+ Expression converted = toInternal(operand, targetType);
+
+ if (converted != operand)
+ return converted;
+
+ return Expressions.convert_(
+ Expressions.call(TypeUtils.class, "toInternal", root, operand,
Expressions.constant(fromType)),
+ targetType
+ );
+ }
+
/** */
private static Expression toInternal(Expression operand,
Type fromType, Type targetType) {
@@ -123,10 +144,18 @@ public class ConverterUtils {
/** */
static List<Expression> fromInternal(Class<?>[] targetTypes,
List<Expression> expressions) {
- final List<Expression> list = new ArrayList<>();
+ return fromInternal(null, targetTypes, expressions);
+ }
+
+ /** Converts user-defined function arguments using the execution context
when available. */
+ static List<Expression> fromInternal(@Nullable Expression root,
+ Class<?>[] targetTypes,
+ List<Expression> expressions
+ ) {
+ final List<Expression> list = new ArrayList<>(expressions.size());
if (targetTypes.length == expressions.size()) {
for (int i = 0; i < expressions.size(); i++)
- list.add(fromInternal(expressions.get(i), targetTypes[i]));
+ list.add(fromInternal(root, expressions.get(i),
targetTypes[i]));
}
else {
int j = 0;
@@ -139,12 +168,32 @@ public class ConverterUtils {
else
type = targetTypes[j].getComponentType();
- list.add(fromInternal(expressions.get(i), type));
+ list.add(fromInternal(root, expressions.get(i), type));
}
}
return list;
}
+ /** */
+ private static Expression fromInternal(@Nullable Expression root,
Expression operand, Type targetType) {
+ if (Types.isAssignableFrom(targetType, operand.getType()))
+ return operand;
+
+ // Preserve Calcite's calendar conversion for JDBC dates and
timestamps.
+ Expression converted = fromInternal(operand, targetType);
+
+ if (root == null || converted != operand ||
!TypeUtils.isConvertableType(targetType))
+ return converted;
+
+ if (Primitive.is(operand.getType()))
+ operand = Expressions.box(operand);
+
+ return Expressions.convert_(
+ Expressions.call(TypeUtils.class, "fromInternal", root, operand,
Expressions.constant(targetType)),
+ targetType
+ );
+ }
+
/** */
private static Type toInternal(RelDataType type) {
return toInternal(type, false);
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteFunctionParameter.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteFunctionParameter.java
new file mode 100644
index 00000000000..f311b192ffb
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteFunctionParameter.java
@@ -0,0 +1,54 @@
+/*
+ * 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.processors.query.calcite.exec.exp;
+
+import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.schema.FunctionParameter;
+
+/** Function parameter that exposes its SQL type to validation. */
+class IgniteFunctionParameter implements FunctionParameter {
+ /** */
+ private final FunctionParameter delegate;
+
+ /** */
+ IgniteFunctionParameter(FunctionParameter delegate) {
+ this.delegate = delegate;
+ }
+
+ /** {@inheritDoc} */
+ @Override public int getOrdinal() {
+ return delegate.getOrdinal();
+ }
+
+ /** {@inheritDoc} */
+ @Override public String getName() {
+ return delegate.getName();
+ }
+
+ /** {@inheritDoc} */
+ @Override public RelDataType getType(RelDataTypeFactory typeFactory) {
+ // Normalize UDF metadata without losing Java types used to convert
query results.
+ return
((JavaTypeFactory)typeFactory).toSql(delegate.getType(typeFactory));
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean isOptional() {
+ return delegate.isOptional();
+ }
+}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteReflectiveFunctionBase.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteReflectiveFunctionBase.java
index 1a5dcf8b045..6971197cb85 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteReflectiveFunctionBase.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteReflectiveFunctionBase.java
@@ -17,18 +17,32 @@
package org.apache.ignite.internal.processors.query.calcite.exec.exp;
import java.lang.reflect.Method;
+import java.util.List;
+import org.apache.calcite.schema.FunctionParameter;
import org.apache.calcite.schema.impl.ReflectiveFunctionBase;
+import static java.util.stream.Collectors.toUnmodifiableList;
+
/** A base for outer java-method functions. */
abstract class IgniteReflectiveFunctionBase extends ReflectiveFunctionBase
implements ImplementableFunction {
/** */
protected final CallImplementor implementor;
+ /** */
+ private final List<FunctionParameter> params;
+
/** */
protected IgniteReflectiveFunctionBase(Method method, CallImplementor
implementor) {
super(method);
this.implementor = implementor;
+
+ params =
super.getParameters().stream().map(IgniteFunctionParameter::new).collect(toUnmodifiableList());
+ }
+
+ /** {@inheritDoc} */
+ @Override public List<FunctionParameter> getParameters() {
+ return params;
}
/** {@inheritDoc} */
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteScalarFunction.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteScalarFunction.java
index 09f377e3560..39cfb5afa6c 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteScalarFunction.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteScalarFunction.java
@@ -18,6 +18,7 @@ package
org.apache.ignite.internal.processors.query.calcite.exec.exp;
import java.lang.reflect.Method;
import org.apache.calcite.adapter.enumerable.NullPolicy;
+import org.apache.calcite.adapter.java.JavaTypeFactory;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.schema.ScalarFunction;
@@ -54,7 +55,9 @@ public class IgniteScalarFunction extends
IgniteReflectiveFunctionBase implement
/** {@inheritDoc} */
@Override public RelDataType getReturnType(RelDataTypeFactory typeFactory)
{
- return typeFactory.createJavaType(method.getReturnType());
+ JavaTypeFactory tf = (JavaTypeFactory)typeFactory;
+
+ return tf.toSql(tf.createJavaType(method.getReturnType()));
}
/**
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ReflectiveCallNotNullImplementor.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ReflectiveCallNotNullImplementor.java
index 0f8958c4e5a..fee01452d5e 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ReflectiveCallNotNullImplementor.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ReflectiveCallNotNullImplementor.java
@@ -49,10 +49,10 @@ public class ReflectiveCallNotNullImplementor implements
NotNullImplementor {
@Override public Expression implement(RexToLixTranslator translator,
RexCall call, List<Expression> translatedOperands) {
translatedOperands =
- ConverterUtils.fromInternal(method.getParameterTypes(),
translatedOperands);
+ ConverterUtils.fromInternal(translator.getRoot(),
method.getParameterTypes(), translatedOperands);
translatedOperands =
ConverterUtils.convertAssignableTypes(method.getParameterTypes(),
translatedOperands);
- final Expression callExpr;
+ Expression callExpr;
if ((method.getModifiers() & Modifier.STATIC) != 0)
callExpr = Expressions.call(method, translatedOperands);
@@ -66,6 +66,10 @@ public class ReflectiveCallNotNullImplementor implements
NotNullImplementor {
callExpr = Expressions.call(target, method, translatedOperands);
}
+
+ callExpr = ConverterUtils.toInternal(translator.getRoot(), callExpr,
+ translator.typeFactory.getJavaClass(call.getType()));
+
if (!containsCheckedException(method))
return callExpr;
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteTypeCoercion.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteTypeCoercion.java
index 96eb4233f1b..6e5924f7607 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteTypeCoercion.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteTypeCoercion.java
@@ -19,6 +19,7 @@ package
org.apache.ignite.internal.processors.query.calcite.prepare;
import java.nio.charset.Charset;
import java.util.Arrays;
+import java.util.List;
import org.apache.calcite.adapter.java.JavaTypeFactory;
import org.apache.calcite.rel.type.DynamicRecordType;
import org.apache.calcite.rel.type.RelDataType;
@@ -29,6 +30,7 @@ import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlCallBinding;
import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.SqlDataTypeSpec;
+import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
@@ -37,6 +39,7 @@ import org.apache.calcite.sql.SqlTypeNameSpec;
import org.apache.calcite.sql.SqlUserDefinedTypeNameSpec;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlOperandMetadata;
import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
@@ -59,6 +62,34 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
super(typeFactory, validator);
}
+ /** {@inheritDoc} */
+ @Override public boolean userDefinedFunctionCoercion(SqlValidatorScope
scope, SqlCall call, SqlFunction function) {
+ SqlOperandMetadata metadata =
(SqlOperandMetadata)function.getOperandTypeChecker();
+ List<RelDataType> paramTypes = metadata.paramTypes(factory);
+
+ for (int i = 0; i < call.operandCount(); i++) {
+ SqlNode operand = call.operand(i);
+ int paramIdx = i;
+
+ if (operand.getKind() == SqlKind.ARGUMENT_ASSIGNMENT) {
+ SqlCall assignment = (SqlCall)operand;
+
+ paramIdx =
metadata.paramNames().indexOf(((SqlIdentifier)assignment.operand(1)).getSimple());
+ operand = assignment.operand(0);
+
+ if (paramIdx < 0)
+ return false;
+ }
+
+ // Numeric-to-timestamp casts are supported explicitly, but are
not temporal UDF arguments.
+ if (SqlTypeUtil.isDatetime(paramTypes.get(paramIdx))
+ && SqlTypeUtil.isNumeric(validator.deriveType(scope, operand)))
+ return false;
+ }
+
+ return super.userDefinedFunctionCoercion(scope, call, function);
+ }
+
/** {@inheritDoc} **/
@Override public boolean binaryComparisonCoercion(SqlCallBinding binding) {
// Although it is not reflected in the docs, this method is also
invoked for MAX, MIN (and other similar operators)
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/type/OtherType.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/type/OtherType.java
index 1a87a117ccd..7ac9aa55053 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/type/OtherType.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/type/OtherType.java
@@ -18,7 +18,6 @@
package org.apache.ignite.internal.processors.query.calcite.type;
import java.lang.reflect.Type;
-import org.jetbrains.annotations.Nullable;
/** OTHER SQL type for any value. */
public class OtherType extends IgniteCustomType {
@@ -29,21 +28,12 @@ public class OtherType extends IgniteCustomType {
/** {@inheritDoc} */
@Override protected void generateTypeString(StringBuilder sb, boolean
withDetail) {
- sb.append("OTHER");
+ // The digest must differ from Calcite's OTHER to keep the types
distinct in its shared type cache.
+ sb.append(withDetail ? "IGNITE_OTHER" : "OTHER");
}
/** @return Storage type */
@Override public Type storageType() {
return Object.class;
}
-
- /** {@inheritDoc} */
- @Override public boolean equals(@Nullable Object obj) {
- // Digest is the same for built-in Calcite's OTHER type, make sure we
get instance of correct class during
- // canonization.
- if (obj == null || obj.getClass() != getClass())
- return false;
-
- return super.equals(obj);
- }
}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
index 6056f2bbc7c..4c75d7113e8 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
@@ -30,8 +30,11 @@ import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneOffset;
import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
@@ -65,7 +68,6 @@ import org.apache.calcite.util.Pair;
import org.apache.calcite.util.TimeString;
import org.apache.calcite.util.TimestampString;
import org.apache.ignite.IgniteException;
-import
org.apache.ignite.internal.cache.query.index.sorted.inline.types.DateValueUtils;
import org.apache.ignite.internal.processors.query.IgniteSQLException;
import
org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
import org.apache.ignite.internal.processors.query.calcite.exec.RowHandler;
@@ -83,6 +85,14 @@ import static
org.apache.ignite.internal.processors.query.calcite.util.Commons.t
/** */
public class TypeUtils {
+ /**
+ * Cutover (1582-10-15) for the calendar used by {@link java.sql.Date} and
{@link Timestamp}.
+ * These classes interpret earlier dates using the Julian calendar; {@link
LocalDate}, {@link LocalDateTime}
+ * and Calcite use the proleptic Gregorian calendar. Conversions must
preserve the calendar date.
+ */
+ private static final long GREGORIAN_CUTOVER =
+ LocalDate.of(1582, 10, 15).toEpochDay() * DateTimeUtils.MILLIS_PER_DAY;
+
/** */
private static final Set<Type> CONVERTABLE_TYPES = ImmutableSet.of(
java.util.Date.class,
@@ -409,23 +419,42 @@ public class TypeUtils {
* @return Millis value.
*/
private static long toLong(DataContext ctx, Object val) {
+ // Java time values have no time zone and use the proleptic Gregorian
calendar, as does Calcite.
if (val instanceof LocalDateTime)
- return
toLong(DateValueUtils.convertToTimestamp((LocalDateTime)val),
DataContext.Variable.TIME_ZONE.get(ctx));
+ return
((LocalDateTime)val).toInstant(ZoneOffset.UTC).toEpochMilli();
if (val instanceof LocalDate)
- return toLong(DateValueUtils.convertToSqlDate((LocalDate)val),
DataContext.Variable.TIME_ZONE.get(ctx));
+ return ((LocalDate)val).toEpochDay() *
DateTimeUtils.MILLIS_PER_DAY;
if (val instanceof LocalTime)
- return toLong(DateValueUtils.convertToSqlTime((LocalTime)val),
DataContext.Variable.TIME_ZONE.get(ctx));
+ return
TimeUnit.NANOSECONDS.toMillis(((LocalTime)val).toNanoOfDay());
- return toLong((java.util.Date)val,
DataContext.Variable.TIME_ZONE.get(ctx));
+ return toLong((java.util.Date)val, timeZone(ctx));
}
/** */
private static long toLong(java.util.Date val, TimeZone tz) {
long time = val.getTime();
+ long locTs = time + tz.getOffset(time);
+
+ // Optimization: skip calendar conversion when both calendars agree.
+ if (locTs >= GREGORIAN_CUTOVER)
+ return locTs;
+
+ // java.sql.Date and Timestamp use the Julian calendar before the
cutover, while Calcite uses the
+ // proleptic Gregorian calendar. Using only the time-zone-adjusted
millis would turn a java.sql.Date
+ // representing 1500-01-02 into SQL DATE '1500-01-11'. Preserve its
calendar fields instead.
+ Calendar cal = new GregorianCalendar(DateTimeUtils.UTC_ZONE,
Locale.ROOT);
- return time + tz.getOffset(time);
+ cal.setTimeInMillis(locTs);
+
+ int year = cal.get(Calendar.YEAR);
+
+ if (cal.get(Calendar.ERA) == GregorianCalendar.BC)
+ year = 1 - year;
+
+ return LocalDate.of(year, cal.get(Calendar.MONTH) + 1,
cal.get(Calendar.DAY_OF_MONTH)).toEpochDay()
+ * DateTimeUtils.MILLIS_PER_DAY + Math.floorMod(locTs,
DateTimeUtils.MILLIS_PER_DAY);
}
/** */
@@ -435,7 +464,7 @@ public class TypeUtils {
else if (storageType == java.sql.Date.class && val instanceof Integer)
return new java.sql.Date(fromLocalTs(ctx, (Integer)val *
DateTimeUtils.MILLIS_PER_DAY));
else if (storageType == LocalDate.class && val instanceof Integer)
- return new java.sql.Date(fromLocalTs(ctx, (Integer)val *
DateTimeUtils.MILLIS_PER_DAY)).toLocalDate();
+ return LocalDate.ofEpochDay((Integer)val);
else if (storageType == java.sql.Time.class && val instanceof Integer)
return new java.sql.Time(fromLocalTs(ctx, (Integer)val));
else if (storageType == LocalTime.class && val instanceof Integer)
@@ -443,7 +472,7 @@ public class TypeUtils {
else if (storageType == Timestamp.class && val instanceof Long)
return new Timestamp(fromLocalTs(ctx, (Long)val));
else if (storageType == LocalDateTime.class && val instanceof Long)
- return new Timestamp(fromLocalTs(ctx,
(Long)val)).toLocalDateTime();
+ return LocalDateTime.ofInstant(Instant.ofEpochMilli((Long)val),
ZoneOffset.UTC);
else if (storageType == java.util.Date.class && val instanceof Long)
return new java.util.Date(fromLocalTs(ctx, (Long)val));
else if (storageType == Duration.class && val instanceof Long)
@@ -514,7 +543,20 @@ public class TypeUtils {
/** */
private static long fromLocalTs(DataContext ctx, long ts) {
- TimeZone tz = DataContext.Variable.TIME_ZONE.get(ctx);
+ if (ts < GREGORIAN_CUTOVER) {
+ LocalDate date = LocalDate.ofEpochDay(Math.floorDiv(ts,
DateTimeUtils.MILLIS_PER_DAY));
+ Calendar cal = new GregorianCalendar(DateTimeUtils.UTC_ZONE,
Locale.ROOT);
+
+ cal.clear();
+ cal.set(Calendar.ERA, date.getYear() > 0 ? GregorianCalendar.AD :
GregorianCalendar.BC);
+ cal.set(date.getYear() > 0 ? date.getYear() : 1 - date.getYear(),
date.getMonthValue() - 1,
+ date.getDayOfMonth());
+
+ // Reconstruct the date in the legacy Java calendar before
applying the query's time zone.
+ ts = cal.getTimeInMillis() + Math.floorMod(ts,
DateTimeUtils.MILLIS_PER_DAY);
+ }
+
+ TimeZone tz = timeZone(ctx);
// Taking into account DST, offset can be changed after converting
from UTC to time-zone.
return ts - tz.getOffset(ts - tz.getOffset(ts));
@@ -528,4 +570,11 @@ public class TypeUtils {
return rexBuilder.makeLiteral(dfltVal, type, true);
}
+
+ /** */
+ private static TimeZone timeZone(DataContext ctx) {
+ TimeZone tz = DataContext.Variable.TIME_ZONE.get(ctx);
+
+ return tz != null ? tz : TimeZone.getDefault();
+ }
}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
index e3f9d5e7cd9..5922949257b 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
@@ -199,6 +199,26 @@ public class DataTypesTest extends
AbstractBasicIntegrationTransactionalTest {
.check();
}
+ /** Tests that implicit casts use the registered SQL name of OTHER. */
+ @Test
+ public void testOtherTypeImplicitCast() {
+ sql("CREATE TABLE t(id INT, oth OTHER) WITH " + atomicity());
+
+ sql("INSERT INTO t VALUES (1, 'str1')");
+ sql("INSERT INTO t VALUES (?, ?)", 2, "str2");
+
+ sql("INSERT INTO t SELECT 3, 42");
+ sql("INSERT INTO t SELECT ?, ?", 4, 69);
+
+ assertQuery("SELECT oth FROM t ORDER BY id")
+ .ordered()
+ .returns("str1")
+ .returns("str2")
+ .returns(42)
+ .returns(69)
+ .check();
+ }
+
/** Tests UUID without index. */
@Test
public void testUuidWithoutIndex() {
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java
index e5f8e569c99..ed3654f13dc 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java
@@ -17,11 +17,21 @@
package org.apache.ignite.internal.processors.query.calcite.integration;
+import java.io.Serializable;
import java.math.BigDecimal;
+import java.sql.Date;
+import java.sql.Time;
import java.sql.Timestamp;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Period;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
+import java.util.TimeZone;
import java.util.stream.Collectors;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.sql.validate.SqlValidatorException;
@@ -37,11 +47,13 @@ import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.processors.query.IgniteSQLException;
import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.ListeningTestLogger;
import org.apache.ignite.testframework.LogListener;
import org.apache.ignite.testframework.junits.WithSystemProperty;
+import org.hamcrest.CoreMatchers;
import org.junit.Test;
import static
org.apache.ignite.internal.processors.query.calcite.CalciteQueryProcessor.IGNITE_CALCITE_USE_QUERY_BLOCKING_TASK_EXECUTOR;
@@ -492,6 +504,383 @@ public class UserDefinedFunctionsIntegrationTest extends
AbstractBasicIntegratio
assertQuery("SELECT udf.decimalToInt(5.3)").returns(5).check();
}
+ /** */
+ @Test
+ public void testObjectTableFunctionResult() {
+ client.getOrCreateCache(new
CacheConfiguration<>("object-table-functions")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(CustomTypeFunctionsLibrary.class));
+
+ Object[] exp = temporalValues();
+
+ assertQuery("SELECT * FROM objectTableValues()")
+ .withResultChecker(rows -> {
+ assertEquals(1, rows.size());
+ assertEquals(exp.length, rows.get(0).size());
+
+ for (int i = 0; i < exp.length; i++) {
+ Object actual = rows.get(0).get(i);
+
+ assertEquals("Unexpected value type at index " + i,
exp[i].getClass(), actual.getClass());
+ assertEquals("Unexpected value at index " + i, exp[i],
actual);
+ }
+ })
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testSerializableTableFunctionResult() {
+ client.getOrCreateCache(new
CacheConfiguration<>("serializable-table-functions")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(SerializableFunctionsLibrary.class));
+
+ assertQuery("SELECT * FROM serializableTableValues()")
+ .withResultChecker(rows -> {
+ assertEquals(1, rows.size());
+ assertEquals(1, rows.get(0).size());
+ assertEquals(Date.class, rows.get(0).get(0).getClass());
+ assertEquals(Date.valueOf("2020-01-01"), rows.get(0).get(0));
+ })
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testTemporalFunctions() {
+ client.getOrCreateCache(new
CacheConfiguration<>("temporal-table-functions")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(TemporalFunctionsLibrary.class));
+
+ assertQuery("SELECT checkTemporalTypes(?, ?, ?, ?, ?, ?, ?, ?, ?)")
+ .withParams(temporalValues())
+ .returns(true)
+ .check();
+
+ assertQuery("SELECT EXTRACT(YEAR FROM udfUtilDateValue()), EXTRACT(DAY
FROM udfDateValue()), "
+ + "EXTRACT(HOUR FROM udfTimeValue()), EXTRACT(YEAR FROM
udfTimestampValue()), "
+ + "EXTRACT(DAY FROM udfLocalDateValue()), EXTRACT(HOUR FROM
udfLocalTimeValue()), "
+ + "EXTRACT(YEAR FROM udfLocalDateTimeValue()), EXTRACT(DAY FROM
udfDurationValue()), "
+ + "EXTRACT(HOUR FROM udfDurationValue()), EXTRACT(MINUTE FROM
udfDurationValue()), "
+ + "EXTRACT(YEAR FROM udfPeriodValue()), EXTRACT(MONTH FROM
udfPeriodValue())")
+ .returns(2020L, 15L, 2L, 2021L, 16L, 3L, 2023L, 1L, 2L, 3L, 1L, 2L)
+ .check();
+
+ assertQuery("SELECT EXTRACT(YEAR FROM util_date), EXTRACT(DAY FROM
sql_date), "
+ + "EXTRACT(HOUR FROM sql_time), EXTRACT(YEAR FROM sql_timestamp), "
+ + "EXTRACT(DAY FROM local_date), EXTRACT(HOUR FROM local_time), "
+ + "EXTRACT(YEAR FROM local_timestamp), EXTRACT(DAY FROM
duration_value), "
+ + "EXTRACT(HOUR FROM duration_value), EXTRACT(MINUTE FROM
duration_value), "
+ + "EXTRACT(YEAR FROM period_value), EXTRACT(MONTH FROM
period_value) "
+ + "FROM temporalTable(?, ?, ?, ?, ?, ?, ?, ?, ?)")
+ .withParams(temporalValues())
+ .returns(2020L, 15L, 2L, 2021L, 16L, 3L, 2023L, 1L, 2L, 3L, 1L, 2L)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testHistoricalTemporalFunctions() {
+ client.getOrCreateCache(new
CacheConfiguration<>("historical-temporal-functions")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(TemporalFunctionsLibrary.class,
DeterministicTemporalFunctionsLibrary.class));
+
+ assertQuery("SELECT detDateToStr(DATE '1500-01-02'), "
+ + "detTimestampToStr(TIMESTAMP '1500-01-02 03:04:05')")
+ .returns("1500-01-02", "1500-01-02 03:04:05.0")
+ .check();
+
+ assertQuery("SELECT CAST(udfDateFromString('1500-01-02') AS VARCHAR), "
+ + "EXTRACT(DAY FROM udfDateFromString('1500-01-02'))")
+ .returns("1500-01-02", 2L)
+ .check();
+
+ assertQuery("SELECT CAST(udfTimestampFromString('1500-01-02 03:04:05')
AS VARCHAR), "
+ + "EXTRACT(DAY FROM udfTimestampFromString('1500-01-02
03:04:05'))")
+ .returns("1500-01-02 03:04:05", 2L)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testTemporalScalarFunctionResultSubtypes() {
+ client.getOrCreateCache(new
CacheConfiguration<>("temporal-scalar-result-subtypes")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(TemporalFunctionsLibrary.class));
+
+ assertQuery("SELECT udfDateAsUtilDate()")
+ .returns(Timestamp.valueOf("2020-01-01 00:00:00"))
+ .check();
+
+ assertQuery("SELECT EXTRACT(YEAR FROM udfDateAsUtilDate()),
EXTRACT(HOUR FROM udfDateAsUtilDate())")
+ .returns(2020L, 0L)
+ .check();
+
+ assertQuery("SELECT udfTimeAsUtilDate()")
+ .returns(Timestamp.valueOf("1970-01-01 02:03:04"))
+ .check();
+
+ assertQuery("SELECT EXTRACT(YEAR FROM udfTimeAsUtilDate()),
EXTRACT(HOUR FROM udfTimeAsUtilDate())")
+ .returns(1970L, 2L)
+ .check();
+
+ assertQuery("SELECT udfTimestampAsUtilDate()")
+ .returns(Timestamp.valueOf("2021-01-15 03:04:05"))
+ .check();
+
+ assertQuery("SELECT EXTRACT(YEAR FROM udfTimestampAsUtilDate()), "
+ + "EXTRACT(HOUR FROM udfTimestampAsUtilDate())")
+ .returns(2021L, 3L)
+ .check();
+
+ assertQuery("SELECT udfNullUtilDate()")
+ .returns(NULL_RESULT)
+ .check();
+
+ assertQuery("SELECT EXTRACT(YEAR FROM udfNullUtilDate()), EXTRACT(HOUR
FROM udfNullUtilDate())")
+ .returns(null, null)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testTemporalTableFunctionResultSubtypes() {
+ client.getOrCreateCache(new
CacheConfiguration<>("temporal-table-result-subtypes")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(TemporalFunctionsLibrary.class));
+
+ assertQuery("SELECT d FROM utilDateSubtypeTable()")
+ .returns(Timestamp.valueOf("2020-01-01 00:00:00"))
+ .returns(Timestamp.valueOf("1970-01-01 02:03:04"))
+ .returns(Timestamp.valueOf("2021-01-15 03:04:05"))
+ .returns(NULL_RESULT)
+ .check();
+
+ assertQuery("SELECT EXTRACT(YEAR FROM d), EXTRACT(HOUR FROM d) FROM
utilDateSubtypeTable()")
+ .returns(2020L, 0L)
+ .returns(1970L, 2L)
+ .returns(2021L, 3L)
+ .returns(null, null)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testJavaTimeFunctionParametersWithSqlTypeValues() {
+ client.getOrCreateCache(new CacheConfiguration<>("java-time-params")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(JavaTimeParametersFunctionsLibrary.class));
+
+ assertQuery("SELECT localDateToStr(?)")
+ .withParams(Date.valueOf("2022-02-16"))
+ .returns("2022-02-16")
+ .check();
+
+ assertQuery("SELECT localTimeToStr(?)")
+ .withParams(Time.valueOf("03:04:05"))
+ .returns("03:04:05")
+ .check();
+
+ assertQuery("SELECT localDateTimeToStr(?)")
+ .withParams(Timestamp.valueOf("2023-03-17 04:05:06"))
+ .returns("2023-03-17T04:05:06")
+ .check();
+
+ // Control: the opposite direction already works for java.sql
parameters.
+ assertQuery("SELECT sqlDateToStr(?)")
+ .withParams(LocalDate.of(2022, 2, 16))
+ .returns("2022-02-16")
+ .check();
+
+ // Incompatible values must be rejected by the validator.
+ assertThrows("SELECT sqlDateToStr(?)", SqlValidatorException.class,
+ "No match found for function signature SQLDATETOSTR(<NUMERIC>)",
5);
+ assertThrows("SELECT localDateToStr(?)", SqlValidatorException.class,
+ "No match found for function signature LOCALDATETOSTR(<NUMERIC>)",
5);
+ assertThrows("SELECT localTimeToStr(?)", SqlValidatorException.class,
+ "No match found for function signature LOCALTIMETOSTR(<NUMERIC>)",
5);
+ assertThrows("SELECT localDateTimeToStr(?)",
SqlValidatorException.class,
+ "No match found for function signature
LOCALDATETIMETOSTR(<NUMERIC>)", 5);
+ }
+
+ /** */
+ @Test
+ public void testJavaTimeFunctionParametersAfterTableQuery() {
+ sql("CREATE TABLE type_warmup(i INT)");
+
+ // Resolving the table's hidden columns caches Ignite OTHER before
Calcite infers UDF parameter types.
+ sql("SELECT * FROM type_warmup");
+
+ testJavaTimeFunctionParametersWithSqlTypeValues();
+ }
+
+ /** */
+ @Test
+ public void testHistoricalJavaTimeFunctions() {
+ client.getOrCreateCache(new
CacheConfiguration<>("historical-java-time-functions")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(JavaTimeParametersFunctionsLibrary.class));
+
+ checkJavaTimeFunctions("1500-01-02", "03:04:05");
+ checkJavaTimeFunctions("1582-10-04", "12:34:56");
+ checkJavaTimeFunctions("1582-10-15", "12:34:56");
+ checkJavaTimeFunctions("1969-12-31", "23:59:59");
+ }
+
+ /** */
+ @Test
+ public void testJavaTimeFunctionResultsAsJdbcValues() {
+ client.getOrCreateCache(new
CacheConfiguration<>("java-time-jdbc-results")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(JavaTimeParametersFunctionsLibrary.class));
+
+ for (String ts : new String[] {
+ "0001-01-01 00:00:00", "1500-01-02 03:04:05.123", "1582-10-04
23:59:59.999",
+ "1582-10-15 00:00:00", "1969-12-31 23:59:59.999", "1970-01-01
00:00:00", "2021-03-14 12:30:00.123"
+ }) {
+ LocalDateTime locTs = LocalDateTime.parse(ts.replace(' ', 'T'));
+ String date = locTs.toLocalDate().toString();
+ Date sqlDate = Date.valueOf(date);
+ Timestamp sqlTs = Timestamp.valueOf(ts);
+
+ // Check the JDBC values returned to the client, without
converting them to strings inside SQL.
+ assertQuery("SELECT localDateFromStr('" + date + "'),
localDateTimeFromStr('" + locTs + "')")
+ .returns(sqlDate, sqlTs)
+ .check();
+
+ assertQuery("SELECT d, ts FROM javaTimeValuesTable('" + date + "',
'"
+ + locTs.toLocalTime() + "', '" + locTs + "')")
+ .returns(sqlDate, sqlTs)
+ .check();
+
+ // The same JDBC values must retain their calendar fields when
passed back to SQL.
+ assertQuery("SELECT localDateToStr(?), localDateTimeToStr(?)")
+ .withParams(sqlDate, sqlTs)
+ .returns(date, locTs.toString())
+ .check();
+ }
+ }
+
+ /** */
+ @Test
+ public void testJavaTimeFunctionsDuringDstTransition() {
+ client.getOrCreateCache(new
CacheConfiguration<>("dst-java-time-functions")
+ .setSqlSchema("PUBLIC")
+ .setSqlFunctionClasses(JavaTimeParametersFunctionsLibrary.class));
+
+ // Initialize Calcite's cached default time zone before changing the
JVM default.
+ checkJavaTimeFunctions("2021-01-01", "12:00:00");
+
+ TimeZone oldTz = TimeZone.getDefault();
+
+ try {
+ TimeZone.setDefault(TimeZone.getTimeZone("America/New_York"));
+
+ checkJavaTimeFunctions("2021-03-14", "02:30:00");
+ checkJavaTimeFunctions("2021-11-07", "01:30:00");
+
+ TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Apia"));
+
+ // This local date was skipped when the time zone moved across the
date line.
+ checkJavaTimeFunctions("2011-12-30", "12:34:56");
+ }
+ finally {
+ TimeZone.setDefault(oldTz);
+ }
+ }
+
+ /** Checks Java time parameters and results independently, so opposite
conversion errors cannot cancel out. */
+ private void checkJavaTimeFunctions(String date, String time) {
+ LocalDate locDate = LocalDate.parse(date);
+ LocalTime locTime = LocalTime.parse(time);
+ LocalDateTime locTs = LocalDateTime.of(locDate, locTime);
+ String ts = date + ' ' + time;
+ String literals = "DATE '" + date + "', TIME '" + time + "', TIMESTAMP
'" + ts + '\'';
+
+ assertQuery("SELECT localDateToStr(DATE '" + date + "'),
localTimeToStr(TIME '" + time + "'), "
+ + "localDateTimeToStr(TIMESTAMP '" + ts + "')")
+ .returns(date, locTime.toString(), locTs.toString())
+ .check();
+
+ assertQuery("SELECT localDateToStr(?), localTimeToStr(?),
localDateTimeToStr(?)")
+ .withParams(locDate, locTime, locTs)
+ .returns(date, locTime.toString(), locTs.toString())
+ .check();
+
+ assertQuery("SELECT CAST(localDateFromStr('" + date + "') AS VARCHAR),
"
+ + "CAST(localTimeFromStr('" + time + "') AS VARCHAR), "
+ + "CAST(localDateTimeFromStr('" + locTs + "') AS VARCHAR)")
+ .returns(date, time, ts)
+ .check();
+
+ assertQuery("SELECT * FROM javaTimeStringsTable(" + literals + ')')
+ .returns(date, locTime.toString(), locTs.toString())
+ .check();
+
+ assertQuery("SELECT * FROM javaTimeStringsTable(?, ?, ?)")
+ .withParams(locDate, locTime, locTs)
+ .returns(date, locTime.toString(), locTs.toString())
+ .check();
+
+ assertQuery("SELECT CAST(d AS VARCHAR), CAST(t AS VARCHAR), CAST(ts AS
VARCHAR) "
+ + "FROM javaTimeValuesTable('" + date + "', '" + time + "', '" +
locTs + "')")
+ .returns(date, time, ts)
+ .check();
+ }
+
+ /** */
+ @Test
+ public void testDeterministicTemporalFunctionReduced() {
+ client.getOrCreateCache(new
CacheConfiguration<>("deterministic-temporal")
+ .setSqlSchema("PUBLIC")
+
.setSqlFunctionClasses(DeterministicTemporalFunctionsLibrary.class));
+
+ sql("CREATE TABLE reduce_tbl (id INT PRIMARY KEY, val INT)");
+ sql("INSERT INTO reduce_tbl VALUES (1, 1), (2, 2)");
+
+ // Control: a non-temporal argument is reduced.
+ assertReduced("SELECT id FROM reduce_tbl WHERE detIntToStr(1) = '1'",
"DETINTTOSTR");
+
+ assertReduced("SELECT id FROM reduce_tbl WHERE detDateToStr(DATE
'2020-01-01') = '2020-01-01'", "DETDATETOSTR");
+ assertReduced("SELECT id FROM reduce_tbl WHERE detTimeToStr(TIME
'02:03:04') = '02:03:04'", "DETTIMETOSTR");
+ assertReduced("SELECT id FROM reduce_tbl WHERE
detTimestampToStr(TIMESTAMP '2021-01-15 02:03:04') = "
+ + "'2021-01-15 02:03:04.0'", "DETTIMESTAMPTOSTR");
+ }
+
+ /** Checks that the function call is not present in the plan (reduced to a
constant) and the query result is correct. */
+ private void assertReduced(String sql, String fnName) {
+ assertQuery(sql)
+ .matches(CoreMatchers.not(QueryChecker.containsSubPlan(fnName)))
+ .returns(1)
+ .returns(2)
+ .check();
+ }
+
+ /** */
+ private static java.util.Date[] temporalSubtypeValues() {
+ return new java.util.Date[] {
+ Date.valueOf("2020-01-01"),
+ Time.valueOf("02:03:04"),
+ Timestamp.valueOf("2021-01-15 03:04:05"),
+ null
+ };
+ }
+
+ /** */
+ private static Object[] temporalValues() {
+ return new Object[] {
+ new java.util.Date(Timestamp.valueOf("2020-01-14
01:02:03").getTime()),
+ Date.valueOf("2021-01-15"),
+ Time.valueOf("02:03:04"),
+ Timestamp.valueOf("2021-01-15 02:03:04"),
+ LocalDate.of(2022, 2, 16),
+ LocalTime.of(3, 4, 5),
+ LocalDateTime.of(2023, 3, 17, 4, 5, 6),
+ Duration.ofDays(1).plusHours(2).plusMinutes(3),
+ Period.of(1, 2, 0)
+ };
+ }
+
/** */
@SuppressWarnings("ThrowableNotThrown")
private void assertThrows(String sql) {
@@ -914,4 +1303,292 @@ public class UserDefinedFunctionsIntegrationTest extends
AbstractBasicIntegratio
return LogListener.matches("Unable to register function '" + fun + "'.
Other function " +
"with the same name and parameters is already registered").build();
}
+
+ /** */
+ public static class SerializableFunctionsLibrary {
+ /** */
+ @QuerySqlTableFunction(columnTypes = {Serializable.class}, columnNames
= {"D"})
+ public static Iterable<Object[]> serializableTableValues() {
+ return Collections.singletonList(new Object[]
{Date.valueOf("2020-01-01")});
+ }
+ }
+
+ /** */
+ public static class CustomTypeFunctionsLibrary {
+ /** */
+ @QuerySqlTableFunction(
+ columnTypes = {
+ Object.class,
+ Object.class,
+ Object.class,
+ Object.class,
+ Object.class,
+ Object.class,
+ Object.class,
+ Object.class,
+ Object.class
+ },
+ columnNames = {
+ "UTIL_DATE",
+ "SQL_DATE",
+ "SQL_TIME",
+ "SQL_TIMESTAMP",
+ "LOCAL_DATE",
+ "LOCAL_TIME",
+ "LOCAL_TIMESTAMP",
+ "DURATION_VALUE",
+ "PERIOD_VALUE"
+ }
+ )
+ public static Iterable<Object[]> objectTableValues() {
+ return Collections.singletonList(temporalValues());
+ }
+ }
+
+ /** */
+ public static class TemporalFunctionsLibrary {
+ /** */
+ @QuerySqlFunction
+ public static java.util.Date udfDateAsUtilDate() {
+ return Date.valueOf("2020-01-01");
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static java.util.Date udfTimeAsUtilDate() {
+ return Time.valueOf("02:03:04");
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static java.util.Date udfTimestampAsUtilDate() {
+ return Timestamp.valueOf("2021-01-15 03:04:05");
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static java.util.Date udfNullUtilDate() {
+ return null;
+ }
+
+ /** */
+ @QuerySqlTableFunction(columnTypes = {java.util.Date.class},
columnNames = {"D"})
+ public static Iterable<Object[]> utilDateSubtypeTable() {
+ return Arrays.stream(temporalSubtypeValues()).map(val -> new
Object[] {val}).collect(Collectors.toList());
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static java.util.Date udfUtilDateValue() {
+ return new java.util.Date(Timestamp.valueOf("2020-01-14
01:02:03").getTime());
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static Date udfDateValue() {
+ return Date.valueOf("2021-01-15");
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static Date udfDateFromString(String val) {
+ return Date.valueOf(val);
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static Timestamp udfTimestampFromString(String val) {
+ return Timestamp.valueOf(val);
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static Time udfTimeValue() {
+ return Time.valueOf("02:03:04");
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static Timestamp udfTimestampValue() {
+ return Timestamp.valueOf("2021-01-15 02:03:04");
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static LocalDate udfLocalDateValue() {
+ return LocalDate.of(2022, 2, 16);
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static LocalTime udfLocalTimeValue() {
+ return LocalTime.of(3, 4, 5);
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static LocalDateTime udfLocalDateTimeValue() {
+ return LocalDateTime.of(2023, 3, 17, 4, 5, 6);
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static Duration udfDurationValue() {
+ return Duration.ofDays(1).plusHours(2).plusMinutes(3);
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static Period udfPeriodValue() {
+ return Period.of(1, 2, 0);
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static boolean checkTemporalTypes(
+ java.util.Date utilDate,
+ Date date,
+ Time time,
+ Timestamp timestamp,
+ LocalDate localDate,
+ LocalTime localTime,
+ LocalDateTime localDateTime,
+ Duration duration,
+ Period period
+ ) {
+ return Arrays.equals(temporalValues(), new Object[] {
+ utilDate, date, time, timestamp, localDate, localTime,
localDateTime, duration, period
+ });
+ }
+
+ /** */
+ @QuerySqlTableFunction(
+ columnTypes = {
+ java.util.Date.class,
+ Date.class,
+ Time.class,
+ Timestamp.class,
+ LocalDate.class,
+ LocalTime.class,
+ LocalDateTime.class,
+ Duration.class,
+ Period.class
+ },
+ columnNames = {
+ "UTIL_DATE",
+ "SQL_DATE",
+ "SQL_TIME",
+ "SQL_TIMESTAMP",
+ "LOCAL_DATE",
+ "LOCAL_TIME",
+ "LOCAL_TIMESTAMP",
+ "DURATION_VALUE",
+ "PERIOD_VALUE"
+ }
+ )
+ public static Iterable<Object[]> temporalTable(
+ java.util.Date utilDate,
+ Date date,
+ Time time,
+ Timestamp timestamp,
+ LocalDate localDate,
+ LocalTime localTime,
+ LocalDateTime localDateTime,
+ Duration duration,
+ Period period
+ ) {
+ return Collections.singletonList(new Object[] {
+ utilDate, date, time, timestamp, localDate, localTime,
localDateTime, duration, period
+ });
+ }
+ }
+
+ /** */
+ public static class JavaTimeParametersFunctionsLibrary {
+ /** */
+ @QuerySqlFunction
+ public static LocalDate localDateFromStr(String val) {
+ return LocalDate.parse(val);
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static LocalTime localTimeFromStr(String val) {
+ return LocalTime.parse(val);
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static LocalDateTime localDateTimeFromStr(String val) {
+ return LocalDateTime.parse(val);
+ }
+
+ /** */
+ @QuerySqlTableFunction(columnTypes = {String.class, String.class,
String.class}, columnNames = {"D", "T", "TS"})
+ public static Iterable<Object[]> javaTimeStringsTable(LocalDate date,
LocalTime time, LocalDateTime ts) {
+ return Collections.singletonList(new Object[] {date.toString(),
time.toString(), ts.toString()});
+ }
+
+ /** */
+ @QuerySqlTableFunction(
+ columnTypes = {LocalDate.class, LocalTime.class,
LocalDateTime.class},
+ columnNames = {"D", "T", "TS"}
+ )
+ public static Iterable<Object[]> javaTimeValuesTable(String date,
String time, String ts) {
+ return Collections.singletonList(new Object[] {
+ LocalDate.parse(date), LocalTime.parse(time),
LocalDateTime.parse(ts)
+ });
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static String localDateToStr(LocalDate val) {
+ return val.toString();
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static String localTimeToStr(LocalTime val) {
+ return val.toString();
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static String localDateTimeToStr(LocalDateTime val) {
+ return val.toString();
+ }
+
+ /** */
+ @QuerySqlFunction
+ public static String sqlDateToStr(Date val) {
+ return val.toString();
+ }
+ }
+
+ /** */
+ public static class DeterministicTemporalFunctionsLibrary {
+ /** */
+ @QuerySqlFunction(deterministic = true)
+ public static String detIntToStr(int val) {
+ return String.valueOf(val);
+ }
+
+ /** */
+ @QuerySqlFunction(deterministic = true)
+ public static String detDateToStr(Date val) {
+ return val.toString();
+ }
+
+ /** */
+ @QuerySqlFunction(deterministic = true)
+ public static String detTimeToStr(Time val) {
+ return val.toString();
+ }
+
+ /** */
+ @QuerySqlFunction(deterministic = true)
+ public static String detTimestampToStr(Timestamp val) {
+ return val.toString();
+ }
+ }
}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/type/OtherTypeTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/type/OtherTypeTest.java
new file mode 100644
index 00000000000..0ce072253e1
--- /dev/null
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/type/OtherTypeTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.processors.query.calcite.type;
+
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.type.BasicSqlType;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertSame;
+
+/** */
+public class OtherTypeTest {
+ /** */
+ @Test
+ public void testEquality() {
+ for (boolean nullable : new boolean[] {false, true}) {
+ RelDataType igniteType = new OtherType(nullable);
+ RelDataType calciteType = new
BasicSqlType(IgniteTypeSystem.INSTANCE, SqlTypeName.OTHER)
+ .createWithNullability(nullable);
+
+ assertNotEquals(igniteType, calciteType);
+ assertNotEquals(calciteType, igniteType);
+ assertEquals(igniteType, new OtherType(nullable));
+ assertNotEquals(igniteType, new OtherType(!nullable));
+ assertEquals("OTHER", igniteType.toString());
+ }
+ }
+
+ /** */
+ @Test
+ public void testTypeInterning() {
+ IgniteTypeFactory igniteFactory = new IgniteTypeFactory();
+ JavaTypeFactoryImpl calciteFactory = new JavaTypeFactoryImpl();
+
+ for (boolean nullable : new boolean[] {false, true}) {
+ RelDataType igniteType =
igniteFactory.createCustomType(Object.class, nullable);
+ RelDataType calciteType = calciteFactory.createTypeWithNullability(
+ calciteFactory.createSqlType(SqlTypeName.OTHER), nullable);
+
+ assertEquals(OtherType.class, igniteType.getClass());
+ assertEquals(BasicSqlType.class, calciteType.getClass());
+ assertEquals(nullable, igniteType.isNullable());
+ assertEquals(nullable, calciteType.isNullable());
+ assertSame(igniteType,
igniteFactory.createCustomType(Object.class, nullable));
+ assertSame(calciteType, calciteFactory.createTypeWithNullability(
+ calciteFactory.createSqlType(SqlTypeName.OTHER), nullable));
+ }
+ }
+}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtilsTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtilsTest.java
new file mode 100644
index 00000000000..635df8da510
--- /dev/null
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtilsTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.processors.query.calcite.util;
+
+import java.lang.reflect.Type;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+import org.apache.calcite.DataContext;
+import org.apache.calcite.DataContexts;
+import org.apache.calcite.util.DateString;
+import org.apache.calcite.util.TimeString;
+import org.apache.calcite.util.TimestampString;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+/** */
+public class TypeUtilsTest {
+ /** */
+ @Test
+ public void testLocalDateConversion() {
+ for (String date : new String[] {
+ "0001-01-01", "1500-01-02", "1582-10-10", "1969-12-31",
"1970-01-01", "2011-12-30", "9999-12-31"
+ })
+ checkConversion(LocalDate.parse(date), new
DateString(date).getDaysSinceEpoch(), Date.class);
+ }
+
+ /** */
+ @Test
+ public void testLocalTimeConversion() {
+ for (String time : new String[] {"00:00:00", "02:30:00",
"12:34:56.123", "23:59:59.999"})
+ checkConversion(LocalTime.parse(time), new
TimeString(time).getMillisOfDay(), Time.class);
+ }
+
+ /** */
+ @Test
+ public void testLocalDateTimeConversion() {
+ for (String ts : new String[] {
+ "0001-01-01 00:00:00", "1500-01-02 03:04:05", "1582-10-10
12:34:56", "1969-12-31 23:59:59.999",
+ "1970-01-01 00:00:00", "2011-12-30 12:34:56", "2021-03-14
02:30:00.123", "2021-11-07 01:30:00.123"
+ }) {
+ checkConversion(LocalDateTime.parse(ts.replace(' ', 'T')), new
TimestampString(ts).getMillisSinceEpoch(),
+ Timestamp.class);
+ }
+ }
+
+ /** */
+ @Test
+ public void testSqlDateConversion() {
+ for (String date : new String[] {
+ "0001-01-01", "1500-01-02", "1582-10-04", "1582-10-15",
"1969-12-31", "1970-01-01", "9999-12-31"
+ })
+ checkConversion(DataContexts.EMPTY, Date.valueOf(date), new
DateString(date).getDaysSinceEpoch(), Date.class);
+ }
+
+ /** */
+ @Test
+ public void testSqlTimestampConversion() {
+ for (String ts : new String[] {
+ "0001-01-01 00:00:00", "1500-01-02 03:04:05.123", "1582-10-04
23:59:59.999",
+ "1582-10-15 00:00:00", "1969-12-31 23:59:59.999", "1970-01-01
00:00:00", "2021-03-14 12:30:00.123"
+ }) {
+ Timestamp val = Timestamp.valueOf(ts);
+ long internal = new TimestampString(ts).getMillisSinceEpoch();
+
+ checkConversion(DataContexts.EMPTY, val, internal,
Timestamp.class);
+ checkConversion(DataContexts.EMPTY, new
java.util.Date(val.getTime()), internal, java.util.Date.class);
+ }
+ }
+
+ /** */
+ @Test
+ public void testHistoricalJdbcConversionWithTimeZone() {
+ for (String zone : new String[] {"UTC", "Europe/Moscow",
"America/New_York", "Pacific/Apia"}) {
+ TimeZone tz = TimeZone.getTimeZone(zone);
+ DataContext ctx =
DataContexts.of(Collections.singletonMap(DataContext.Variable.TIME_ZONE.camelName,
tz));
+ Calendar cal = new GregorianCalendar(tz, Locale.ROOT);
+
+ cal.clear();
+ cal.set(1500, Calendar.JANUARY, 2);
+
+ checkConversion(ctx, new Date(cal.getTimeInMillis()), new
DateString("1500-01-02").getDaysSinceEpoch(),
+ Date.class);
+
+ cal.set(1500, Calendar.JANUARY, 2, 3, 4, 5);
+ cal.set(Calendar.MILLISECOND, 123);
+
+ long internal = new TimestampString("1500-01-02
03:04:05.123").getMillisSinceEpoch();
+
+ checkConversion(ctx, new Timestamp(cal.getTimeInMillis()),
internal, Timestamp.class);
+ checkConversion(ctx, new java.util.Date(cal.getTimeInMillis()),
internal, java.util.Date.class);
+ }
+ }
+
+ /** */
+ private void checkConversion(Object val, Object internal, Type
sqlJavaType) {
+ // Constant reduction has no time zone in its data context.
+ checkConversion(DataContexts.EMPTY, val, internal, sqlJavaType);
+
+ for (String zone : new String[] {"UTC", "Europe/Moscow",
"America/New_York", "Pacific/Apia"}) {
+ DataContext ctx = DataContexts.of(Collections.singletonMap(
+ DataContext.Variable.TIME_ZONE.camelName,
TimeZone.getTimeZone(zone)));
+
+ checkConversion(ctx, val, internal, sqlJavaType);
+ }
+ }
+
+ /** */
+ private void checkConversion(DataContext ctx, Object val, Object internal,
Type sqlJavaType) {
+ assertEquals(internal, TypeUtils.toInternal(ctx, val));
+ // Table functions and dynamic parameters may use the corresponding
JDBC class as the storage type.
+ assertEquals(internal, TypeUtils.toInternal(ctx, val, sqlJavaType));
+ assertEquals(val, TypeUtils.fromInternal(ctx, internal,
val.getClass()));
+ assertSame(val, TypeUtils.toInternal(ctx, val, Object.class));
+ }
+}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/UtilTestSuite.java
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/UtilTestSuite.java
index 527f0240060..2a731dde3d3 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/UtilTestSuite.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/UtilTestSuite.java
@@ -24,6 +24,8 @@ import
org.apache.ignite.internal.processors.query.calcite.exec.exp.IgniteSqlFun
import
org.apache.ignite.internal.processors.query.calcite.exec.task.QueryBlockingTaskExecutorTest;
import
org.apache.ignite.internal.processors.query.calcite.exec.task.QueryTasksQueueTest;
import
org.apache.ignite.internal.processors.query.calcite.exec.tracker.MemoryTrackerTest;
+import org.apache.ignite.internal.processors.query.calcite.type.OtherTypeTest;
+import org.apache.ignite.internal.processors.query.calcite.util.TypeUtilsTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@@ -39,6 +41,8 @@ import org.junit.runners.Suite;
KeyFilteringCursorTest.class,
QueryBlockingTaskExecutorTest.class,
QueryTasksQueueTest.class,
+ OtherTypeTest.class,
+ TypeUtilsTest.class,
})
public class UtilTestSuite {
}