This is an automated email from the ASF dual-hosted git repository.
jooger pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new c6ebbe0c79a IGNITE-24909 Sql. Improve test coverage for date/time
templates support (#5750)
c6ebbe0c79a is described below
commit c6ebbe0c79ad5e10902069a21f0adbef392d4a5e
Author: Max Zhuravkov <[email protected]>
AuthorDate: Mon May 12 10:00:27 2025 +0300
IGNITE-24909 Sql. Improve test coverage for date/time templates support
(#5750)
Co-authored-by: Iurii Gerzhedovich <[email protected]>
---
.../engine/datatypes/ItDateTimeCastFormatTest.java | 888 +++++++++++++++++++++
.../sql/engine/exec/exp/IgniteSqlFunctions.java | 28 +
.../sql/engine/exec/exp/RexToLixTranslator.java | 27 +-
.../internal/sql/engine/util/IgniteMethod.java | 6 +
.../engine/exec/exp/IgniteSqlFunctionsTest.java | 53 ++
5 files changed, 992 insertions(+), 10 deletions(-)
diff --git
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/ItDateTimeCastFormatTest.java
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/ItDateTimeCastFormatTest.java
new file mode 100644
index 00000000000..aacf7077253
--- /dev/null
+++
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/ItDateTimeCastFormatTest.java
@@ -0,0 +1,888 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.datatypes;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.internal.sql.BaseSqlIntegrationTest;
+import org.apache.ignite.internal.sql.SqlCommon;
+import org.apache.ignite.sql.SqlException;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/** Test cases for cast to datetime type with specified format {@code (CAST x
AS DATETIME TYPE FORMAT 'FMT')}. */
+public class ItDateTimeCastFormatTest extends BaseSqlIntegrationTest {
+
+ private static final ZoneId TIME_ZONE_ID = ZoneId.of("Europe/Paris");
+
+ @BeforeAll
+ public void createDateTimeColumnsTable() {
+ sql("CREATE TABLE datetime_cols (id INT PRIMARY KEY, "
+ + "fmt_col VARCHAR,"
+ + "date0_col DATE, "
+
+ + "time0_col TIME(0), "
+ + "time1_col TIME(1), "
+ + "time2_col TIME(2), "
+ + "time3_col TIME(3), "
+
+ + "timestamp0_col TIMESTAMP(0), "
+ + "timestamp1_col TIMESTAMP(1), "
+ + "timestamp2_col TIMESTAMP(2), "
+ + "timestamp3_col TIMESTAMP(3), "
+
+ + "timestamp_with_local_time_zone0_col TIMESTAMP(0) WITH LOCAL
TIME ZONE, "
+ + "timestamp_with_local_time_zone1_col TIMESTAMP(1) WITH LOCAL
TIME ZONE, "
+ + "timestamp_with_local_time_zone2_col TIMESTAMP(2) WITH LOCAL
TIME ZONE, "
+ + "timestamp_with_local_time_zone3_col TIMESTAMP(3) WITH LOCAL
TIME ZONE "
+ + ")");
+ sql("INSERT INTO datetime_cols (id) VALUES (1)");
+ }
+
+ @ParameterizedTest
+ @MethodSource("date")
+ public void dateLiterals(DateTimeArgs<LocalTime> args) {
+ String sqlCast = format("SELECT CAST('{}' AS DATE FORMAT '{}')",
args.str, args.format);
+
+ checkQuery(sqlCast, args.value, args.error);
+ }
+
+ @ParameterizedTest
+ @MethodSource("date")
+ public void dateDynamicParams(DateTimeArgs<LocalTime> args) {
+ String sqlCast = format("SELECT CAST(? AS DATE FORMAT '{}')",
args.format);
+
+ checkQuery(sqlCast, args.value, args.error, args.str);
+ }
+
+ @ParameterizedTest
+ @MethodSource("date")
+ public void dateUpdateFromLiteral(DateTimeArgs<LocalTime> args) {
+ String sqlCast = format(
+ "UPDATE datetime_cols SET date0_col=CAST(? AS DATE FORMAT
'{}') WHERE id = 1",
+ args.format
+ );
+
+ checkDml(sqlCast, args.error, args.str);
+
+ if (args.value != null) {
+ assertQuery("SELECT date0_col FROM datetime_cols WHERE id = 1")
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("date")
+ public void dateUpdateFromDynamicParam(DateTimeArgs<LocalTime> args) {
+ String sqlCast = format(
+ "UPDATE datetime_cols SET date0_col=CAST('{}' AS DATE FORMAT
'{}') WHERE id = 1",
+ args.str, args.format
+ );
+
+ checkDml(sqlCast, args.error);
+
+ if (args.value != null) {
+ assertQuery("SELECT date0_col FROM datetime_cols WHERE id = 1")
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ private static Stream<DateTimeArgs<LocalDate>> date() {
+ return Stream.of(
+ dateTime("2000-01-01", "yyyy-MM-dd", LocalDate.of(2000, 1, 1),
null),
+ dateTime("2-01-01", "y-MM-dd", LocalDate.of(2, 1, 1), null),
+ dateTime("02-01-01", "y-MM-dd", LocalDate.of(2002, 1, 1),
null),
+ dateTime("20-01-01", "yy-MM-dd", LocalDate.of(2020, 1, 1),
null),
+ dateTime("020-01-01", "yyy-MM-dd", LocalDate.of(20, 1, 1),
null),
+ dateTime("002-01-01", "yyy-MM-dd", LocalDate.of(2, 1, 1),
null),
+ dateTime("200-01-01", "yyy-MM-dd", LocalDate.of(200, 1, 1),
null),
+ dateTime("20-01-01", "yyyy-MM-dd", LocalDate.of(20, 1, 1),
null),
+ dateTime("9999-01-01", "yyyy-MM-dd", LocalDate.of(9999, 1, 1),
null),
+
+ dateTime("2000/01-01", "yyyy/MM-dd", LocalDate.of(2000, 1, 1),
null),
+
+ dateTime("10000-01-01", "yyyy-MM-dd", null, "DATE out of
range"),
+ dateTime("10000000-01-01", "yyyy-MM-dd", null, "DATE out of
range"),
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25339
Incorrect parsing of RR/RRRR
+ /*
+ dateTime("01-01-01", "RR-MM-dd", LocalDate.of(2001, 1, 1),
null),
+ dateTime("33-01-01", "RR-MM-dd", LocalDate.of(2033, 1, 1),
null),
+ dateTime("49-01-01", "RR-MM-dd", LocalDate.of(2049, 1, 1),
null),
+ */
+ dateTime("51-01-01", "RR-MM-dd", LocalDate.of(1951, 1, 1),
null),
+ dateTime("77-01-01", "RR-MM-dd", LocalDate.of(1977, 1, 1),
null),
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25339
Incorrect parsing of RR/RRRR
+ /*
+ dateTime("01-01-01", "RRRR-MM-dd", LocalDate.of(2001, 1, 1),
null),
+ dateTime("33-01-01", "RRRR-MM-dd", LocalDate.of(2033, 1, 1),
null),
+ dateTime("49-01-01", "RRRR-MM-dd", LocalDate.of(2049, 1, 1),
null),
+ dateTime("51-01-01", "RRRR-MM-dd", LocalDate.of(1951, 1, 1),
null),
+ dateTime("77-01-01", "RRRR-MM-dd", LocalDate.of(1977, 1, 1),
null),
+ */
+
+ dateTime("2001-01-01", "RRRR-MM-dd", LocalDate.of(2001, 1, 1),
null),
+ dateTime("2033-01-01", "RRRR-MM-dd", LocalDate.of(2033, 1, 1),
null),
+ dateTime("2049-01-01", "RRRR-MM-dd", LocalDate.of(2049, 1, 1),
null),
+ dateTime("2051-01-01", "RRRR-MM-dd", LocalDate.of(2051, 1, 1),
null),
+ dateTime("1951-01-01", "RRRR-MM-dd", LocalDate.of(1951, 1, 1),
null),
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25339
Incorrect parsing of RR/RRRR
+ /*
+ dateTime("001-01-01", "RRRR-MM-dd", LocalDate.of(2001, 1, 1),
null),
+ dateTime("1-01-01", "RRRR-MM-dd", LocalDate.of(2001, 1, 1),
null),
+ dateTime("33-01-01", "RRRR-MM-dd", LocalDate.of(2033, 1, 1),
null),
+ dateTime("033-01-01", "RRRR-MM-dd", LocalDate.of(2033, 1, 1),
null),
+ dateTime("51-01-01", "RRRR-MM-dd", LocalDate.of(1951, 1, 1),
null),
+ dateTime("051-01-01", "RRRR-MM-dd", LocalDate.of(1951, 1, 1),
null),
+ dateTime("077-01-01", "RRRR-MM-dd", LocalDate.of(1973, 1, 1),
null),
+ */
+
+ dateTime("151-01-01", "RRRR-MM-dd", LocalDate.of(151, 1, 1),
null),
+ dateTime("177-01-01", "RRRR-MM-dd", LocalDate.of(177, 1, 1),
null)
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25319 parse
accepts incorrect year, month, day fields.
+ /*
+ dateTime("2000-05-07", "yyyy-MM-dddd", null, "Invalid format"),
+ dateTime("2000-05-07", "yyyy-MM-dddd", null, "Invalid format"),
+ dateTime("2000-5-07", "yyyyy-M-dd", null, "Invalid format"),
+ dateTime("2000-005-07", "yyyyy-MMM-dddd", null, "Invalid format"),
+
+ dateTime("100-05-07", "R-MM-dd", null, "Invalid format"),
+ dateTime("100-05-07", "RRR-MM-dd", null, "Invalid format"),
+ dateTime("100-05-07", "RRRRR-MM-dd", null, "Invalid format"),
+ dateTime("201-01-01", "RR-MM-dd", null, "Invalid format"),
+
+ // Different error for in combination with ff/ff0 TIMESTAMP/ TIMESTAMP
LTZ: Illegal pattern character 'f'
+ dateTime("2000-005-07", "yyyy-MMM-dd", null, "Invalid format")
+ */
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25010
+ /*
+ dateTime("0-0-0", "yyyy-MM-dd", null, "DATE out of range"),
+ dateTime("0000-01-01", "yyyy-MM-dd", null, "DATE out of range"),
+ dateTime("-01-01-01", "yyyy-MM-dd", null, "DATE out of range")
+ */
+ }
+
+ // TIME
+
+ @ParameterizedTest
+ @MethodSource("time")
+ public void timeLiterals(DateTimeArgs<LocalTime> args) {
+ String sqlCast = format("SELECT CAST('{}' AS TIME FORMAT '{}')",
args.str, args.format);
+
+ // TIME has 0 precision by default
+ boolean subMillis = args.value != null && args.value.getNano() != 0 &&
args.format.contains("ff");
+ Assumptions.assumeFalse(subMillis);
+
+ checkQuery(sqlCast, args.value, args.error);
+ }
+
+ @ParameterizedTest
+ @MethodSource("time")
+ public void timeDynamicParams(DateTimeArgs<LocalTime> args) {
+ String sqlCast = format("SELECT CAST(? AS TIME FORMAT '{}')",
args.format);
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313
+ // Nothing but ff3 works due to SimpleDateFormat
+ Assumptions.assumeTrue(!args.format.contains("ff") ||
args.format.contains("ff3"));
+
+ checkQuery(sqlCast, args.value, args.error, args.str);
+ }
+
+ @ParameterizedTest
+ @MethodSource("time")
+ public void timeUpdateFromLiteral(DateTimeArgs<LocalTime> args) {
+ String sqlCast = format(
+ "UPDATE datetime_cols SET time0_col=CAST(? AS TIME FORMAT
'{}') WHERE id = 1",
+ args.format
+ );
+
+ checkDml(sqlCast, args.error, args.str);
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313
+ // Nothing but ff3 works due to SimpleDateFormat
+ Assumptions.assumeTrue(!args.format.contains("ff") ||
args.format.contains("ff3"));
+
+ if (args.value != null) {
+ assertQuery("SELECT time0_col FROM datetime_cols WHERE id = 1")
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("time")
+ public void timeUpdateFromDynamicParam(DateTimeArgs<LocalTime> args) {
+ String sqlCast = format(
+ "UPDATE datetime_cols SET time0_col=CAST('{}' AS TIME FORMAT
'{}') WHERE id = 1",
+ args.str,
+ args.format
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25045
Insert/Update RexToLix cast translation bug
+ Assumptions.assumeTrue(args.value == null || args.value.getNano() ==
0);
+
+ checkDml(sqlCast, args.error);
+
+ if (args.value != null) {
+ assertQuery("SELECT time0_col FROM datetime_cols WHERE id = 1")
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ private static Stream<DateTimeArgs<LocalTime>> time() {
+ return Stream.of(
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25314
+ // Sql. Cast FORMAT. Parsing of hh/h12 format is incorrect
+ /*
+ dateTime("05:02 a.m.", "hh12:mi a.m.", LocalTime.of(5, 2),
null),
+ dateTime("11:02 a.m.", "hh12:mi a.m.", LocalTime.of(11, 2),
null),
+ dateTime("12:02 a.m.", "hh12:mi a.m.", LocalTime.of(0, 2),
null),
+ dateTime("13:02 a.m.", "hh12:mi a.m.", null, "TIME out of
range"),
+
+ dateTime("05:02 p.m.", "hh12:mi p.m.", LocalTime.of(17, 2),
null),
+ dateTime("11:02 p.m.", "hh12:mi p.m.", LocalTime.of(23, 2),
null),
+ dateTime("12:02 p.m.", "hh12:mi p.m.", LocalTime.of(12, 2),
null),
+ dateTime("13:02 p.m.", "hh12:mi p.m.", null, "TIME out of
range"),
+ */
+
+ // hh24
+ dateTime("12:02:03", "hh24:mi:ss", LocalTime.of(12, 2, 3),
null),
+ dateTime("23:02:03", "hh24:mi:ss", LocalTime.of(23, 2, 3),
null),
+ dateTime("23/02:03", "hh24/mi:ss", LocalTime.of(23, 2, 3),
null),
+
+ // fractional
+ dateTime("23:02:03.99", "hh24:mi:ss.ff2", LocalTime.of(23, 2,
3, 990_000_000), null),
+ dateTime("23:02:03.999", "hh24:mi:ss.ff3", LocalTime.of(23, 2,
3, 999_000_000), null),
+ dateTime("23:02:03.123", "hh24:mi:ss.ff3", LocalTime.of(23, 2,
3, 123_000_000), null),
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25317
Sub-millsecond fractional format is broken (SimpleDateFormat)
+ /*
+ dateTime("23:02:03.1234", "hh24:mi:ss.ff3", LocalTime.of(23,
2, 3, 123_400_000), null),
+ dateTime("23:02:03.1234", "hh24:mi:ss.ff4", LocalTime.of(23,
2, 3, 123_400_000), null),
+ */
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313
(SimpleDateFormat)
+ /*
+ dateTime("24:02:03", "hh24:mi:ss", null, "TIME out of range"),
+ dateTime("123:02:03", "hh24:mi:ss", null, "TIME out of range"),
+ dateTime("23:60:03", "hh24:mi:ss", null, "TIME out of range"),
+ dateTime("23:123:03", "hh24:mi:ss", null, "TIME out of range"),
+ dateTime("23:02:60", "hh24:mi:ss", null, "TIME out of range"),
+ dateTime("23:02:123", "hh24:mi:ss", null, "TIME out of range"),
+ */
+
+ dateTime("22:02:03", "hX:mi:ss", null, "Invalid format"),
+ dateTime("22:02:03", "hh:mX:ss", null, "Invalid format"),
+ dateTime("22:02:03", "hh:mm:sX", null, "Invalid format"),
+
+ dateTime("22:02:03", "hh:mm:ss.ff", null, "Illegal pattern
character 'f'"),
+ dateTime("22:02:03", "hh:mm:ss.ff0", null, "Illegal pattern
character 'f'"),
+ dateTime("22:02:03", "hh:mm:ss.ff10", null, "Invalid format"),
+
+ dateTime("23:02:03.123", "hh24:mi:ss", null, "Invalid format")
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25315 Max length
of a fractional part is ignored
+ /*
+ dateTime("23:02:03.12", "hh24:mi:ss.ff3", null, "Invalid format"),
+ dateTime("23:02:03.1234", "hh24:mi:ss.ff3", null, "Invalid format")
+ */
+ }
+
+ @ParameterizedTest
+ @MethodSource("timeWithPrecision")
+ public void timeWithPrecisionLiterals(int precision,
DateTimeArgs<LocalTime> args) {
+ String sqlCast = format("SELECT CAST('{}' AS TIME({}) FORMAT '{}')",
args.str, precision, args.format);
+
+ checkQuery(sqlCast, args.value, args.error);
+ }
+
+ // Cast to TIME/TIMESTAMP/TIMESTAMP_LTZ ignores target type's precision
+ @Disabled("https://issues.apache.org/jira/browse/IGNITE-25045")
+ @ParameterizedTest
+ @MethodSource("timeWithPrecision")
+ public void timeWithPrecisionDynamicParams(int precision,
DateTimeArgs<LocalTime> args) {
+ String sqlCast = format("SELECT CAST(? AS TIME({}) FORMAT '{}')",
precision, args.format);
+
+ checkQuery(sqlCast, args.value, args.error, args.str);
+ }
+
+ @ParameterizedTest
+ @MethodSource("timeWithPrecision")
+ public void timeWithPrecisionUpdateFromLiteral(int precision,
DateTimeArgs<LocalTime> args) {
+ String col = format("time{}_col", precision);
+
+ String sqlCast = format(
+ "UPDATE datetime_cols SET {}=CAST(? AS TIME FORMAT '{}') WHERE
id = 1",
+ col, args.format
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25045
+ // Cast to TIME/TIMESTAMP/TIMESTAMP_LTZ ignores target type's precision
+ Assumptions.assumeTrue(precision == 3);
+
+ checkDml(sqlCast, args.error, args.str);
+
+ if (args.value != null) {
+ assertQuery(format("SELECT {} FROM datetime_cols WHERE id = 1",
col))
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("timeWithPrecision")
+ public void timeWithPrecisionUpdateFromDynamicParam(int precision,
DateTimeArgs<LocalTime> args) {
+ String col = format("time{}_col", precision);
+
+ String sqlCast = format(
+ "UPDATE datetime_cols SET {}=CAST('{}' AS TIME FORMAT '{}')
WHERE id = 1",
+ col,
+ args.str,
+ args.format
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313 confusing
submillis parsing
+ boolean subMillis = args.value != null && args.value.getNano() != 0 &&
args.format.contains("ff");
+ Assumptions.assumeFalse(subMillis);
+
+ checkDml(sqlCast, args.error);
+
+ if (args.value != null) {
+ assertQuery(format("SELECT {} FROM datetime_cols WHERE id = 1",
col))
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ private static Stream<Arguments> timeWithPrecision() {
+ return Stream.of(
+ /* TODO https://issues.apache.org/jira/browse/IGNITE-25313
CAST FORMAT confusing behaviour when translating milliseconds
+ // FF1
+
+ Arguments.of(0,
+ dateTime("15:32:17.1", "hh24:mi:ss.ff1",
LocalTime.of(15, 32, 17).withNano(0), null)),
+ Arguments.of(1,
+ dateTime("15:32:17.1", "hh24:mi:ss.ff1",
LocalTime.of(15, 32, 17).withNano(100_000_000), null)),
+ Arguments.of(2,
+ dateTime("15:32:17.1", "hh24:mi:ss.ff1",
LocalTime.of(15, 32, 17).withNano(100_000_000), null)),
+ Arguments.of(3,
+ dateTime("15:32:17.1", "hh24:mi:ss.ff1",
LocalTime.of(15, 32, 17).withNano(100_000_000), null)),
+
+ // FF2
+
+ Arguments.of(0,
+ dateTime("15:32:17.12", "hh24:mi:ss.ff2",
LocalTime.of(15, 32, 17).withNano(0), null)),
+ Arguments.of(1,
+ dateTime("15:32:17.12", "hh24:mi:ss.ff2",
LocalTime.of(15, 32, 17).withNano(100_000_000), null)),
+ Arguments.of(2,
+ dateTime("15:32:17.12", "hh24:mi:ss.ff2",
LocalTime.of(15, 32, 17).withNano(120_000_000), null)),
+ Arguments.of(3,
+ dateTime("15:32:17.12", "hh24:mi:ss.ff2",
LocalTime.of(15, 32, 17).withNano(120_000_000), null)),
+ */
+
+ // FF3
+ Arguments.of(0,
+ dateTime("15:32:17.123", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(0), null)),
+ Arguments.of(0,
+ dateTime("15:32:17.500", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(0), null)),
+ Arguments.of(0,
+ dateTime("15:32:17.999", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(0), null)),
+
+ Arguments.of(1,
+ dateTime("15:32:17.123", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(100_000_000), null)),
+ Arguments.of(1,
+ dateTime("15:32:17.500", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(500_000_000), null)),
+ Arguments.of(1,
+ dateTime("15:32:17.999", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(900_000_000), null)),
+
+ Arguments.of(2,
+ dateTime("15:32:17.123", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(120_000_000), null)),
+ Arguments.of(2,
+ dateTime("15:32:17.500", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(500_000_000), null)),
+ Arguments.of(2,
+ dateTime("15:32:17.999", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(990_000_000), null)),
+
+ Arguments.of(3,
+ dateTime("15:32:17.123", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(123_000_000), null)),
+ Arguments.of(3,
+ dateTime("15:32:17.500", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(500_000_000), null)),
+ Arguments.of(3,
+ dateTime("15:32:17.999", "hh24:mi:ss.ff3",
LocalTime.of(15, 32, 17).withNano(999_000_000), null))
+ );
+ }
+
+ // TIMESTAMP
+
+ @ParameterizedTest
+ @MethodSource("timestamp")
+ public void timestampLiterals(DateTimeArgs<LocalDateTime> args) {
+ String sqlCast = format("SELECT CAST('{}' AS TIMESTAMP FORMAT '{}')",
args.str, args.format);
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313 confusing
submills parsing
+ boolean subMillis = args.value != null && args.value.getNano() != 0 &&
args.format.contains("ff");
+ Assumptions.assumeFalse(subMillis);
+
+ checkQuery(sqlCast, args.value, args.error);
+ }
+
+ private static Stream<DateTimeArgs<LocalDateTime>> timestamp() {
+ List<DateTimeArgs<LocalDate>> date =
date().collect(Collectors.toList());
+ List<DateTimeArgs<LocalTime>> time =
time().collect(Collectors.toList());
+
+ List<DateTimeArgs<LocalDateTime>> result = new ArrayList<>();
+
+ for (DateTimeArgs<LocalDate> d : date) {
+ for (DateTimeArgs<LocalTime> t : time) {
+ String tsStr = d.str + " " + t.str;
+ String tsFmt = d.format + " " + t.format;
+
+ if (d.value != null && t.value != null) {
+ LocalDateTime tsExpected = LocalDateTime.of(d.value,
t.value);
+ result.add(dateTime(tsStr, tsFmt, tsExpected, null));
+ } else if (isFormatError(d.error)) {
+ result.add(dateTime(tsStr, tsFmt, null, d.error));
+ } else if (isFormatError(t.error)) {
+ result.add(dateTime(tsStr, tsFmt, null, t.error));
+ } else {
+ // TIMESTAMP out of range
+ result.add(dateTime(tsStr, tsFmt, null, "TIMESTAMP out of
range"));
+ }
+ }
+ }
+
+ return result.stream();
+ }
+
+ private static boolean isFormatError(@Nullable String message) {
+ return message != null && (message.contains("Invalid format") ||
message.contains("Illegal pattern character"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("timestamp")
+ public void timestampUpdateFromLiteral(DateTimeArgs<LocalDateTime> args) {
+ String sqlCast = format(
+ "UPDATE datetime_cols SET timestamp0_col=CAST(? AS TIMESTAMP
FORMAT '{}') WHERE id = 1",
+ args.format
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313 confusing
submills parsing
+ Assumptions.assumeTrue(args.value == null ||
!args.format.contains("ff2"));
+
+ checkDml(sqlCast, args.error, args.str);
+
+ if (args.value != null) {
+ assertQuery("SELECT timestamp0_col FROM datetime_cols WHERE id =
1")
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("timestamp")
+ public void timestampUpdateFromDynamicParam(DateTimeArgs<LocalDateTime>
args) {
+ String sqlCast = format(
+ "UPDATE datetime_cols SET timestamp0_col=CAST('{}' AS
TIMESTAMP FORMAT '{}') WHERE id = 1",
+ args.str, args.format
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313
+ // Nothing but ff3 works due to SimpleDateFormat
+ Assumptions.assumeTrue(!args.format.contains("ff"));
+ // TODO https://issues.apache.org/jira/browse/IGNITE-24889
+ // FF3 does not work due to cast transaction error
+
+ checkDml(sqlCast, args.error);
+
+ if (args.value != null) {
+ assertQuery("SELECT timestamp0_col FROM datetime_cols WHERE id =
1")
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("timestampWithPrecision")
+ public void timestampWithPrecisionLiterals(int precision,
DateTimeArgs<LocalDateTime> args) {
+ String sqlCast = format("SELECT CAST('{}' AS TIMESTAMP({}) FORMAT
'{}')", args.str, precision, args.format);
+
+ checkQuery(sqlCast, args.value, args.error);
+ }
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313
SimpleDateFormat problems
+ // Cast translation errors
+ @Disabled("https://issues.apache.org/jira/browse/IGNITE-24889")
+ @ParameterizedTest
+ @MethodSource("timestampWithPrecision")
+ public void timestampWithPrecisionUpdateFromLiteral(int precision,
DateTimeArgs<LocalDateTime> args) {
+ String col = format("timestamp{}_col", precision);
+
+ String sqlCast = format(
+ "UPDATE datetime_cols SET {}=CAST(? AS TIMESTAMP FORMAT '{}')
WHERE id = 1",
+ col, args.format
+ );
+
+ checkDml(sqlCast, args.error, args.str);
+
+ if (args.value != null) {
+ assertQuery(format("SELECT {} FROM datetime_cols WHERE id = 1",
col))
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("timestampWithPrecision")
+ public void timestampWithPrecisionUpdateFromDynamicParam(int precision,
DateTimeArgs<LocalDateTime> args) {
+ String col = format("timestamp{}_col", precision);
+
+ String sqlCast = format(
+ "UPDATE datetime_cols SET {}=CAST('{}' AS TIMESTAMP FORMAT
'{}') WHERE id = 1",
+ col, args.str, args.format
+ );
+
+ checkDml(sqlCast, args.error);
+
+ if (args.value != null) {
+ assertQuery(format("SELECT {} FROM datetime_cols WHERE id = 1",
col))
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ private static Stream<Arguments> timestampWithPrecision() {
+ LocalDate date = LocalDate.of(2020, 2, 5);
+ return Stream.of(
+ /* TODO https://issues.apache.org/jira/browse/IGNITE-25313
CAST FORMAT confusing behaviour when translating milliseconds
+ // FF1
+ Arguments.of(0,
+ dateTime("2020-02-05 15:32:17.1", "yyyy-MM-dd
hh24:mi:ss.ff1",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(0)), null)),
+ Arguments.of(1,
+ dateTime("2020-02-05 15:32:17.1", "yyyy-MM-dd
hh24:mi:ss.ff1",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(100_000_000)), null)),
+ Arguments.of(2,
+ dateTime("2020-02-05 15:32:17.1", "yyyy-MM-dd
hh24:mi:ss.ff1",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(100_000_000)), null)),
+ Arguments.of(3,
+ dateTime("2020-02-05 15:32:17.1", "yyyy-MM-dd
hh24:mi:ss.ff1",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(100_000_000)), null)),
+
+ // FF2
+
+ Arguments.of(0,
+ dateTime("2020-02-05 15:32:17.12", "yyyy-MM-dd
hh24:mi:ss.ff2",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(0)), null)),
+ Arguments.of(1,
+ dateTime("2020-02-05 15:32:17.12", "yyyy-MM-dd
hh24:mi:ss.ff2",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(100_000_000)), null)),
+ Arguments.of(2,
+ dateTime("2020-02-05 15:32:17.12", "yyyy-MM-dd
hh24:mi:ss.ff2",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(120_000_000)), null)),
+ Arguments.of(3,
+ dateTime("2020-02-05 15:32:17.12", "yyyy-MM-dd
hh24:mi:ss.ff2",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(120_000_000)), null)),
+
+ */
+
+ // FF3
+
+ Arguments.of(0,
+ dateTime("2020-02-05 15:32:17.123", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(0)), null)),
+ Arguments.of(0,
+ dateTime("2020-02-05 15:32:17.500", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(0)), null)),
+ Arguments.of(0,
+ dateTime("2020-02-05 15:32:17.999", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(0)), null)),
+
+ Arguments.of(1,
+ dateTime("2020-02-05 15:32:17.123", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(100_000_000)), null)),
+ Arguments.of(1,
+ dateTime("2020-02-05 15:32:17.500", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(500_000_000)), null)),
+ Arguments.of(1,
+ dateTime("2020-02-05 15:32:17.999", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(900_000_000)), null)),
+
+ Arguments.of(2,
+ dateTime("2020-02-05 15:32:17.123", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(120_000_000)), null)),
+ Arguments.of(2,
+ dateTime("2020-02-05 15:32:17.500", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(500_000_000)), null)),
+ Arguments.of(2,
+ dateTime("2020-02-05 15:32:17.999", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(990_000_000)), null)),
+
+ Arguments.of(3,
+ dateTime("2020-02-05 15:32:17.123", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(123_000_000)), null)),
+ Arguments.of(3,
+ dateTime("2020-02-05 15:32:17.500", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(500_000_000)), null)),
+ Arguments.of(3,
+ dateTime("2020-02-05 15:32:17.999", "yyyy-MM-dd
hh24:mi:ss.ff3",
+ LocalDateTime.of(date, LocalTime.of(15, 32,
17).withNano(999_000_000)), null))
+ );
+ }
+
+ // TIMESTAMP LTZ
+
+ @ParameterizedTest
+ @MethodSource("timestampLtz")
+ public void timestampLtzLiterals(DateTimeArgs<Instant> args) {
+ String sqlCast = format(
+ "SELECT CAST('{}' AS TIMESTAMP WITH LOCAL TIME ZONE FORMAT
'{}')",
+ args.str, args.format
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313 confusing
submillis parsing
+ boolean subMillis = args.value != null && args.value.getNano() != 0 &&
args.format.contains("ff");
+ Assumptions.assumeFalse(subMillis);
+
+ checkQuery(sqlCast, args.value, args.error);
+ }
+
+ @ParameterizedTest
+ @MethodSource("timestampLtz")
+ public void timestampLtzUpdateFromLiteral(DateTimeArgs<Instant> args) {
+ String sqlCast = format(""
+ + "UPDATE datetime_cols "
+ + "SET timestamp_with_local_time_zone0_col=CAST(? AS
TIMESTAMP WITH LOCAL TIME ZONE FORMAT '{}') WHERE id = 1",
+ args.format
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313 confusing
submillis parsing
+ boolean subMillis = args.value != null && args.value.getNano() != 0 &&
args.format.contains("ff");
+ Assumptions.assumeFalse(subMillis);
+
+ checkDml(sqlCast, args.error, args.str);
+
+ if (args.value != null) {
+ assertQuery("SELECT timestamp_with_local_time_zone0_col FROM
datetime_cols WHERE id = 1")
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("timestampLtz")
+ public void timestampLtzUpdateFromDynamicParam(DateTimeArgs<Instant> args)
{
+ String sqlCast = format(""
+ + "UPDATE datetime_cols "
+ + "SET timestamp_with_local_time_zone0_col=CAST('{}'
AS TIMESTAMP WITH LOCAL TIME ZONE FORMAT '{}') WHERE id = 1",
+ args.str, args.format
+ );
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25313 confusing
submillis parsing
+ boolean subMillis = args.value != null && args.value.getNano() != 0 &&
args.format.contains("ff");
+ Assumptions.assumeFalse(subMillis);
+
+ checkDml(sqlCast, args.error);
+
+ if (args.value != null) {
+ assertQuery("SELECT timestamp_with_local_time_zone0_col FROM
datetime_cols WHERE id = 1")
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ private static Stream<DateTimeArgs<Instant>> timestampLtz() {
+ return timestamp().map(dt -> {
+ if (dt.value != null) {
+ Instant expectedInstant = ZonedDateTime.of(dt.value,
TIME_ZONE_ID).toInstant();
+ return dateTime(dt.str, dt.format, expectedInstant, null);
+ } else if (dt.error != null && dt.error.contains("out of range")) {
+ // TIMESTAMP_WITH_LOCAL_TIME_ZONE out of range
+ return dateTime(dt.str, dt.format, null,
"TIMESTAMP_WITH_LOCAL_TIME_ZONE out of range");
+ } else {
+ return dateTime(dt.str, dt.format, null, dt.error);
+ }
+ });
+ }
+
+ // CAST FORMAT confusing behaviour when translating milliseconds
+ @Disabled("https://issues.apache.org/jira/browse/IGNITE-25313")
+ @ParameterizedTest
+ @MethodSource("timestampLtzWithPrecision")
+ public void timestampLtzWithPrecisionLiterals(int precision,
DateTimeArgs<Instant> args) {
+ String sqlCast = format(
+ "SELECT CAST('{}' AS TIMESTAMP({}) WITH LOCAL TIME ZONE FORMAT
'{}')",
+ args.str, precision, args.format
+ );
+
+ checkQuery(sqlCast, args.value, args.error);
+ }
+
+ // CAST FORMAT confusing behaviour when translating milliseconds
+ @Disabled("https://issues.apache.org/jira/browse/IGNITE-25313")
+ @ParameterizedTest
+ @MethodSource("timestampLtzWithPrecision")
+ public void timestampLtzWithPrecisionUpdateFromLiteral(int precision,
DateTimeArgs<Instant> args) {
+ String col = format("timestamp_with_local_time_zone{}_col", precision);
+
+ String sqlCast = format(""
+ + "UPDATE datetime_cols "
+ + "SET {}=CAST(? AS TIMESTAMP WITH LOCAL TIME ZONE
FORMAT '{}') WHERE id = 1",
+ col, args.format
+ );
+
+ checkDml(sqlCast, args.error, args.str);
+
+ if (args.value != null) {
+ assertQuery(format("SELECT {} FROM datetime_cols WHERE id = 1",
col))
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ // CAST FORMAT confusing behaviour when translating milliseconds
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25045
+ @Disabled("https://issues.apache.org/jira/browse/IGNITE-25313")
+ @ParameterizedTest
+ @MethodSource("timestampLtzWithPrecision")
+ public void timestampLtzWithPrecisionUpdateFromDynamicParam(int precision,
DateTimeArgs<Instant> args) {
+ String col = format("timestamp_with_local_time_zone{}_col", precision);
+
+ String sqlCast = format(""
+ + "UPDATE datetime_cols "
+ + "SET {}=CAST('{}' AS TIMESTAMP WITH LOCAL TIME ZONE
FORMAT '{}') WHERE id = 1",
+ col, args.str, args.format
+ );
+
+ checkDml(sqlCast, args.error);
+
+ if (args.value != null) {
+ assertQuery(format("SELECT {} FROM datetime_cols WHERE id = 1",
col))
+ .returns(args.value)
+ .check();
+ }
+ }
+
+ private static Stream<Arguments> timestampLtzWithPrecision() {
+ return timestampWithPrecision().map(a -> {
+ Object[] array = a.get();
+ int precision = (int) array[0];
+ DateTimeArgs<LocalDateTime> args = (DateTimeArgs<LocalDateTime>)
array[1];
+ assert args.value != null;
+
+ Instant expectedInstant = ZonedDateTime.of(args.value,
TIME_ZONE_ID).toInstant();
+
+ return Arguments.of(precision, dateTime(args.str, args.format,
expectedInstant, null));
+ });
+ }
+
+ private static <T> DateTimeArgs<T> dateTime(String str, String format,
@Nullable T value, @Nullable String error) {
+ return new DateTimeArgs<>(str, format, value, error);
+ }
+
+ static class DateTimeArgs<T> {
+
+ final String str;
+
+ final String format;
+
+ @Nullable
+ private final String error;
+
+ @Nullable
+ private final T value;
+
+ DateTimeArgs(String str, String format, @Nullable T value, @Nullable
String error) {
+ this.str = str;
+ this.format = format;
+ this.error = error;
+ this.value = value;
+ }
+
+ public String toString() {
+ return str + " " + format + " " + (error != null ? "ERROR: " +
error : "") + (value != null ? "= " + value : "");
+ }
+ }
+
+ private static void checkQuery(String query, @Nullable Object expected,
@Nullable String error, Object... params) {
+ if (error != null) {
+ try {
+ Ignite node = CLUSTER.node(0);
+ List<List<Object>> rows = sql(node, null,
SqlCommon.DEFAULT_SCHEMA_NAME, TIME_ZONE_ID, query, params);
+ fail("Expected error: " + error + ". But got rows: " + rows);
+ } catch (SqlException e) {
+ e.printStackTrace(System.err);
+ assertThat("error message", e.getMessage(),
containsString(error));
+ }
+ } else {
+ assertQuery(query)
+ .withTimeZoneId(TIME_ZONE_ID)
+ .withParams(params)
+ .returns(expected)
+ .check();
+ }
+ }
+
+ private static void checkDml(String query, @Nullable String error,
Object... params) {
+ if (error != null) {
+ try {
+ Ignite node = CLUSTER.node(0);
+ sql(node, null, SqlCommon.DEFAULT_SCHEMA_NAME, TIME_ZONE_ID,
query, params);
+
+ List<?> rows = sql("SELECT * FROM datetime_cols");
+
+ fail("Expected error: " + error + ". But got rows: " + rows);
+ } catch (SqlException e) {
+ e.printStackTrace(System.err);
+ assertThat("error message", e.getMessage(),
containsString(error));
+ }
+ } else {
+ assertQuery(query)
+ .withTimeZoneId(TIME_ZONE_ID)
+ .withParams(params)
+ .returns(1L)
+ .check();
+ }
+ }
+}
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctions.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctions.java
index 6187b43388d..45eb98c2f0f 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctions.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctions.java
@@ -25,10 +25,17 @@ import static
org.apache.ignite.lang.ErrorGroups.Sql.RUNTIME_ERR;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+import java.time.zone.ZoneRules;
+import java.util.Objects;
+import java.util.TimeZone;
import java.util.UUID;
import org.apache.calcite.avatica.util.ByteString;
import org.apache.calcite.linq4j.function.NonDeterministic;
import org.apache.calcite.runtime.SqlFunctions;
+import org.apache.calcite.runtime.SqlFunctions.DateParseFunction;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.ignite.internal.schema.SchemaUtils;
import org.apache.ignite.internal.sql.engine.util.Commons;
@@ -543,6 +550,27 @@ public class IgniteSqlFunctions {
return args1;
}
+ /** Converts timestamp string into a timestamp with local time zone. */
+ public static @Nullable Long toTimestampWithLocalTimeZone(@Nullable String
v, String format, TimeZone timeZone) {
+ if (v == null) {
+ return null;
+ }
+
+ Objects.requireNonNull(format, "format");
+ Objects.requireNonNull(timeZone, "timeZone");
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25320 reuse to
improve performance.
+ DateParseFunction function = new DateParseFunction();
+ long ts = function.parseTimestamp(format, v);
+ Instant instant = Instant.ofEpochMilli(ts);
+
+ // Adjust instant millis
+ ZoneRules rules = timeZone.toZoneId().getRules();
+ ZoneOffset offset = rules.getOffset(instant);
+ Instant adjusted = instant.minus(offset.getTotalSeconds(),
ChronoUnit.SECONDS);
+ return adjusted.toEpochMilli();
+ }
+
private static @Nullable Object leastOrGreatest(boolean least, Object
arg0, Object arg1) {
if (arg0 == null || arg1 == null) {
return null;
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
index 5cf6cac04d3..56690cb0e3b 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
@@ -118,6 +118,7 @@ import org.locationtech.jts.geom.Geometry;
* Added pad-truncate from CHARACTER to INTERVAL types
* Added time-zone dependency for cast from CHARACTER types to TIMESTAMP
WITH LOCAL TIMEZONE (see point 3)
* Cast TIMESTAMP to TIMESTAMP WITH LOCAL TIMEZONE use our
implementation, see IgniteMethod.UNIX_TIMESTAMP_TO_STRING_PRECISION_AWARE
+ * Cast TIMESTAMP LTZ accepts FORMAT. (See
IgniteMethod.TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE).
* 6. Translate literals changes:
* DECIMAL use own implementation see IgniteSqlFunctions.class,
“toBigDecimal"
* TIMESTAMP_WITH_LOCAL_TIME_ZONE use own implementation
@@ -126,6 +127,7 @@ import org.locationtech.jts.geom.Geometry;
* IgniteMethod.CONTEXT_GET_PARAMETER_VALUE instead of
BuiltInMethod.DATA_CONTEXT_GET
* added conversation for Decimals
* 8. Added parameter `RelDataType valueType` for implementRecursively method
to do right datatype conversion
+ * 9. Added parameter 'Format' to translateCastToTimestampWithLocalTimeZone.
*/
public class RexToLixTranslator implements
RexVisitor<RexToLixTranslator.Result> {
public static final Map<Method, SqlOperator> JAVA_TO_SQL_METHOD_MAP =
@@ -377,7 +379,7 @@ public class RexToLixTranslator implements
RexVisitor<RexToLixTranslator.Result>
return translateCastToTimestamp(sourceType, operand, format,
defaultExpression);
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
- return translateCastToTimestampWithLocalTimeZone(sourceType, targetType,
operand, defaultExpression);
+ return translateCastToTimestampWithLocalTimeZone(sourceType, targetType,
operand, format, defaultExpression);
case BOOLEAN:
switch (sourceType.getSqlTypeName()) {
@@ -778,7 +780,7 @@ public class RexToLixTranslator implements
RexVisitor<RexToLixTranslator.Result>
}
private Expression translateCastToTimestampWithLocalTimeZone(RelDataType
sourceType, RelDataType targetType,
- Expression operand, Supplier<Expression> defaultExpression) {
+ Expression operand, ConstantExpression format, Supplier<Expression>
defaultExpression) {
switch (sourceType.getSqlTypeName()) {
case CHAR:
@@ -786,14 +788,19 @@ public class RexToLixTranslator implements
RexVisitor<RexToLixTranslator.Result>
// By default Calcite for this type requires that the time zone be
explicitly specified.
// Since this type implies a local timezone, its explicit indication
seems redundant,
// so we prohibit the user from explicitly setting a timezone.
- return
- Expressions.call(
- IgniteMethod.TO_TIMESTAMP_LTZ_EXACT.method(),
- Expressions.call(
-
BuiltInMethod.TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method,
- operand,
- Expressions.call(BuiltInMethod.TIME_ZONE.method,
root))
- );
+ Expression getTimeZone =
Expressions.call(BuiltInMethod.TIME_ZONE.method, root);
+
+ if (Expressions.isConstantNull(format)) {
+ return Expressions.call(
+ IgniteMethod.TO_TIMESTAMP_LTZ_EXACT.method(),
+
Expressions.call(BuiltInMethod.TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method,
operand, getTimeZone)
+ );
+ } else {
+ return Expressions.call(
+ IgniteMethod.TO_TIMESTAMP_LTZ_EXACT.method(),
+
Expressions.call(IgniteMethod.TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method(),
operand, format, getTimeZone)
+ );
+ }
case DATE:
return
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteMethod.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteMethod.java
index f9eddc16aac..831882cd3b3 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteMethod.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteMethod.java
@@ -150,6 +150,12 @@ public enum IgniteMethod {
*/
CURRENT_DATE(IgniteSqlDateTimeUtils.class, "currentDate",
DataContext.class),
+ /**
+ * SQL CAST(? AS TIMESTAMP WITH LOCAL TIME ZONE FORMAT {@code format}).
The same as
+ * {@link SqlFunctions#timeWithLocalTimeZoneToTimestampWithLocalTimeZone}
but accepts date format literal.
+ */
+
TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE(IgniteSqlFunctions.class,
+ "toTimestampWithLocalTimeZone", String.class, String.class,
TimeZone.class),
;
private final Method method;
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
index c410a45985b..c74f8facbfc 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
@@ -24,13 +24,23 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.TimeZone;
import java.util.function.Supplier;
+import java.util.stream.Stream;
+import org.apache.calcite.runtime.SqlFunctions;
import org.apache.ignite.internal.sql.engine.util.IgniteMath;
import org.apache.ignite.lang.ErrorGroups.Sql;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
/**
* Sql functions test.
@@ -495,4 +505,47 @@ public class IgniteSqlFunctionsTest {
assertThrows(ArithmeticException.class, () ->
IgniteMath.decimalDivide(num, denum, 4, 2));
}
}
+
+ @ParameterizedTest
+ @MethodSource("timeZoneTime")
+ public void toTimestampWithLocalTimeZoneFormat(String zoneIdstr,
LocalDateTime time) {
+ ZoneId zoneId = ZoneId.of(zoneIdstr);
+
+ String v = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd
HH:mm:ss.SSS"));
+
+ TimeZone timeZone = TimeZone.getTimeZone(zoneId);
+ long calciteTsLtz = SqlFunctions.toTimestampWithLocalTimeZone(v,
timeZone);
+ long tsLtz = IgniteSqlFunctions.toTimestampWithLocalTimeZone(v,
"yyyy-MM-dd hh:mi:ss.ff3", timeZone);
+
+ assertEquals(Instant.ofEpochMilli(calciteTsLtz),
Instant.ofEpochMilli(tsLtz));
+ }
+
+ private static Stream<Arguments> timeZoneTime() {
+ List<String> zones = List.of("Europe/Paris", "Europe/Moscow",
"Asia/Tokyo", "America/New_York");
+ List<LocalDateTime> times = List.of(
+ LocalDateTime.of(2012, 7, 19, 11, 13, 58, 1_000_000),
+ LocalDateTime.of(2012, 7, 19, 11, 13, 58, 123_000_000),
+ LocalDateTime.of(2012, 7, 19, 11, 13, 58, 500_000_000),
+ LocalDateTime.of(2012, 7, 19, 11, 13, 58, 999_000_000),
+
+ LocalDateTime.of(2025, 5, 7, 11, 13, 58, 1_000_000),
+ LocalDateTime.of(2025, 5, 7, 11, 13, 58, 123_000_000),
+ LocalDateTime.of(2025, 5, 7, 11, 13, 58, 500_000_000),
+ LocalDateTime.of(2025, 5, 7, 11, 13, 58, 999_000_000),
+
+ LocalDateTime.of(2023, 10, 29, 2, 1, 1, 111_000_000),
+ LocalDateTime.of(2023, 10, 29, 3, 1, 1, 111_000_000),
+ LocalDateTime.of(2023, 10, 29, 4, 1, 1, 111_000_000),
+ LocalDateTime.of(2023, 10, 29, 5, 1, 1, 111_000_000),
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-25342
+ // This time (02:00 - 02:59) does not exist because France
switches to the summer time schedule
+ // LocalDateTime.of(2024, 3, 31, 2, 1, 1, 111_000_000),
+ LocalDateTime.of(2024, 3, 31, 3, 1, 1, 111_000_000),
+ LocalDateTime.of(2024, 3, 31, 4, 1, 1, 111_000_000),
+ LocalDateTime.of(2024, 3, 31, 5, 1, 1, 111_000_000)
+ );
+
+ return zones.stream().flatMap(z -> times.stream().map(t ->
Arguments.of(z, t)));
+ }
}