From 6e406e465718b11d5532a1fadffdffc239b204bd Mon Sep 17 00:00:00 2001 From: Kirill Tkalenko Date: Wed, 9 Sep 2026 19:28:40 +0300 Subject: [PATCH 1/3] IGNITE-29045 Wip --- .../calcite/exec/LogicalRelImplementor.java | 4 +- .../query/calcite/exec/TableFunctionScan.java | 41 +- .../calcite/exec/exp/ConverterUtils.java | 36 +- .../exec/exp/IgniteFunctionParameter.java | 54 ++ .../exp/IgniteReflectiveFunctionBase.java | 14 + .../exec/exp/IgniteScalarFunction.java | 5 +- .../exp/ReflectiveCallNotNullImplementor.java | 24 +- .../calcite/prepare/IgniteTypeCoercion.java | 31 ++ .../query/calcite/util/TypeUtils.java | 17 +- .../UserDefinedFunctionsIntegrationTest.java | 516 ++++++++++++++++++ 10 files changed, 724 insertions(+), 18 deletions(-) create mode 100644 modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteFunctionParameter.java 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 e2777240471c6..1d0ab5ad454eb 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 @@ else if (rel instanceof Intersect) RelDataType rowType = rel.getRowType(); - RowFactory 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 b29f91d6a7fe8..91b7df3359f31 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 implements Iterable { + /** */ + private final ExecutionContext ctx; + /** */ private final RelDataType rowType; @@ -38,13 +48,15 @@ public class TableFunctionScan implements Iterable { /** */ public TableFunctionScan( + ExecutionContext ctx, RelDataType rowType, - Supplier> dataSupplier, - RowFactory rowFactory + Supplier> dataSupplier ) { + this.ctx = ctx; this.rowType = rowType; this.dataSupplier = dataSupplier; - this.rowFactory = rowFactory; + + rowFactory = ctx.rowHandler().factory(ctx.getTypeFactory(), rowType); } /** {@inheritDoc} */ @@ -58,7 +70,7 @@ private Row convertToRow(Object rowContainer) { throw new IgniteSQLException("Unable to process table function data: row type is neither Collection or Object[]."); Object[] rowArr = rowContainer.getClass() == Object[].class - ? (Object[])rowContainer + ? ((Object[])rowContainer).clone() : ((Collection)rowContainer).toArray(); if (rowArr.length != rowType.getFieldCount()) { @@ -66,6 +78,27 @@ private Row convertToRow(Object rowContainer) { + "] doesn't match defined columns number [" + rowType.getFieldCount() + "]."); } + 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); + } } 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 3d61ad80048ac..88bc5c0ca3c24 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.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 { @@ -123,10 +125,18 @@ else if (targetType == java.sql.Timestamp.class) { /** */ static List fromInternal(Class[] targetTypes, List expressions) { - final List list = new ArrayList<>(); + return fromInternal(null, targetTypes, expressions); + } + + /** Converts user-defined function arguments using the execution context when available. */ + static List fromInternal(@Nullable Expression root, + Class[] targetTypes, + List expressions + ) { + final List 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 +149,32 @@ static List fromInternal(Class[] targetTypes, 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) { + // 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 (Types.isAssignableFrom(targetType, operand.getType())) + return operand; + + 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 0000000000000..f311b192ffbc0 --- /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 1a5dcf8b04578..6971197cb8577 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 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 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 09f377e3560bc..39cfb5afa6cde 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 @@ 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 static ScalarFunction create(Method method, boolean deterministic) { /** {@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 0f8958c4e5a8b..8f875d16c99c4 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 @@ -18,11 +18,13 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; import java.util.List; import org.apache.calcite.linq4j.tree.Expression; import org.apache.calcite.linq4j.tree.Expressions; import org.apache.calcite.rex.RexCall; +import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils; import static org.apache.ignite.internal.processors.query.calcite.util.IgniteMethod.UDF_INSTANCE; @@ -49,10 +51,10 @@ public ReflectiveCallNotNullImplementor(Method method) { @Override public Expression implement(RexToLixTranslator translator, RexCall call, List 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 +68,24 @@ public ReflectiveCallNotNullImplementor(Method method) { callExpr = Expressions.call(target, method, translatedOperands); } + + if (TypeUtils.isConvertableType(method.getReturnType())) { + Type targetType = translator.typeFactory.getJavaClass(call.getType()); + Expression converted = ConverterUtils.toInternal(callExpr, targetType); + + if (converted != callExpr) + callExpr = converted; + else { + Expression result = method.getReturnType().isPrimitive() ? Expressions.box(callExpr) : callExpr; + + callExpr = Expressions.convert_( + Expressions.call(TypeUtils.class, "toInternal", translator.getRoot(), result, + Expressions.constant(method.getReturnType())), + targetType + ); + } + } + 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 96eb4233f1b0c..6e5924f76073c 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 @@ 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.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.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 IgniteTypeCoercion(RelDataTypeFactory typeFactory, SqlValidator validator super(typeFactory, validator); } + /** {@inheritDoc} */ + @Override public boolean userDefinedFunctionCoercion(SqlValidatorScope scope, SqlCall call, SqlFunction function) { + SqlOperandMetadata metadata = (SqlOperandMetadata)function.getOperandTypeChecker(); + List 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/util/TypeUtils.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java index 6056f2bbc7c30..3274b5d13c25e 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 @@ -410,15 +410,15 @@ else if (val instanceof Number && storageType != val.getClass()) { */ private static long toLong(DataContext ctx, Object val) { if (val instanceof LocalDateTime) - return toLong(DateValueUtils.convertToTimestamp((LocalDateTime)val), DataContext.Variable.TIME_ZONE.get(ctx)); + return toLong(DateValueUtils.convertToTimestamp((LocalDateTime)val), timeZone(ctx)); if (val instanceof LocalDate) - return toLong(DateValueUtils.convertToSqlDate((LocalDate)val), DataContext.Variable.TIME_ZONE.get(ctx)); + return toLong(DateValueUtils.convertToSqlDate((LocalDate)val), timeZone(ctx)); if (val instanceof LocalTime) - return toLong(DateValueUtils.convertToSqlTime((LocalTime)val), DataContext.Variable.TIME_ZONE.get(ctx)); + return toLong(DateValueUtils.convertToSqlTime((LocalTime)val), timeZone(ctx)); - return toLong((java.util.Date)val, DataContext.Variable.TIME_ZONE.get(ctx)); + return toLong((java.util.Date)val, timeZone(ctx)); } /** */ @@ -514,7 +514,7 @@ else if (UUID.class.equals(storageType)) { /** */ private static long fromLocalTs(DataContext ctx, long ts) { - TimeZone tz = DataContext.Variable.TIME_ZONE.get(ctx); + 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 +528,11 @@ public static RexNode toRexLiteral(Object dfltVal, RelDataType type, DataContext 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/UserDefinedFunctionsIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java index e5f8e569c998e..06eb36badcd8e 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,10 +17,19 @@ 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.stream.Collectors; import org.apache.calcite.schema.SchemaPlus; @@ -37,11 +46,13 @@ 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 +503,258 @@ public void testBigDecimalFunctionArgument() { 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()); + assertEqualsArraysAware("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()", 5); + assertThrows("SELECT localDateToStr(?)", SqlValidatorException.class, + "No match found for function signature LOCALDATETOSTR()", 5); + assertThrows("SELECT localTimeToStr(?)", SqlValidatorException.class, + "No match found for function signature LOCALTIMETOSTR()", 5); + assertThrows("SELECT localDateTimeToStr(?)", SqlValidatorException.class, + "No match found for function signature LOCALDATETIMETOSTR()", 5); + } + + /** */ + @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 +1177,257 @@ private static LogListener createUnableRegisterFunctionLogListener(String fun) { 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 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 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 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 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 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(); + } + } } From c090fb5c0ec56d3b63c7291de82648ce1eef11a7 Mon Sep 17 00:00:00 2001 From: Kirill Tkalenko Date: Thu, 10 Sep 2026 10:13:25 +0300 Subject: [PATCH 2/3] IGNITE-29045 Wip --- .../query/calcite/type/OtherType.java | 14 +--- .../UserDefinedFunctionsIntegrationTest.java | 11 +++ .../query/calcite/type/OtherTypeTest.java | 68 +++++++++++++++++++ .../ignite/testsuites/UtilTestSuite.java | 2 + 4 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/type/OtherTypeTest.java 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 1a87a117ccd96..7ac9aa55053fc 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 OtherType(boolean nullable) { /** {@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/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 06eb36badcd8e..26583badef5b2 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 @@ -702,6 +702,17 @@ public void testJavaTimeFunctionParametersWithSqlTypeValues() { "No match found for function signature LOCALDATETIMETOSTR()", 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 testDeterministicTemporalFunctionReduced() { 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 0000000000000..0ce072253e197 --- /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/testsuites/UtilTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/UtilTestSuite.java index 527f0240060d6..1e30b94f4e7fa 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,7 @@ 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.junit.runner.RunWith; import org.junit.runners.Suite; @@ -39,6 +40,7 @@ KeyFilteringCursorTest.class, QueryBlockingTaskExecutorTest.class, QueryTasksQueueTest.class, + OtherTypeTest.class, }) public class UtilTestSuite { } From 1429e68f5f5c4eb7375f738190e254bb11541154 Mon Sep 17 00:00:00 2001 From: Kirill Tkalenko Date: Thu, 10 Sep 2026 11:53:42 +0300 Subject: [PATCH 3/3] IGNITE-29045 Wip --- .../query/calcite/util/TypeUtils.java | 49 +++++- .../UserDefinedFunctionsIntegrationTest.java | 150 ++++++++++++++++++ .../query/calcite/util/TypeUtilsTest.java | 143 +++++++++++++++++ .../ignite/testsuites/UtilTestSuite.java | 2 + 4 files changed, 337 insertions(+), 7 deletions(-) create mode 100644 modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtilsTest.java 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 3274b5d13c25e..453897264896c 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.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.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,10 @@ /** */ public class TypeUtils { + /** Start of the Gregorian part of the calendar used by JDBC temporal types. */ + private static final long GREGORIAN_CUTOVER = + LocalDate.of(1582, 10, 15).toEpochDay() * DateTimeUtils.MILLIS_PER_DAY; + /** */ private static final Set CONVERTABLE_TYPES = ImmutableSet.of( java.util.Date.class, @@ -409,14 +415,15 @@ else if (val instanceof Number && storageType != val.getClass()) { * @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), timeZone(ctx)); + return ((LocalDateTime)val).toInstant(ZoneOffset.UTC).toEpochMilli(); if (val instanceof LocalDate) - return toLong(DateValueUtils.convertToSqlDate((LocalDate)val), timeZone(ctx)); + return ((LocalDate)val).toEpochDay() * DateTimeUtils.MILLIS_PER_DAY; if (val instanceof LocalTime) - return toLong(DateValueUtils.convertToSqlTime((LocalTime)val), timeZone(ctx)); + return TimeUnit.NANOSECONDS.toMillis(((LocalTime)val).toNanoOfDay()); return toLong((java.util.Date)val, timeZone(ctx)); } @@ -424,8 +431,23 @@ private static long toLong(DataContext ctx, Object val) { /** */ private static long toLong(java.util.Date val, TimeZone tz) { long time = val.getTime(); + long locTs = time + tz.getOffset(time); + + if (locTs >= GREGORIAN_CUTOVER) + return locTs; + + // JDBC uses the Julian calendar before the cutover; Calcite uses the proleptic Gregorian calendar. + Calendar cal = new GregorianCalendar(DateTimeUtils.UTC_ZONE, Locale.ROOT); + + cal.setTimeInMillis(locTs); + + int year = cal.get(Calendar.YEAR); - return time + tz.getOffset(time); + 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 +457,7 @@ public static Object fromInternal(DataContext ctx, Object val, Type storageType) 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 +465,7 @@ else if (storageType == LocalTime.class && val instanceof Integer) 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,6 +536,19 @@ else if (UUID.class.equals(storageType)) { /** */ private static long fromLocalTs(DataContext ctx, long ts) { + 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 same calendar date in JDBC 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. 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 26583badef5b2..4485e6fd9f29a 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 @@ -31,6 +31,7 @@ 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; @@ -713,6 +714,120 @@ public void testJavaTimeFunctionParametersAfterTableQuery() { 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() { @@ -1390,6 +1505,41 @@ public static Iterable temporalTable( /** */ 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 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 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) { 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 0000000000000..0e6d914335e40 --- /dev/null +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtilsTest.java @@ -0,0 +1,143 @@ +/* + * 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 1e30b94f4e7fa..2a731dde3d393 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 @@ -25,6 +25,7 @@ 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; @@ -41,6 +42,7 @@ QueryBlockingTaskExecutorTest.class, QueryTasksQueueTest.class, OtherTypeTest.class, + TypeUtilsTest.class, }) public class UtilTestSuite { }