This is an automated email from the ASF dual-hosted git repository. ppa 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 71b24675d2 IGNITE-21551 Sql. Enable TIMESTAMP_WITH_LOCAL_TIME_ZONE type (#3229) 71b24675d2 is described below commit 71b24675d2def00a9485b26ce2e6af51b41c5864 Author: Pavel Pereslegin <xxt...@gmail.com> AuthorDate: Mon Feb 26 15:20:59 2024 +0300 IGNITE-21551 Sql. Enable TIMESTAMP_WITH_LOCAL_TIME_ZONE type (#3229) --- .../main/java/org/apache/ignite/sql/Session.java | 16 ++ .../ignite/internal/client/sql/ClientSession.java | 8 + .../internal/client/sql/ClientSessionBuilder.java | 7 + .../apache/ignite/client/fakes/FakeSession.java | 8 + .../ignite/client/fakes/FakeSessionBuilder.java | 7 + .../internal/table/ItPublicApiColocationTest.java | 4 +- .../internal/ClusterPerClassIntegrationTest.java | 14 +- .../internal/ClusterPerTestIntegrationTest.java | 2 +- .../ignite/internal/sql/api/ItCommonApiTest.java | 39 ++- .../internal/sql/engine/ItCreateTableDdlTest.java | 6 +- .../internal/sql/engine/ItDataTypesTest.java | 122 ++++---- .../internal/sql/engine/ItFunctionsTest.java | 57 ++-- .../ignite/internal/sql/engine/ItIntervalTest.java | 318 +++++++++++++++++---- .../ignite/internal/sql/engine/ItMetadataTest.java | 14 +- .../src/main/codegen/includes/parserImpls.ftl | 10 +- .../internal/sql/api/SessionBuilderImpl.java | 11 + .../ignite/internal/sql/api/SessionImpl.java | 17 +- .../ignite/internal/sql/engine/QueryProperty.java | 2 + .../internal/sql/engine/SqlQueryProcessor.java | 20 +- .../internal/sql/engine/exec/ExecutionContext.java | 22 +- .../sql/engine/exec/ExecutionServiceImpl.java | 13 +- .../sql/engine/exec/exp/ExpressionFactoryImpl.java | 7 + .../sql/engine/exec/exp/IgniteSqlFunctions.java | 18 +- .../internal/sql/engine/exec/exp/RexImpTable.java | 2 + .../sql/engine/exec/exp/RexToLixTranslator.java | 17 +- .../sql/engine/message/QueryStartRequest.java | 5 + .../internal/sql/engine/util/BaseQueryContext.java | 28 +- .../engine/util/IgniteCustomAssignmentsRules.java | 14 +- .../internal/sql/engine/util/IgniteMethod.java | 4 + .../ignite/internal/sql/engine/util/TypeUtils.java | 3 +- .../ignite/internal/sql/api/SessionImplTest.java | 35 ++- .../sql/engine/exec/ExecutionServiceImplTest.java | 33 +-- .../sql/engine/exec/RuntimeSortedIndexTest.java | 4 +- .../engine/exec/exp/IgniteSqlFunctionsTest.java | 34 +++ .../sql/engine/exec/rel/AbstractExecutionTest.java | 4 +- .../engine/exec/rel/MergeJoinExecutionTest.java | 5 +- .../sql/engine/framework/TestBuilders.java | 4 +- .../internal/sql/engine/framework/TestNode.java | 2 + .../sql/engine/planner/CastResolutionTest.java | 37 +-- .../internal/sql/engine/sql/SqlDdlParserTest.java | 19 +- .../internal/sql/engine/util/QueryChecker.java | 3 + .../internal/sql/engine/util/QueryCheckerImpl.java | 33 ++- 42 files changed, 763 insertions(+), 265 deletions(-) diff --git a/modules/api/src/main/java/org/apache/ignite/sql/Session.java b/modules/api/src/main/java/org/apache/ignite/sql/Session.java index a969942acc..1d64ed1915 100644 --- a/modules/api/src/main/java/org/apache/ignite/sql/Session.java +++ b/modules/api/src/main/java/org/apache/ignite/sql/Session.java @@ -17,6 +17,7 @@ package org.apache.ignite.sql; +import java.time.ZoneId; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Flow; import java.util.concurrent.TimeUnit; @@ -283,6 +284,13 @@ public interface Session extends AutoCloseable { */ int defaultPageSize(); + /** + * Returns time zone used for this session. + * + * @return Time zone used for this session. + */ + ZoneId timeZoneId(); + /** * Returns a session property. * @@ -398,6 +406,14 @@ public interface Session extends AutoCloseable { */ SessionBuilder defaultPageSize(int pageSize); + /** + * Sets a default time zone for this session. + * + * @param timeZoneId Time zone ID. + * @return {@code this} for chaining. + */ + SessionBuilder timeZoneId(ZoneId timeZoneId); + /** * Returns a session property. * diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSession.java b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSession.java index ce21e337e9..bb7bfdff5a 100644 --- a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSession.java +++ b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSession.java @@ -21,6 +21,7 @@ import static org.apache.ignite.internal.client.ClientUtils.sync; import static org.apache.ignite.internal.client.table.ClientTable.writeTx; import static org.apache.ignite.internal.util.CompletableFutures.nullCompletedFuture; +import java.time.ZoneId; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -283,6 +284,13 @@ public class ClientSession implements AbstractSession { return defaultPageSize == null ? 0 : defaultPageSize; } + /** {@inheritDoc} */ + @Override + public ZoneId timeZoneId() { + // TODO https://issues.apache.org/jira/browse/IGNITE-21568 + throw new UnsupportedOperationException(); + } + /** {@inheritDoc} */ @Override public @Nullable Object property(String name) { diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSessionBuilder.java b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSessionBuilder.java index ad8e3c3d63..ca168b26dc 100644 --- a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSessionBuilder.java +++ b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSessionBuilder.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.client.sql; +import java.time.ZoneId; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -119,6 +120,12 @@ public class ClientSessionBuilder implements SessionBuilder { return this; } + @Override + public SessionBuilder timeZoneId(ZoneId timeZoneId) { + // TODO https://issues.apache.org/jira/browse/IGNITE-21568 + throw new UnsupportedOperationException(); + } + @Override public @Nullable Object property(String name) { return properties.get(name); diff --git a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeSession.java b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeSession.java index 9a0e79eed1..9a42b8db2e 100644 --- a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeSession.java +++ b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeSession.java @@ -21,6 +21,7 @@ import static org.apache.ignite.internal.client.ClientUtils.sync; import static org.apache.ignite.internal.util.CompletableFutures.nullCompletedFuture; import static org.apache.ignite.lang.ErrorGroups.Sql.STMT_VALIDATION_ERR; +import java.time.ZoneId; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -239,6 +240,13 @@ public class FakeSession implements AbstractSession { return defaultPageSize; } + /** {@inheritDoc} */ + @Override + public ZoneId timeZoneId() { + // TODO https://issues.apache.org/jira/browse/IGNITE-21568 + throw new UnsupportedOperationException(); + } + /** {@inheritDoc} */ @Override public @Nullable Object property(String name) { diff --git a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeSessionBuilder.java b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeSessionBuilder.java index 3d8876167a..c704377a13 100644 --- a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeSessionBuilder.java +++ b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeSessionBuilder.java @@ -17,6 +17,7 @@ package org.apache.ignite.client.fakes; +import java.time.ZoneId; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -107,6 +108,12 @@ public class FakeSessionBuilder implements SessionBuilder { return this; } + /** {@inheritDoc} */ + @Override + public SessionBuilder timeZoneId(ZoneId timeZoneId) { + throw new UnsupportedOperationException(); + } + /** {@inheritDoc} */ @Override public @Nullable Object property(String name) { diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java index 6669649d90..45985a1c22 100644 --- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java +++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java @@ -66,9 +66,7 @@ public class ItPublicApiColocationTest extends ClusterPerClassIntegrationTest { */ private static final Set<NativeTypeSpec> EXCLUDED_TYPES = Stream.of( NativeTypeSpec.BITMASK, - NativeTypeSpec.NUMBER, - // TODO: Remove after IGNITE-19274 - NativeTypeSpec.TIMESTAMP) + NativeTypeSpec.NUMBER) .collect(Collectors.toSet()); @AfterEach diff --git a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java index 32233e6e49..ca84a41922 100644 --- a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java +++ b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java @@ -24,6 +24,7 @@ import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCo import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -356,13 +357,14 @@ public abstract class ClusterPerClassIntegrationTest extends IgniteIntegrationTe * * @param node Ignite instance to run a query. * @param tx Transaction to run a given query. Can be {@code null} to run within implicit transaction. + * @param zoneId Client time zone. * @param sql Query to be run. * @param args Dynamic parameters for a given query. * @return List of lists, where outer list represents a rows, internal lists represents a columns. */ - public static List<List<Object>> sql(Ignite node, @Nullable Transaction tx, String sql, Object... args) { + public static List<List<Object>> sql(Ignite node, @Nullable Transaction tx, @Nullable ZoneId zoneId, String sql, Object... args) { try ( - Session session = node.sql().createSession(); + Session session = node.sql().sessionBuilder().timeZoneId(zoneId).build(); ResultSet<SqlRow> rs = session.execute(tx, sql, args) ) { return getAllResultSet(rs); @@ -374,12 +376,16 @@ public abstract class ClusterPerClassIntegrationTest extends IgniteIntegrationTe } protected static List<List<Object>> sql(int nodeIndex, @Nullable Transaction tx, String sql, Object[] args) { + return sql(nodeIndex, tx, null, sql, args); + } + + protected static List<List<Object>> sql(int nodeIndex, @Nullable Transaction tx, @Nullable ZoneId zoneId, String sql, Object[] args) { IgniteImpl node = CLUSTER.node(nodeIndex); if (!AWAIT_INDEX_AVAILABILITY.get()) { - return sql(node, tx, sql, args); + return sql(node, tx, zoneId, sql, args); } else { - return executeAwaitingIndexes(node, (n) -> sql(n, tx, sql, args)); + return executeAwaitingIndexes(node, (n) -> sql(n, tx, zoneId, sql, args)); } } diff --git a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerTestIntegrationTest.java b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerTestIntegrationTest.java index d115619117..6362c04674 100644 --- a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerTestIntegrationTest.java +++ b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerTestIntegrationTest.java @@ -213,6 +213,6 @@ public abstract class ClusterPerTestIntegrationTest extends IgniteIntegrationTes protected final List<List<Object>> executeSql(int nodeIndex, String sql, Object... args) { IgniteImpl ignite = node(nodeIndex); - return ClusterPerClassIntegrationTest.sql(ignite, null, sql, args); + return ClusterPerClassIntegrationTest.sql(ignite, null, null, sql, args); } } diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItCommonApiTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItCommonApiTest.java index 08ded62911..041d54e619 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItCommonApiTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItCommonApiTest.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.sql.api; +import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCause; import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition; import static org.hamcrest.MatcherAssert.assertThat; @@ -28,6 +29,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.apache.ignite.Ignite; @@ -102,41 +105,49 @@ public class ItCommonApiTest extends BaseSqlIntegrationTest { //String tsStr = "2023-03-29T08:22:33.005007Z"; String tsStr = "2023-03-29T08:22:33.005Z"; - Instant ins = Instant.parse(tsStr); + LocalDateTime localDate = LocalDateTime.parse(tsStr, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")); + Instant instant = localDate.atZone(ZoneId.systemDefault()).toInstant(); - sql("CREATE TABLE timestamps(id INTEGER PRIMARY KEY, i TIMESTAMP(9))"); + sql("CREATE TABLE timestamps(id INTEGER PRIMARY KEY, i TIMESTAMP(9), i_tz TIMESTAMP WITH LOCAL TIME ZONE)"); - // TODO: IGNITE-19274 Add column with TIMESTAMP WITH LOCAL TIME ZONE - sql(String.format("CREATE TABLE %s(\"%s\" INTEGER PRIMARY KEY, \"TIMESTAMP\" TIMESTAMP(9))", kvTblName, keyCol)); + sql(format("CREATE TABLE {}(" + + "\"{}\" INTEGER PRIMARY KEY, " + + "\"TIMESTAMP\" TIMESTAMP(9), " + + "\"TIMESTAMP_TZ\" TIMESTAMP(9) WITH LOCAL TIME ZONE)", kvTblName, keyCol)); Table tbl = node.tables().table(kvTblName); + localDate = LocalDateTime.of(2023, 3, 29, 8, 22, 33, 5000000); + Tuple rec = Tuple.create() .set("KEY", 1) - .set("TIMESTAMP", LocalDateTime.of(2023, 3, 29, 8, 22, 33, 5000000)); + .set("TIMESTAMP", localDate) + .set("TIMESTAMP_TZ", instant); tbl.recordView().insert(null, rec); // TODO: https://issues.apache.org/jira/browse/IGNITE-19161 Can`t insert timestamp representing in ISO_INSTANT format - tsStr = tsStr.replace("T", " ").substring(0, tsStr.length() - 1); + String tsValue = tsStr.replace("T", " ").substring(0, tsStr.length() - 1); - sql("INSERT INTO timestamps VALUES (101, TIMESTAMP '" + tsStr + "')"); + sql(format("INSERT INTO timestamps VALUES (101, TIMESTAMP '{}', TIMESTAMP WITH LOCAL TIME ZONE '{}')", tsValue, tsValue)); try (Session ses = node.sql().createSession()) { // for projection pop up - ResultSet<SqlRow> res = ses.execute(null, "SELECT i, id FROM timestamps"); - - String srtRepr = ins.toString(); + ResultSet<SqlRow> res = ses.execute(null, "SELECT i, i_tz, id FROM timestamps"); - String expDateTimeStr = srtRepr.substring(0, srtRepr.length() - 1); + SqlRow row = res.next(); - assertEquals(expDateTimeStr, res.next().datetimeValue(0).toString()); + assertEquals(localDate, row.datetimeValue("i")); + assertEquals(instant, row.timestampValue("i_tz")); - String query = "select \"KEY\", \"TIMESTAMP\" from TBL_ALL_COLUMNS_SQL ORDER BY KEY"; + String query = "select \"KEY\", \"TIMESTAMP\", \"TIMESTAMP_TZ\" from TBL_ALL_COLUMNS_SQL ORDER BY KEY"; res = ses.execute(null, query); - assertEquals(expDateTimeStr, res.next().datetimeValue(1).toString()); + row = res.next(); + + assertEquals(localDate, row.datetimeValue("TIMESTAMP")); + assertEquals(instant, row.timestampValue("TIMESTAMP_TZ")); } } diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItCreateTableDdlTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItCreateTableDdlTest.java index 4ffbe37093..7e966c3e6c 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItCreateTableDdlTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItCreateTableDdlTest.java @@ -232,9 +232,7 @@ public class ItCreateTableDdlTest extends BaseSqlIntegrationTest { Set<NativeTypeSpec> unsupportedTypes = Set.of( // TODO https://issues.apache.org/jira/browse/IGNITE-18431 - NativeTypeSpec.BITMASK, - // TODO https://issues.apache.org/jira/browse/IGNITE-19274 - NativeTypeSpec.TIMESTAMP + NativeTypeSpec.BITMASK ); // List of columns for 'ADD COLUMN' statement. @@ -256,7 +254,7 @@ public class ItCreateTableDdlTest extends BaseSqlIntegrationTest { dropColumnsList.app(','); } - addColumnsList.app("c").app(i).app(' ').app(relDataType.getSqlTypeName()); + addColumnsList.app("c").app(i).app(' ').app(relDataType.getSqlTypeName().getSpaceName()); dropColumnsList.app("c").app(i); } diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java index b3517b88db..c17f979aaa 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java @@ -26,8 +26,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.params.provider.Arguments.arguments; import java.math.BigDecimal; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -194,48 +198,74 @@ public class ItDataTypesTest extends BaseSqlIntegrationTest { */ @Test public void testDateTime() { - assertQuery("SELECT date '1992-01-19'").returns(sqlDate("1992-01-19")).check(); - assertQuery("SELECT date '1992-01-18' + interval (1) days").returns(sqlDate("1992-01-19")).check(); - assertQuery("SELECT date '1992-01-18' + interval (24) hours").returns(sqlDate("1992-01-19")).check(); - assertQuery("SELECT timestamp '1992-01-18 02:30:00' + interval (25) hours") - .returns(sqlDateTime("1992-01-19T03:30:00")).check(); - assertQuery("SELECT timestamp '1992-01-18 02:30:00' + interval (23) hours") - .returns(sqlDateTime("1992-01-19T01:30:00.000")).check(); - assertQuery("SELECT timestamp '1992-01-18 02:30:00' + interval (24) hours") - .returns(sqlDateTime("1992-01-19T02:30:00.000")).check(); - - assertQuery("SELECT date '1992-03-29'").returns(sqlDate("1992-03-29")).check(); - assertQuery("SELECT date '1992-03-28' + interval (1) days").returns(sqlDate("1992-03-29")).check(); - assertQuery("SELECT date '1992-03-28' + interval (24) hours").returns(sqlDate("1992-03-29")).check(); - assertQuery("SELECT timestamp '1992-03-28 02:30:00' + interval (25) hours") - .returns(sqlDateTime("1992-03-29T03:30:00.000")).check(); - assertQuery("SELECT timestamp '1992-03-28 02:30:00' + interval (23) hours") - .returns(sqlDateTime("1992-03-29T01:30:00.000")).check(); - assertQuery("SELECT timestamp '1992-03-28 02:30:00' + interval (24) hours") - .returns(sqlDateTime("1992-03-29T02:30:00.000")).check(); - - assertQuery("SELECT date '1992-09-27'").returns(sqlDate("1992-09-27")).check(); - assertQuery("SELECT date '1992-09-26' + interval (1) days").returns(sqlDate("1992-09-27")).check(); - assertQuery("SELECT date '1992-09-26' + interval (24) hours").returns(sqlDate("1992-09-27")).check(); - assertQuery("SELECT timestamp '1992-09-26 02:30:00' + interval (25) hours") - .returns(sqlDateTime("1992-09-27T03:30:00.000")).check(); - assertQuery("SELECT timestamp '1992-09-26 02:30:00' + interval (23) hours") - .returns(sqlDateTime("1992-09-27T01:30:00.000")).check(); - assertQuery("SELECT timestamp '1992-09-26 02:30:00' + interval (24) hours") - .returns(sqlDateTime("1992-09-27T02:30:00.000")).check(); - - assertQuery("SELECT date '2021-11-07'").returns(sqlDate("2021-11-07")).check(); - assertQuery("SELECT date '2021-11-06' + interval (1) days").returns(sqlDate("2021-11-07")).check(); - assertQuery("SELECT date '2021-11-06' + interval (24) hours").returns(sqlDate("2021-11-07")).check(); - assertQuery("SELECT timestamp '2021-11-06 01:30:00' + interval (25) hours") - .returns(sqlDateTime("2021-11-07T02:30:00.000")).check(); - // Check string representation here, since after timestamp calculation we have '2021-11-07T01:30:00.000-0800' - // but Timestamp.valueOf method converts '2021-11-07 01:30:00' in 'America/Los_Angeles' time zone to - // '2021-11-07T01:30:00.000-0700' (we pass through '2021-11-07 01:30:00' twice after DST ended). - assertQuery("SELECT (timestamp '2021-11-06 02:30:00' + interval (23) hours)::varchar") - .returns("2021-11-07 01:30:00").check(); - assertQuery("SELECT (timestamp '2021-11-06 01:30:00' + interval (24) hours)::varchar") - .returns("2021-11-07 01:30:00").check(); + assertQuery("SELECT time '12:34:56'") + .returns(LocalTime.parse("12:34:56")) + .check(); + + assertQuery("SELECT date '1992-01-19'") + .returns(LocalDate.parse("1992-01-19")) + .check(); + + assertQuery("SELECT timestamp '1992-01-18 02:30:00.123'") + .returns(LocalDateTime.parse("1992-01-18T02:30:00.123")) + .check(); + + assertQuery("SELECT timestamp with local time zone '1992-01-18 02:30:00.123'") + .withTimeZoneId(ZoneId.of("America/New_York")) + .returns(Instant.parse("1992-01-18T07:30:00.123Z")) + .check(); + } + + /** + * Checks the conversion from {@link SqlTypeName#TIMESTAMP} to {@link SqlTypeName#TIMESTAMP_WITH_LOCAL_TIME_ZONE} + * and vise versa using dynamic parameters. + */ + @Test + public void testTimestampConversion() { + String date = "1970-01-01"; + String time = "00:00:00.056"; + + String tsStr = date + ' ' + time; + String isoStr = date + 'T' + time; + + sql("CREATE TABLE timestamps(id INT PRIMARY KEY, ts TIMESTAMP, ts_tz TIMESTAMP WITH LOCAL TIME ZONE)"); + + ZoneId zoneId = ZoneId.of("America/New_York"); + + LocalDateTime localDateTime = LocalDateTime.parse(isoStr); + Instant instant = localDateTime.atZone(zoneId).toInstant(); + + assertQuery(format("INSERT INTO timestamps VALUES(1, '{}', '{}')", tsStr, tsStr)) + .withTimeZoneId(zoneId) + .returns(1L) + .check(); + assertQuery("SELECT ts, ts_tz FROM timestamps WHERE id=?") + .withParam(1) + .returns(localDateTime, instant) + .check(); + + // Instant -> LocalDateTime + assertQuery("INSERT INTO timestamps VALUES(2, ?, ?)") + .withTimeZoneId(zoneId) + .withParams(instant, instant) + .returns(1L) + .check(); + assertQuery("SELECT ts, ts_tz FROM timestamps WHERE id=?") + .withParam(2) + .returns(localDateTime, instant) + .check(); + + // LocalDateTime -> Instant + assertQuery("INSERT INTO timestamps VALUES(3, ?, ?)") + .withTimeZoneId(zoneId) + .withParams(localDateTime, localDateTime) + .returns(1L) + .check(); + assertQuery("SELECT ts, ts_tz FROM timestamps WHERE id=?") + .withParam(3) + // TODO Conversion loses precision https://issues.apache.org/jira/browse/IGNITE-21567 + .returns(localDateTime, instant.truncatedTo(ChronoUnit.SECONDS)) + .check(); } /** Test decimal scale for dynamic parameters. */ @@ -688,12 +718,4 @@ public class ItDataTypesTest extends BaseSqlIntegrationTest { assertThat(err.getMessage(), containsString(result.error)); } } - - private LocalDate sqlDate(String str) { - return LocalDate.parse(str); - } - - private LocalDateTime sqlDateTime(String str) { - return LocalDateTime.parse(str); - } } diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItFunctionsTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItFunctionsTest.java index a58be5b466..868253f5a3 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItFunctionsTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItFunctionsTest.java @@ -28,15 +28,21 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; import java.time.temporal.Temporal; +import java.util.List; import java.util.function.Function; import java.util.stream.Stream; import org.apache.calcite.sql.validate.SqlValidatorException; import org.apache.ignite.internal.sql.BaseSqlIntegrationTest; import org.apache.ignite.internal.sql.engine.util.MetadataMatcher; +import org.apache.ignite.internal.util.ArrayUtils; import org.apache.ignite.lang.ErrorGroups.Sql; import org.apache.ignite.lang.IgniteException; import org.apache.ignite.sql.ColumnType; @@ -44,6 +50,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; /** * Test Ignite SQL functions. @@ -75,25 +82,41 @@ public class ItFunctionsTest extends BaseSqlIntegrationTest { assertQuery("SELECT OCTET_LENGTH(NULL)").returns(NULL_RESULT).check(); } - @Test - public void testCurrentDateTimeTimeStamp() { - checkDateTimeQuery("SELECT CURRENT_DATE", Clock.DATE_CLOCK, LocalDate.class); - checkDateTimeQuery("SELECT CURRENT_TIME", Clock.TIME_CLOCK, LocalTime.class); - checkDateTimeQuery("SELECT CURRENT_TIMESTAMP", Clock.DATE_TIME_CLOCK, LocalDateTime.class); - checkDateTimeQuery("SELECT LOCALTIME", Clock.TIME_CLOCK, LocalTime.class); - checkDateTimeQuery("SELECT LOCALTIMESTAMP", Clock.DATE_TIME_CLOCK, LocalDateTime.class); - checkDateTimeQuery("SELECT {fn CURDATE()}", Clock.DATE_CLOCK, LocalDate.class); - checkDateTimeQuery("SELECT {fn CURTIME()}", Clock.TIME_CLOCK, LocalTime.class); - checkDateTimeQuery("SELECT {fn NOW()}", Clock.DATE_TIME_CLOCK, LocalDateTime.class); + @ParameterizedTest(name = "use default time zone: {0}") + @ValueSource(booleans = {true, false}) + public void testCurrentDateTimeTimeStamp(boolean useDefaultTimeZone) { + ZoneId zoneId = ZoneId.systemDefault(); + + if (!useDefaultTimeZone) { + ZoneId utcZone = ZoneId.ofOffset("GMT", ZoneOffset.UTC); + Instant now = Instant.now(); + + if (now.atZone(zoneId).toLocalDateTime().equals(now.atZone(utcZone).toLocalDateTime())) { + zoneId = ZoneId.ofOffset("GMT", ZoneOffset.of("+01:00")); + } else { + zoneId = utcZone; + } + } + + checkDateTimeQuery("SELECT CURRENT_DATE", Clock.DATE_CLOCK, LocalDate.class, zoneId); + checkDateTimeQuery("SELECT CURRENT_TIME", Clock.TIME_CLOCK, LocalTime.class, zoneId); + checkDateTimeQuery("SELECT CURRENT_TIMESTAMP", Clock.DATE_TIME_CLOCK, LocalDateTime.class, zoneId); + checkDateTimeQuery("SELECT LOCALTIME", Clock.TIME_CLOCK, LocalTime.class, zoneId); + checkDateTimeQuery("SELECT LOCALTIMESTAMP", Clock.DATE_TIME_CLOCK, LocalDateTime.class, zoneId); + checkDateTimeQuery("SELECT {fn CURDATE()}", Clock.DATE_CLOCK, LocalDate.class, zoneId); + checkDateTimeQuery("SELECT {fn CURTIME()}", Clock.TIME_CLOCK, LocalTime.class, zoneId); + checkDateTimeQuery("SELECT {fn NOW()}", Clock.DATE_TIME_CLOCK, LocalDateTime.class, zoneId); } - private static <T extends Temporal & Comparable<? super T>> void checkDateTimeQuery(String sql, Clock<T> clock, Class<T> cls) { + private static <T extends Temporal & Comparable<? super T>> void checkDateTimeQuery( + String sql, Clock<T> clock, Class<T> cls, ZoneId timeZone + ) { while (true) { - T tsBeg = clock.now(); + T tsBeg = clock.now(timeZone); - var res = sql(sql); + List<List<Object>> res = sql(0, null, timeZone, sql, ArrayUtils.OBJECT_EMPTY_ARRAY); - T tsEnd = clock.now(); + T tsEnd = clock.now(timeZone); // Date changed, time comparison may return wrong result. if (tsBeg.compareTo(tsEnd) > 0) { @@ -560,7 +583,7 @@ public class ItFunctionsTest extends BaseSqlIntegrationTest { /** * A clock reporting a local time. */ - Clock<LocalTime> TIME_CLOCK = LocalTime::now; + Clock<LocalTime> TIME_CLOCK = zoneId -> LocalTime.now(zoneId).truncatedTo(ChronoUnit.MILLIS); /** * A clock reporting a local date. @@ -570,13 +593,13 @@ public class ItFunctionsTest extends BaseSqlIntegrationTest { /** * A clock reporting a local datetime. */ - Clock<LocalDateTime> DATE_TIME_CLOCK = LocalDateTime::now; + Clock<LocalDateTime> DATE_TIME_CLOCK = zoneId -> LocalDateTime.now(zoneId).truncatedTo(ChronoUnit.MILLIS); /** * Returns a temporal value representing the current moment. * * @return Current moment representing by a temporal value. */ - T now(); + T now(ZoneId zoneId); } } diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItIntervalTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItIntervalTest.java index 36706a6552..100f84a9f4 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItIntervalTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItIntervalTest.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.sql.engine; +import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; import static org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -28,12 +29,28 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Period; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiConsumer; +import java.util.function.Function; +import org.apache.calcite.sql.type.SqlTypeName; import org.apache.ignite.internal.lang.IgniteInternalException; import org.apache.ignite.internal.sql.BaseSqlIntegrationTest; import org.apache.ignite.lang.ErrorGroups.Sql; import org.apache.ignite.lang.IgniteException; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; /** Interval coverage tests. */ public class ItIntervalTest extends BaseSqlIntegrationTest { @@ -227,52 +244,24 @@ public class ItIntervalTest extends BaseSqlIntegrationTest { .check(); } + @ParameterizedTest(name = "{0}") + @MethodSource("dateTimeIntervalTestCases") + public void testBasicDateTimeIntervalArithmetic(DateTimeIntervalBasicTestCase testCase) { + assertQuery(testCase.query()) + .withTimeZoneId(DateTimeIntervalBasicTestCase.TIME_ZONE_ID) + .returns(testCase.expected()) + .check(); + } + /** - * Test interval arithmetic. + * Test date and time interval arithmetic. */ @Test - public void testIntervalArithmetic() { + public void testDateTimeIntervalArithmetic() { // Date +/- interval. - assertEquals(LocalDate.parse("2021-01-02"), eval("DATE '2021-01-01' + INTERVAL 1 DAY")); - assertEquals(LocalDate.parse("2020-12-31"), eval("DATE '2021-01-01' - INTERVAL 1 DAY")); assertEquals(LocalDate.parse("2020-12-31"), eval("DATE '2021-01-01' + INTERVAL -1 DAY")); - assertEquals(LocalDate.parse("2021-02-01"), eval("DATE '2021-01-01' + INTERVAL 1 MONTH")); - assertEquals(LocalDate.parse("2022-01-01"), eval("DATE '2021-01-01' + INTERVAL 1 YEAR")); assertEquals(LocalDate.parse("2022-02-01"), eval("DATE '2021-01-01' + INTERVAL '1-1' YEAR TO MONTH")); - // Timestamp +/- interval. - assertEquals(LocalDateTime.parse("2021-01-01T00:00:01"), - eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 SECOND")); - assertEquals(LocalDateTime.parse("2021-01-01T00:00:01.123"), - eval("TIMESTAMP '2021-01-01 00:00:00.123' + INTERVAL 1 SECOND")); - assertEquals(LocalDateTime.parse("2021-01-01T00:00:01.123"), - eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL '1.123' SECOND")); - assertEquals(LocalDateTime.parse("2021-01-01T00:00:01.246"), - eval("TIMESTAMP '2021-01-01 00:00:00.123' + INTERVAL '1.123' SECOND")); - assertEquals(LocalDateTime.parse("2020-12-31T23:59:59"), - eval("TIMESTAMP '2021-01-01 00:00:00' - INTERVAL 1 SECOND")); - assertEquals(LocalDateTime.parse("2020-12-31T23:59:59"), - eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL -1 SECOND")); - assertEquals(LocalDateTime.parse("2021-01-01T00:01:00"), - eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 MINUTE")); - assertEquals(LocalDateTime.parse("2021-01-01T01:00:00"), - eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 HOUR")); - assertEquals(LocalDateTime.parse("2021-01-02T00:00:00"), - eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 DAY")); - assertEquals(LocalDateTime.parse("2021-02-01T00:00:00"), - eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 MONTH")); - assertEquals(LocalDateTime.parse("2022-01-01T00:00:00"), - eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 YEAR")); - assertEquals(LocalDateTime.parse("2021-01-02T01:01:01.123"), - eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL '1 1:1:1.123' DAY TO SECOND")); - assertEquals(LocalDateTime.parse("2022-02-01T01:01:01.123"), - eval("TIMESTAMP '2021-01-01 01:01:01.123' + INTERVAL '1-1' YEAR TO MONTH")); - - // Time +/- interval. - assertEquals(LocalTime.parse("00:00:01"), eval("TIME '00:00:00' + INTERVAL 1 SECOND")); - assertEquals(LocalTime.parse("00:01:00"), eval("TIME '00:00:00' + INTERVAL 1 MINUTE")); - assertEquals(LocalTime.parse("01:00:00"), eval("TIME '00:00:00' + INTERVAL 1 HOUR")); - // Date - date as interval. assertEquals(Duration.ofDays(1), eval("(DATE '2021-01-02' - DATE '2021-01-01') DAYS")); assertEquals(Duration.ofDays(-1), eval("(DATE '2021-01-01' - DATE '2021-01-02') DAYS")); @@ -282,35 +271,82 @@ public class ItIntervalTest extends BaseSqlIntegrationTest { assertEquals(Period.ofMonths(-1), eval("(DATE '2021-01-01' - DATE '2021-02-01') MONTHS")); assertEquals(Period.ofMonths(0), eval("(DATE '2021-01-20' - DATE '2021-01-01') MONTHS")); + // Time - time as interval. + assertEquals(Duration.ofHours(1), eval("(TIME '02:00:00' - TIME '01:00:00') HOURS")); + assertEquals(Duration.ofMinutes(1), eval("(TIME '00:02:00' - TIME '00:01:00') HOURS")); + assertEquals(Duration.ofMinutes(1), eval("(TIME '00:02:00' - TIME '00:01:00') MINUTES")); + assertEquals(Duration.ofSeconds(1), eval("(TIME '00:00:02' - TIME '00:00:01') SECONDS")); + assertEquals(Duration.ofMillis(123), eval("(TIME '00:00:01.123' - TIME '00:00:01') SECONDS")); + } + + /** Timestamp [with local time zone] interval arithmetic. */ + @ParameterizedTest + @EnumSource(value = SqlTypeName.class, names = {"TIMESTAMP", "TIMESTAMP_WITH_LOCAL_TIME_ZONE"}) + public void testTimestampIntervalArithmetic(SqlTypeName sqlTypeName) { + String typeName = sqlTypeName.getSpaceName(); + // Timestamp - timestamp as interval. assertEquals(Duration.ofDays(1), - eval("(TIMESTAMP '2021-01-02 00:00:00' - TIMESTAMP '2021-01-01 00:00:00') DAYS")); + eval(format("({} '2021-01-02 00:00:00' - {} '2021-01-01 00:00:00') DAYS", typeName, typeName))); assertEquals(Duration.ofDays(-1), - eval("(TIMESTAMP '2021-01-01 00:00:00' - TIMESTAMP '2021-01-02 00:00:00') DAYS")); + eval(format("({} '2021-01-01 00:00:00' - {} '2021-01-02 00:00:00') DAYS", typeName, typeName))); assertEquals(Duration.ofHours(1), - eval("(TIMESTAMP '2021-01-01 01:00:00' - TIMESTAMP '2021-01-01 00:00:00') HOURS")); + eval(format("({} '2021-01-01 01:00:00' - {} '2021-01-01 00:00:00') HOURS", typeName, typeName))); assertEquals(Duration.ofMinutes(1), - eval("(TIMESTAMP '2021-01-01 00:01:00' - TIMESTAMP '2021-01-01 00:00:00') MINUTES")); + eval(format("({} '2021-01-01 00:01:00' - {} '2021-01-01 00:00:00') MINUTES", typeName, typeName))); assertEquals(Duration.ofSeconds(1), - eval("(TIMESTAMP '2021-01-01 00:00:01' - TIMESTAMP '2021-01-01 00:00:00') SECONDS")); + eval(format("({} '2021-01-01 00:00:01' - {} '2021-01-01 00:00:00') SECONDS", typeName, typeName))); assertEquals(Duration.ofMillis(123), - eval("(TIMESTAMP '2021-01-01 00:00:00.123' - TIMESTAMP '2021-01-01 00:00:00') SECONDS")); + eval(format("({} '2021-01-01 00:00:00.123' - {} '2021-01-01 00:00:00') SECONDS", typeName, typeName))); assertEquals(Period.ofYears(1), - eval("(TIMESTAMP '2022-01-01 00:00:00' - TIMESTAMP '2021-01-01 00:00:00') YEARS")); + eval(format("({} '2022-01-01 00:00:00' - {} '2021-01-01 00:00:00') YEARS", typeName, typeName))); assertEquals(Period.ofMonths(1), - eval("(TIMESTAMP '2021-02-01 00:00:00' - TIMESTAMP '2021-01-01 00:00:00') MONTHS")); + eval(format("({} '2021-02-01 00:00:00' - {} '2021-01-01 00:00:00') MONTHS", typeName, typeName))); assertEquals(Period.ofMonths(-1), - eval("(TIMESTAMP '2021-01-01 00:00:00' - TIMESTAMP '2021-02-01 00:00:00') MONTHS")); + eval(format("({} '2021-01-01 00:00:00' - {} '2021-02-01 00:00:00') MONTHS", typeName, typeName))); assertEquals(Period.ofMonths(0), - eval("(TIMESTAMP '2021-01-20 00:00:00' - TIMESTAMP '2021-01-01 00:00:00') MONTHS")); - - // Time - time as interval. - assertEquals(Duration.ofHours(1), eval("(TIME '02:00:00' - TIME '01:00:00') HOURS")); - assertEquals(Duration.ofMinutes(1), eval("(TIME '00:02:00' - TIME '00:01:00') HOURS")); - assertEquals(Duration.ofMinutes(1), eval("(TIME '00:02:00' - TIME '00:01:00') MINUTES")); - assertEquals(Duration.ofSeconds(1), eval("(TIME '00:00:02' - TIME '00:00:01') SECONDS")); - assertEquals(Duration.ofMillis(123), eval("(TIME '00:00:01.123' - TIME '00:00:01') SECONDS")); + eval(format("({} '2021-01-20 00:00:00' - {} '2021-01-01 00:00:00') MONTHS", typeName, typeName))); + + // Check string representation here, since after timestamp calculation we have '2021-11-07T01:30:00.000-0800' + // but Timestamp.valueOf method converts '2021-11-07 01:30:00' in 'America/Los_Angeles' time zone to + // '2021-11-07T01:30:00.000-0700' (we pass through '2021-11-07 01:30:00' twice after DST ended). + ZoneId zoneId = ZoneId.systemDefault(); + String tzSuffix = sqlTypeName == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE ? ' ' + zoneId.getId() : ""; + + assertQuery(format("SELECT ({} '2021-11-06 02:30:00' + interval (23) hours)::varchar", typeName)) + .withTimeZoneId(zoneId) + .returns("2021-11-07 01:30:00" + tzSuffix).check(); + + assertQuery(format("SELECT ({} '2021-11-06 01:30:00' + interval (24) hours)::varchar", typeName)) + .withTimeZoneId(zoneId) + .returns("2021-11-07 01:30:00" + tzSuffix).check(); + + // Timestamp - interval. + BiConsumer<String, String> timestampChecker = (expression, expected) -> { + ZoneId timeZoneId = ZoneId.systemDefault(); + + Function<String, Object> validator = sqlTypeName == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE + ? ts -> LocalDateTime.parse(ts).atZone(timeZoneId).toInstant() + : LocalDateTime::parse; + + assertQuery(format(expression, sqlTypeName.getSpaceName())) + .withTimeZoneId(timeZoneId) + .returns(validator.apply(expected)) + .check(); + }; + + timestampChecker.accept("SELECT {} '2021-01-01 00:00:00' + INTERVAL '1.123' SECOND", "2021-01-01T00:00:01.123"); + timestampChecker.accept("SELECT {} '2021-01-01 00:00:00.123' + INTERVAL '1.123' SECOND", "2021-01-01T00:00:01.246"); + timestampChecker.accept("SELECT {} '2021-01-01 00:00:00' + INTERVAL '1 1:1:1.123' DAY TO SECOND", "2021-01-02T01:01:01.123"); + + // TODO Enable this case after https://issues.apache.org/jira/browse/IGNITE-21557 + if (sqlTypeName != SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE) { + timestampChecker.accept("SELECT {} '2021-01-01 01:01:01.123' + INTERVAL '1-1' YEAR TO MONTH", "2022-02-01T01:01:01.123"); + } + } + @Test + public void testIntervalArithmetic() { // Interval +/- interval. assertEquals(Duration.ofSeconds(2), eval("INTERVAL 1 SECONDS + INTERVAL 1 SECONDS")); assertEquals(Duration.ofSeconds(1), eval("INTERVAL 2 SECONDS - INTERVAL 1 SECONDS")); @@ -408,4 +444,174 @@ public class ItIntervalTest extends BaseSqlIntegrationTest { assertTrue(ex.getMessage().toLowerCase().contains(errMsg.toLowerCase())); } + + private static List<DateTimeIntervalBasicTestCase> dateTimeIntervalTestCases() { + SqlTypeName[] types = { + SqlTypeName.TIME, + SqlTypeName.DATE, + SqlTypeName.TIMESTAMP, + SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE + }; + + Map<ChronoUnit, int[]> timeUnitData = new LinkedHashMap<>(); + + timeUnitData.put(ChronoUnit.SECONDS, new int[]{1, 59, 60, 61, 100, 1_000, 10_000}); + timeUnitData.put(ChronoUnit.MINUTES, new int[]{1, 59, 60, 61, 100, 1_000, 10_000}); + timeUnitData.put(ChronoUnit.HOURS, new int[]{1, 23, 24, 25, 48, 96, 1_000, 10_000}); + timeUnitData.put(ChronoUnit.DAYS, new int[]{1, 29, 30, 31, 100, 1_000, 10_000}); + timeUnitData.put(ChronoUnit.MONTHS, new int[]{1, 11, 12, 13, 100, 1_000}); + timeUnitData.put(ChronoUnit.YEARS, new int[]{1, 10, 100, 1_000}); + + List<DateTimeIntervalBasicTestCase> testCases = new ArrayList<>(); + + for (SqlTypeName typeName : types) { + for (Entry<ChronoUnit, int[]> entry : timeUnitData.entrySet()) { + ChronoUnit unit = entry.getKey(); + // TODO Remove after https://issues.apache.org/jira/browse/IGNITE-21557 + if (typeName == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE + && (unit == ChronoUnit.MONTHS || unit == ChronoUnit.YEARS)) { + continue; + } + + // TODO Remove after https://issues.apache.org/jira/browse/IGNITE-21589 + if (typeName == SqlTypeName.TIME && (unit == ChronoUnit.HOURS || unit == ChronoUnit.DAYS)) { + continue; + } + + for (int amount : entry.getValue()) { + testCases.add(DateTimeIntervalBasicTestCase.newTestCase(typeName, unit, amount)); + testCases.add(DateTimeIntervalBasicTestCase.newTestCase(typeName, unit, -amount)); + } + } + } + + return testCases; + } + + abstract static class DateTimeIntervalBasicTestCase { + private static final ZoneId TIME_ZONE_ID = ZoneId.of("Asia/Nicosia"); + private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + private static final String dateString = "1992-01-19 00:00:00.123"; + private static final LocalDateTime testLocalDate = LocalDateTime.parse(dateString, dateTimeFormatter); + + final SqlTypeName type; + final ChronoUnit unit; + final int amount; + + private String query; + + private DateTimeIntervalBasicTestCase(SqlTypeName type, ChronoUnit unit, int amount) { + this.type = type; + this.unit = unit; + this.amount = amount; + } + + public abstract Temporal expected(); + + public String query() { + if (query == null) { + String intervalSubstring = (amount > 0 ? '+' : '-') + " interval (" + Math.abs(amount) + ") " + unit; + query = format("SELECT {} '{}'{}", type.getSpaceName(), sqlDateLiteral(), intervalSubstring); + } + + return query; + } + + String sqlDateLiteral() { + return dateString; + } + + static DateTimeIntervalBasicTestCase newTestCase(SqlTypeName type, ChronoUnit unit, Integer amount) { + switch (type) { + case TIMESTAMP: + return new SqlTimestampIntervalIntervalTestCase(unit, amount); + + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return new SqlTimestampTzIntervalIntervalTestCase(unit, amount); + + case DATE: + return new SqlDateIntervalIntervalTestCase(unit, amount); + + case TIME: + return new SqlTimeIntervalIntervalTestCase(unit, amount); + + default: + throw new UnsupportedOperationException("Not implemented: " + type); + } + } + + @Override + public String toString() { + return query(); + } + + private static class SqlTimestampIntervalIntervalTestCase extends DateTimeIntervalBasicTestCase { + private SqlTimestampIntervalIntervalTestCase(ChronoUnit unit, Integer amount) { + super(SqlTypeName.TIMESTAMP, unit, amount); + } + + @Override + public LocalDateTime expected() { + return testLocalDate.plus(amount, unit); + } + } + + private static class SqlTimestampTzIntervalIntervalTestCase extends DateTimeIntervalBasicTestCase { + private SqlTimestampTzIntervalIntervalTestCase(ChronoUnit unit, Integer amount) { + super(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, unit, amount); + } + + @Override + public Temporal expected() { + return testLocalDate.atZone(TIME_ZONE_ID).toInstant().plus(amount, unit); + } + } + + private static class SqlDateIntervalIntervalTestCase extends DateTimeIntervalBasicTestCase { + final LocalDateTime testLocalDateEndOfDay; + + SqlDateIntervalIntervalTestCase(ChronoUnit unit, Integer amount) { + super(SqlTypeName.DATE, unit, amount); + + // DateTime + 23:59:59.999 + testLocalDateEndOfDay = testLocalDate + .plus(1, ChronoUnit.DAYS) + .minus(1 + testLocalDate.get(ChronoField.MILLI_OF_SECOND), ChronoUnit.MILLIS); + } + + @Override + String sqlDateLiteral() { + return dateString.substring(0, 10); + } + + @Override + public Temporal expected() { + return (amount < 0 ? testLocalDateEndOfDay : testLocalDate).plus(amount, unit).toLocalDate(); + } + } + + private static class SqlTimeIntervalIntervalTestCase extends DateTimeIntervalBasicTestCase { + private final LocalTime initTime; + + private SqlTimeIntervalIntervalTestCase(ChronoUnit unit, Integer amount) { + super(SqlTypeName.TIME, unit, amount); + + initTime = testLocalDate.toLocalTime(); + } + + @Override + String sqlDateLiteral() { + return dateString.substring(11); + } + + @Override + public LocalTime expected() { + if (initTime.isSupported(unit)) { + return initTime.plus(amount, unit); + } + + return initTime; + } + } + } } diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java index 3ea29eb6bc..842405b367 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java @@ -166,13 +166,12 @@ public class ItMetadataTest extends BaseSqlIntegrationTest { // ANSI`99 syntax "WITH TIME ZONE" is not supported, // a "WITH LOCAL TIME ZONE" syntax MUST be used instead. + "DATE_C DATE, " + "TIME_C TIME, " + "TIME_C2 TIME(9), " - // TODO: IGNITE-19274 Ignite doesn't support the client's time zone yet. + // TODO: IGNITE-21555 Ignite doesn't support TIME_WITH_LOCAL_TIME_ZONE data type. // + "TIME_LTZ_C TIME WITH LOCAL TIME ZONE, " // + "TIME_LTZ_C2 TIME(9) WITH LOCAL TIME ZONE, " + "DATETIME_C TIMESTAMP, " + "DATETIME_C2 TIMESTAMP(9), " - // TODO: IGNITE-19274 Ignite doesn't support the client's time zone yet. - // + "TIMESTAMP_C TIMESTAMP WITH LOCAL TIME ZONE, " - // + "TIMESTAMP_C2 TIMESTAMP(9) WITH LOCAL TIME ZONE, " + + "TIMESTAMP_C TIMESTAMP WITH LOCAL TIME ZONE, " + + "TIMESTAMP_C2 TIMESTAMP(9) WITH LOCAL TIME ZONE, " // Interval types // TODO: IGNITE-17373: Ignite doesn't support interval types yet. @@ -231,14 +230,13 @@ public class ItMetadataTest extends BaseSqlIntegrationTest { new MetadataMatcher().name("DATE_C").type(ColumnType.DATE).precision(0).scale(UNDEFINED_SCALE), new MetadataMatcher().name("TIME_C").type(ColumnType.TIME).precision(0).scale(UNDEFINED_SCALE), new MetadataMatcher().name("TIME_C2").type(ColumnType.TIME).precision(9).scale(UNDEFINED_SCALE), - // TODO: IGNITE-19274 Ignite doesn't support the client's time zone yet. + // TODO: IGNITE-21555 Ignite doesn't support TIME_WITH_LOCAL_TIME_ZONE data type. // new MetadataMatcher().name("TIME_LTZ_C").type(ColumnType.TIME).precision(0).scale(UNDEFINED_SCALE), // new MetadataMatcher().name("TIME_LTZ_C2").type(ColumnType.TIME).precision(9).scale(UNDEFINED_SCALE), new MetadataMatcher().name("DATETIME_C").type(ColumnType.DATETIME).precision(6).scale(UNDEFINED_SCALE), new MetadataMatcher().name("DATETIME_C2").type(ColumnType.DATETIME).precision(9).scale(UNDEFINED_SCALE), - // TODO: IGNITE-19274 Ignite doesn't support the client's time zone yet. - // new MetadataMatcher().name("TIMESTAMP_C").type(ColumnType.TIMESTAMP).precision(6).scale(UNDEFINED_SCALE), - // new MetadataMatcher().name("TIMESTAMP_C2").type(ColumnType.TIMESTAMP).precision(9).scale(UNDEFINED_SCALE), + new MetadataMatcher().name("TIMESTAMP_C").type(ColumnType.TIMESTAMP).precision(6).scale(UNDEFINED_SCALE), + new MetadataMatcher().name("TIMESTAMP_C2").type(ColumnType.TIMESTAMP).precision(9).scale(UNDEFINED_SCALE), // Interval types // TODO: IGNITE-17373: Ignite doesn't support interval types yet. diff --git a/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl b/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl index 51933242f0..3302091770 100644 --- a/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl +++ b/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl @@ -615,12 +615,13 @@ void AlterZoneOption(List<SqlNode> list) : /** * Parse datetime types: date, time, timestamp. * -* TODO Method doesn't recognize '*_WITH_LOCAL_TIME_ZONE' types and should be removed after IGNITE-19274. +* TODO Method doesn't recognize 'TIME_WITH_LOCAL_TIME_ZONE' type and should be removed after IGNITE-21555. */ SqlTypeNameSpec IgniteDateTimeTypeName() : { int precision = -1; SqlTypeName typeName; + boolean withLocalTimeZone = false; final Span s; } { @@ -638,8 +639,13 @@ SqlTypeNameSpec IgniteDateTimeTypeName() : | <TIMESTAMP> { s = span(); } precision = PrecisionOpt() + withLocalTimeZone = TimeZoneOpt() { - typeName = SqlTypeName.TIMESTAMP; + if (withLocalTimeZone) { + typeName = SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE; + } else { + typeName = SqlTypeName.TIMESTAMP; + } return new SqlBasicTypeNameSpec(typeName, precision, s.end(this)); } } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/SessionBuilderImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/SessionBuilderImpl.java index 6d5ec67356..c4ad326745 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/SessionBuilderImpl.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/SessionBuilderImpl.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.sql.api; +import java.time.ZoneId; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentMap; @@ -63,6 +64,8 @@ public class SessionBuilderImpl implements SessionBuilder { private int pageSize = AbstractSession.DEFAULT_PAGE_SIZE; + private ZoneId timeZoneId; + /** * Session builder constructor. * @@ -167,6 +170,13 @@ public class SessionBuilderImpl implements SessionBuilder { return this; } + @Override + public SessionBuilder timeZoneId(ZoneId timeZoneId) { + this.timeZoneId = timeZoneId; + + return this; + } + /** {@inheritDoc} */ @Override public @Nullable Object property(String name) { @@ -187,6 +197,7 @@ public class SessionBuilderImpl implements SessionBuilder { SqlProperties propsHolder = SqlPropertiesHelper.newBuilder() .set(QueryProperty.QUERY_TIMEOUT, queryTimeout) .set(QueryProperty.DEFAULT_SCHEMA, schema) + .set(QueryProperty.TIME_ZONE_ID, timeZoneId == null ? ZoneId.systemDefault() : timeZoneId) .build(); SessionId sessionId = new SessionId(UUID.randomUUID()); diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/SessionImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/SessionImpl.java index 069524d8b5..c7b2453aa9 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/SessionImpl.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/SessionImpl.java @@ -24,6 +24,7 @@ import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR; import static org.apache.ignite.lang.ErrorGroups.Sql.SESSION_CLOSED_ERR; import it.unimi.dsi.fastutil.longs.LongArrayList; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; @@ -172,6 +173,12 @@ public class SessionImpl implements AbstractSession { return pageSize; } + /** {@inheritDoc} */ + @Override + public ZoneId timeZoneId() { + return props.get(QueryProperty.TIME_ZONE_ID); + } + /** {@inheritDoc} */ @Override public @Nullable Object property(String name) { @@ -213,7 +220,7 @@ public class SessionImpl implements AbstractSession { CompletableFuture<AsyncResultSet<SqlRow>> result; try { - SqlProperties properties = SqlPropertiesHelper.newBuilder() + SqlProperties properties = SqlPropertiesHelper.builderFromProperties(props) .set(QueryProperty.ALLOWED_QUERY_TYPES, SqlQueryType.SINGLE_STMT_TYPES) .build(); @@ -298,7 +305,7 @@ public class SessionImpl implements AbstractSession { } try { - SqlProperties properties = SqlPropertiesHelper.newBuilder() + SqlProperties properties = SqlPropertiesHelper.builderFromProperties(props) .set(QueryProperty.ALLOWED_QUERY_TYPES, EnumSet.of(SqlQueryType.DML)) .build(); @@ -405,10 +412,12 @@ public class SessionImpl implements AbstractSession { return CompletableFuture.failedFuture(sessionIsClosedException()); } + SqlProperties properties = SqlPropertiesHelper.builderFromProperties(props) + .set(QueryProperty.ALLOWED_QUERY_TYPES, SqlQueryType.ALL) + .build(); + CompletableFuture<Void> resFut = new CompletableFuture<>(); try { - SqlProperties properties = SqlPropertiesHelper.emptyProperties(); - CompletableFuture<AsyncSqlCursor<InternalSqlRow>> f = qryProc.queryScriptAsync(properties, transactions, null, query, arguments); diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProperty.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProperty.java index 53d2d085bb..e04b2d9267 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProperty.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProperty.java @@ -19,6 +19,7 @@ package org.apache.ignite.internal.sql.engine; import static org.apache.ignite.internal.sql.engine.property.SqlPropertiesHelper.createPropsByNameMap; +import java.time.ZoneId; import java.util.Map; import java.util.Set; import org.apache.ignite.internal.sql.engine.property.Property; @@ -32,6 +33,7 @@ public final class QueryProperty { public static final Property<Set<SqlQueryType>> ALLOWED_QUERY_TYPES = new Property<>("allowed_query_types", cast(Set.class)); public static final Property<String> DEFAULT_SCHEMA = new Property<>("default_schema", String.class); + public static final Property<ZoneId> TIME_ZONE_ID = new Property<>("time_zone_id", ZoneId.class); private static final Map<String, Property<?>> propsByName = createPropsByNameMap(QueryProperty.class); diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java index 7d9965647b..55c93cf0d1 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java @@ -32,6 +32,7 @@ import static org.apache.ignite.lang.ErrorGroups.Replicator.REPLICA_UNAVAILABLE_ import static org.apache.ignite.lang.ErrorGroups.Sql.EXECUTION_CANCELLED_ERR; import static org.apache.ignite.lang.ErrorGroups.Sql.STMT_VALIDATION_ERR; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -135,6 +136,9 @@ import org.jetbrains.annotations.TestOnly; * Main implementation of {@link QueryProcessor}. */ public class SqlQueryProcessor implements QueryProcessor { + /** Default time zone ID. */ + public static final ZoneId DEFAULT_TIME_ZONE_ID = ZoneId.of("UTC"); + /** The logger. */ private static final IgniteLogger LOG = Loggers.forClass(SqlQueryProcessor.class); @@ -152,6 +156,7 @@ public class SqlQueryProcessor implements QueryProcessor { private static final SqlProperties DEFAULT_PROPERTIES = SqlPropertiesHelper.newBuilder() .set(QueryProperty.DEFAULT_SCHEMA, DEFAULT_SCHEMA_NAME) .set(QueryProperty.ALLOWED_QUERY_TYPES, SqlQueryType.ALL) + .set(QueryProperty.TIME_ZONE_ID, DEFAULT_TIME_ZONE_ID) .build(); private static final CacheFactory CACHE_FACTORY = CaffeineCacheFactory.INSTANCE; @@ -541,6 +546,7 @@ public class SqlQueryProcessor implements QueryProcessor { ) { SqlProperties properties0 = SqlPropertiesHelper.chain(properties, DEFAULT_PROPERTIES); String schemaName = properties0.get(QueryProperty.DEFAULT_SCHEMA); + ZoneId timeZoneId = properties0.get(QueryProperty.TIME_ZONE_ID); QueryCancel queryCancel = new QueryCancel(); @@ -554,7 +560,7 @@ public class SqlQueryProcessor implements QueryProcessor { QueryTransactionWrapper txWrapper = txCtx.getOrStartImplicit(result.queryType()); - return executeParsedStatement(schemaName, result, txWrapper, queryCancel, params, null); + return executeParsedStatement(schemaName, result, txWrapper, queryCancel, timeZoneId, params, null); }); // TODO IGNITE-20078 Improve (or remove) CancellationException handling. @@ -577,6 +583,7 @@ public class SqlQueryProcessor implements QueryProcessor { ) { SqlProperties properties0 = SqlPropertiesHelper.chain(properties, DEFAULT_PROPERTIES); String schemaName = properties0.get(QueryProperty.DEFAULT_SCHEMA); + ZoneId timeZoneId = properties0.get(QueryProperty.TIME_ZONE_ID); CompletableFuture<?> start = new CompletableFuture<>(); @@ -584,7 +591,7 @@ public class SqlQueryProcessor implements QueryProcessor { .thenApply(ignored -> parserService.parseScript(sql)) .thenCompose(parsedResults -> { MultiStatementHandler handler = new MultiStatementHandler( - schemaName, txCtx, parsedResults, params); + schemaName, txCtx, parsedResults, params, timeZoneId); return handler.processNext(); }); @@ -618,6 +625,7 @@ public class SqlQueryProcessor implements QueryProcessor { ParsedResult parsedResult, QueryTransactionWrapper txWrapper, QueryCancel queryCancel, + ZoneId timeZoneId, Object[] params, @Nullable CompletableFuture<AsyncSqlCursor<InternalSqlRow>> nextStatement ) { @@ -631,6 +639,7 @@ public class SqlQueryProcessor implements QueryProcessor { .cancel(queryCancel) .prefetchCallback(callback) .parameters(params) + .timeZoneId(timeZoneId) .build(); return prepareSvc.prepareAsync(parsedResult, ctx) @@ -766,6 +775,7 @@ public class SqlQueryProcessor implements QueryProcessor { } private class MultiStatementHandler { + private final ZoneId timeZoneId; private final String schemaName; private final Queue<ScriptStatement> statements; private final ScriptTransactionContext txCtx; @@ -785,8 +795,10 @@ public class SqlQueryProcessor implements QueryProcessor { String schemaName, QueryTransactionContext txCtx, List<ParsedResult> parsedResults, - Object[] params + Object[] params, + ZoneId timeZoneId ) { + this.timeZoneId = timeZoneId; this.schemaName = schemaName; this.statements = prepareStatementsQueue(parsedResults, params); this.txCtx = new ScriptTransactionContext(txCtx); @@ -867,7 +879,7 @@ public class SqlQueryProcessor implements QueryProcessor { txCtx.registerCursorFuture(parsedResult.queryType(), cursorFuture); - fut = executeParsedStatement(schemaName, parsedResult, txWrapper, new QueryCancel(), params, nextCurFut); + fut = executeParsedStatement(schemaName, parsedResult, txWrapper, new QueryCancel(), timeZoneId, params, nextCurFut); } fut.whenComplete((cursor, ex) -> { diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionContext.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionContext.java index 8e69810b07..bf4a9e1b44 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionContext.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionContext.java @@ -21,6 +21,8 @@ import static org.apache.ignite.internal.sql.engine.util.Commons.FRAMEWORK_CONFI import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR; import java.lang.reflect.Type; +import java.time.Instant; +import java.time.ZoneId; import java.util.List; import java.util.Locale; import java.util.Map; @@ -52,8 +54,6 @@ import org.jetbrains.annotations.Nullable; public class ExecutionContext<RowT> implements DataContext { private static final IgniteLogger LOG = Loggers.forClass(ExecutionContext.class); - private static final TimeZone TIME_ZONE = TimeZone.getDefault(); // TODO DistributedSqlConfiguration#timeZone - /** * TODO: https://issues.apache.org/jira/browse/IGNITE-15276 Support other locales. */ @@ -85,6 +85,8 @@ public class ExecutionContext<RowT> implements DataContext { private final TxAttributes txAttributes; + private final ZoneId timeZoneId; + private SharedState sharedState = new SharedState(); /** @@ -92,9 +94,13 @@ public class ExecutionContext<RowT> implements DataContext { * * @param executor Task executor. * @param qryId Query ID. + * @param localNode Local node. + * @param originatingNodeName Name of the node that initiated the query. * @param description Partitions information. * @param handler Row handler. * @param params Parameters. + * @param txAttributes Transaction attributes. + * @param timeZoneId Session time zone ID. */ @SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType") public ExecutionContext( @@ -105,7 +111,8 @@ public class ExecutionContext<RowT> implements DataContext { FragmentDescription description, RowHandler<RowT> handler, Map<String, Object> params, - TxAttributes txAttributes + TxAttributes txAttributes, + ZoneId timeZoneId ) { this.executor = executor; this.qryId = qryId; @@ -115,14 +122,15 @@ public class ExecutionContext<RowT> implements DataContext { this.localNode = localNode; this.originatingNodeName = originatingNodeName; this.txAttributes = txAttributes; + this.timeZoneId = timeZoneId; expressionFactory = new ExpressionFactoryImpl<>( this, FRAMEWORK_CONFIG.getParserConfig().conformance() ); - long ts = System.currentTimeMillis(); - startTs = ts + TIME_ZONE.getOffset(ts); + Instant nowUtc = Instant.now(); + startTs = nowUtc.plusSeconds(this.timeZoneId.getRules().getOffset(nowUtc).getTotalSeconds()).toEpochMilli(); if (LOG.isTraceEnabled()) { LOG.trace("Context created [qryId={}, fragmentId={}]", qryId, fragmentId()); @@ -238,6 +246,10 @@ public class ExecutionContext<RowT> implements DataContext { return LOCALE; } + if (Variable.TIME_ZONE.camelName.equals(name)) { + return TimeZone.getTimeZone(timeZoneId); + } + if (name.startsWith("?")) { Object val = params.get(name); return val != null ? TypeUtils.toInternal(val, val.getClass()) : null; diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImpl.java index 05e0aa081b..b145ce405b 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImpl.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImpl.java @@ -29,6 +29,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -271,7 +272,7 @@ public class ExecutionServiceImpl<RowT> implements ExecutionService, TopologyEve return queryManager.execute(tx, plan); } - private BaseQueryContext createQueryContext(UUID queryId, int schemaVersion, Object[] params) { + private BaseQueryContext createQueryContext(UUID queryId, int schemaVersion, ZoneId timeZoneId, Object[] params) { return BaseQueryContext.builder() .queryId(queryId) .parameters(params) @@ -280,6 +281,7 @@ public class ExecutionServiceImpl<RowT> implements ExecutionService, TopologyEve .defaultSchema(sqlSchemaManager.schema(schemaVersion)) .build() ) + .timeZoneId(timeZoneId) .build(); } @@ -343,7 +345,8 @@ public class ExecutionServiceImpl<RowT> implements ExecutionService, TopologyEve DUMMY_DESCRIPTION, handler, Commons.parametersMap(ctx.parameters()), - TxAttributes.fromTx(tx) + TxAttributes.fromTx(tx), + ctx.timeZoneId() ); return plan.execute(ectx, tx, tableRegistry, callback); @@ -510,7 +513,7 @@ public class ExecutionServiceImpl<RowT> implements ExecutionService, TopologyEve private DistributedQueryManager getOrCreateQueryManager(String coordinatorNodeName, QueryStartRequest msg) { return queryManagerMap.computeIfAbsent(msg.queryId(), key -> { - BaseQueryContext ctx = createQueryContext(key, msg.schemaVersion(), msg.parameters()); + BaseQueryContext ctx = createQueryContext(key, msg.schemaVersion(), ZoneId.of(msg.timeZoneId()), msg.parameters()); return new DistributedQueryManager(coordinatorNodeName, ctx); }); @@ -712,6 +715,7 @@ public class ExecutionServiceImpl<RowT> implements ExecutionService, TopologyEve .parameters(ctx.parameters()) .txAttributes(txAttributes) .schemaVersion(ctx.schemaVersion()) + .timeZoneId(ctx.timeZoneId().getId()) .build(); return messageService.send(targetNodeName, request); @@ -798,7 +802,8 @@ public class ExecutionServiceImpl<RowT> implements ExecutionService, TopologyEve desc, handler, Commons.parametersMap(ctx.parameters()), - txAttributes + txAttributes, + ctx.timeZoneId() ); } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java index 9c29d4e89c..30c81c1705 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java @@ -32,12 +32,14 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.TimeZone; import java.util.concurrent.ConcurrentMap; import java.util.function.BiPredicate; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import org.apache.calcite.DataContext; +import org.apache.calcite.DataContext.Variable; import org.apache.calcite.adapter.enumerable.EnumUtils; import org.apache.calcite.linq4j.function.Function1; import org.apache.calcite.linq4j.tree.BlockBuilder; @@ -316,6 +318,11 @@ public class ExpressionFactoryImpl<RowT> implements ExpressionFactory<RowT> { RexLiteral literal = values.get(i); Object val = literal.getValueAs(types.get(field)); + // Literal was parsed as UTC timestamp, now we need to adjust it to the client's time zone. + if (val != null && literal.getTypeName() == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE) { + val = IgniteSqlFunctions.subtractTimeZoneOffset((long) val, (TimeZone) ctx.get(Variable.TIME_ZONE.camelName)); + } + rowBuilder.addField(val); } 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 8ae478c785..1b0d6fd210 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 @@ -26,12 +26,12 @@ 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.DateTimeException; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.ResolverStyle; +import java.util.TimeZone; import java.util.UUID; import org.apache.calcite.DataContext; import org.apache.calcite.avatica.util.ByteString; @@ -97,14 +97,6 @@ public class IgniteSqlFunctions { * Otherwise need to fix {@code DateTimeUtils#unixTimestampToString} usage additionally. */ public static long timestampStringToNumeric(String dtStr) { - try { - return timestampStringToNumeric0(dtStr); - } catch (DateTimeException e) { - throw new SqlException(RUNTIME_ERR, e.getMessage()); - } - } - - private static long timestampStringToNumeric0(String dtStr) { dtStr = dtStr.trim(); //"YYYY-MM-dd HH:mm:ss.ninenanos" if (dtStr.length() > 29) { @@ -564,6 +556,14 @@ public class IgniteSqlFunctions { return args1; } + /** Returns the timestamp value minus the offset of the specified timezone. */ + public static Long subtractTimeZoneOffset(long timestamp, TimeZone timeZone) { + // A second offset calculation is required to handle DST transition period correctly. + int offset = timeZone.getOffset(timestamp - timeZone.getOffset(timestamp)); + + return timestamp - offset; + } + 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/RexImpTable.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java index a9ffec1e4f..7f8c472218 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java @@ -420,6 +420,7 @@ import org.apache.calcite.sql.fun.SqlJsonArrayAggAggFunction; import org.apache.calcite.sql.fun.SqlJsonObjectAggAggFunction; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.fun.SqlTrimFunction; +import org.apache.calcite.sql.type.SqlTypeFamily; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.type.SqlTypeUtil; import org.apache.calcite.sql.validate.SqlUserDefinedAggFunction; @@ -3542,6 +3543,7 @@ public class RexImpTable { case DATE: switch (typeName) { case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: trop0 = Expressions.convert_( IgniteExpressions.multiplyExact(trop0, 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 59e7c2bb95..1f8b00138e 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 @@ -355,6 +355,9 @@ public class RexToLixTranslator implements RexVisitor<RexToLixTranslator.Result> switch (sourceType.getSqlTypeName()) { case CHAR: case VARCHAR: + // 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. convert = Expressions.call(IgniteMethod.STRING_TO_TIMESTAMP.method(), operand); break; @@ -400,9 +403,7 @@ public class RexToLixTranslator implements RexVisitor<RexToLixTranslator.Result> case CHAR: case VARCHAR: convert = - Expressions.call( - BuiltInMethod.STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE.method, - operand); + Expressions.call(IgniteMethod.STRING_TO_TIMESTAMP.method(), operand); break; case DATE: convert = @@ -845,6 +846,15 @@ public class RexToLixTranslator implements RexVisitor<RexToLixTranslator.Result> Expressions.constant(type.getPrecision()), Expressions.constant(type.getScale()) ); + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + Object val = literal.getValueAs(Long.class); + + // Literal was parsed as UTC timestamp, now we need to adjust it to the client's time zone. + return Expressions.call( + IgniteMethod.SUBTRACT_TIMEZONE_OFFSET.method(), + Expressions.constant(val, long.class), + Expressions.call(BuiltInMethod.TIME_ZONE.method, DataContext.ROOT) + ); case DATE: case TIME: case TIME_WITH_LOCAL_TIME_ZONE: @@ -855,7 +865,6 @@ public class RexToLixTranslator implements RexVisitor<RexToLixTranslator.Result> javaClass = int.class; break; case TIMESTAMP: - case TIMESTAMP_WITH_LOCAL_TIME_ZONE: case INTERVAL_DAY: case INTERVAL_DAY_HOUR: case INTERVAL_DAY_MINUTE: diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/message/QueryStartRequest.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/message/QueryStartRequest.java index a543f7c452..eb847bdf10 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/message/QueryStartRequest.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/message/QueryStartRequest.java @@ -54,4 +54,9 @@ public interface QueryStartRequest extends ExecutionContextAwareMessage { * Return last schema version, just a stub, need to be removed after IGNITE-20633. */ int schemaVersion(); + + /** + * Session time zone ID. + */ + String timeZoneId(); } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/BaseQueryContext.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/BaseQueryContext.java index e27f2a0c86..3337a58dbd 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/BaseQueryContext.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/BaseQueryContext.java @@ -23,6 +23,7 @@ import static org.apache.ignite.internal.sql.engine.util.Commons.FRAMEWORK_CONFI import com.google.common.collect.Multimap; import java.lang.reflect.Method; +import java.time.ZoneId; import java.util.List; import java.util.Objects; import java.util.Properties; @@ -142,6 +143,8 @@ public final class BaseQueryContext implements Context { private final QueryPrefetchCallback prefetchCallback; + private final ZoneId timeZoneId; + private CalciteCatalogReader catalogReader; /** @@ -152,7 +155,8 @@ public final class BaseQueryContext implements Context { FrameworkConfig cfg, QueryCancel cancel, Object[] parameters, - QueryPrefetchCallback prefetchCallback + QueryPrefetchCallback prefetchCallback, + ZoneId timeZoneId ) { this.parentCtx = Contexts.chain(cfg.getContext()); @@ -163,6 +167,7 @@ public final class BaseQueryContext implements Context { this.cancel = cancel; this.parameters = parameters; this.prefetchCallback = prefetchCallback; + this.timeZoneId = timeZoneId; } public static Builder builder() { @@ -227,6 +232,10 @@ public final class BaseQueryContext implements Context { return cancel; } + public ZoneId timeZoneId() { + return timeZoneId; + } + /** * Query context builder. */ @@ -246,6 +255,8 @@ public final class BaseQueryContext implements Context { private Object[] parameters = ArrayUtils.OBJECT_EMPTY_ARRAY; + private ZoneId timeZoneId; + private QueryPrefetchCallback prefetchCallback; public Builder frameworkConfig(FrameworkConfig frameworkCfg) { @@ -273,8 +284,21 @@ public final class BaseQueryContext implements Context { return this; } + public Builder timeZoneId(ZoneId timeZoneId) { + this.timeZoneId = timeZoneId; + return this; + } + + /** Creates new context. */ public BaseQueryContext build() { - return new BaseQueryContext(Objects.requireNonNull(queryId, "queryId"), frameworkCfg, cancel, parameters, prefetchCallback); + return new BaseQueryContext( + Objects.requireNonNull(queryId, "queryId"), + frameworkCfg, + cancel, + parameters, + prefetchCallback, + timeZoneId + ); } } } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteCustomAssignmentsRules.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteCustomAssignmentsRules.java index e46cebed09..833c897fde 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteCustomAssignmentsRules.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteCustomAssignmentsRules.java @@ -129,6 +129,7 @@ public class IgniteCustomAssignmentsRules implements SqlTypeMappingRule { rule.add(SqlTypeName.DATE); rule.add(SqlTypeName.TIME); rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); rules.add(SqlTypeName.CHAR, rule); rules.add(SqlTypeName.VARCHAR, rule); @@ -141,6 +142,7 @@ public class IgniteCustomAssignmentsRules implements SqlTypeMappingRule { rule.add(SqlTypeName.DATE); rule.addAll(CHAR_TYPES); rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); rules.add(SqlTypeName.DATE, rule); // TIME is assignable from... @@ -148,6 +150,7 @@ public class IgniteCustomAssignmentsRules implements SqlTypeMappingRule { rule.add(SqlTypeName.TIME); rule.addAll(CHAR_TYPES); rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); rules.add(SqlTypeName.TIME, rule); // TIME WITH LOCAL TIME ZONE is assignable from... @@ -157,14 +160,20 @@ public class IgniteCustomAssignmentsRules implements SqlTypeMappingRule { // TIMESTAMP is assignable from ... rule.clear(); rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); rule.addAll(CHAR_TYPES); rule.add(SqlTypeName.TIME); rule.add(SqlTypeName.DATE); rules.add(SqlTypeName.TIMESTAMP, rule); // TIMESTAMP WITH LOCAL TIME ZONE is assignable from... - rules.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, - EnumSet.of(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)); + rule.clear(); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); + rule.add(SqlTypeName.TIMESTAMP); + rule.addAll(CHAR_TYPES); + rule.add(SqlTypeName.TIME); + rule.add(SqlTypeName.DATE); + rules.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, rule); // GEOMETRY is assignable from ... rule.clear(); @@ -233,6 +242,7 @@ public class IgniteCustomAssignmentsRules implements SqlTypeMappingRule { rule.add(SqlTypeName.TIME); rule.add(SqlTypeName.DATE); rule.add(SqlTypeName.TIMESTAMP); + rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE); rules.add(SqlTypeName.ANY, rule); INSTANCE = new IgniteCustomAssignmentsRules(rules.map); 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 1cf0b3705e..5d419abbf7 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 @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Objects; +import java.util.TimeZone; import java.util.UUID; import org.apache.calcite.DataContext; import org.apache.calcite.avatica.util.ByteString; @@ -72,6 +73,9 @@ public enum IgniteMethod { STRING_TO_TIMESTAMP(IgniteSqlFunctions.class, "timestampStringToNumeric", String.class), + /** See {@link IgniteSqlFunctions#subtractTimeZoneOffset(long, TimeZone)}. **/ + SUBTRACT_TIMEZONE_OFFSET(IgniteSqlFunctions.class, "subtractTimeZoneOffset", long.class, TimeZone.class), + /** See {@link SqlParserUtil#intervalToMonths(String, SqlIntervalQualifier)}. */ PARSE_INTERVAL_YEAR_MONTH(SqlParserUtil.class, "intervalToMonths", String.class, SqlIntervalQualifier.class), diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java index 087fff236f..0a3ed82b8a 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java @@ -285,8 +285,7 @@ public class TypeUtils { } else if (storageType == LocalTime.class && val instanceof Integer) { return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(Long.valueOf((Integer) val))); } else if (storageType == LocalDateTime.class && (val instanceof Long)) { - return LocalDateTime.ofEpochSecond(TimeUnit.MILLISECONDS.toSeconds((Long) val), - (int) TimeUnit.MILLISECONDS.toNanos((Long) val % 1000), ZoneOffset.UTC); + return LocalDateTime.ofInstant(Instant.ofEpochMilli((long) val), ZoneOffset.UTC); } else if (storageType == Instant.class && val instanceof Long) { return Instant.ofEpochMilli((long) val); } else if (storageType == Duration.class && val instanceof Long) { diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/api/SessionImplTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/api/SessionImplTest.java index 4709eedd18..be8659ce98 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/api/SessionImplTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/api/SessionImplTest.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.time.ZoneId; import java.util.HashMap; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -61,6 +62,7 @@ import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; import org.apache.ignite.internal.util.AsyncCursor.BatchedResult; import org.apache.ignite.internal.util.IgniteSpinBusyLock; import org.apache.ignite.sql.BatchedArguments; +import org.apache.ignite.sql.Session; import org.apache.ignite.sql.Session.SessionBuilder; import org.apache.ignite.sql.SqlException; import org.apache.ignite.sql.async.AsyncResultSet; @@ -480,8 +482,35 @@ class SessionImplTest extends BaseIgniteAbstractTest { assertThat(session.openedCursors(), empty()); } + @Test + public void localTimeZoneUsedByDefault() { + SessionBuilder sessionBuilder = newSessionBuilder(); + + // Check default value. + { + Session session = sessionBuilder.build(); + + assertEquals(ZoneId.systemDefault(), session.timeZoneId()); + } + + // Check that time zone can be changed. + { + ZoneId utcTz = ZoneId.of("UTC"); + + Session session = sessionBuilder.timeZoneId(utcTz).build(); + + assertEquals(utcTz, session.timeZoneId()); + } + } + private SessionImpl newSession(long idleTimeout) { - SessionBuilder builder = new SessionBuilderImpl( + return (SessionImpl) newSessionBuilder() + .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + private SessionBuilder newSessionBuilder() { + return new SessionBuilderImpl( new IgniteSpinBusyLock(), sessions, queryProcessor, @@ -489,9 +518,5 @@ class SessionImplTest extends BaseIgniteAbstractTest { clock::get, new HashMap<>() ); - - return (SessionImpl) builder - .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) - .build(); } } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java index 67b4c73fa2..6cd336f9c3 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java @@ -63,7 +63,6 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.calcite.jdbc.CalciteSchema; @@ -81,6 +80,7 @@ import org.apache.ignite.internal.sql.engine.NodeLeftException; import org.apache.ignite.internal.sql.engine.QueryCancel; import org.apache.ignite.internal.sql.engine.QueryCancelledException; import org.apache.ignite.internal.sql.engine.QueryPrefetchCallback; +import org.apache.ignite.internal.sql.engine.SqlQueryProcessor; import org.apache.ignite.internal.sql.engine.exec.ExecutionServiceImplTest.TestCluster.TestNode; import org.apache.ignite.internal.sql.engine.exec.ddl.DdlCommandHandler; import org.apache.ignite.internal.sql.engine.exec.mapping.ExecutionTarget; @@ -643,17 +643,6 @@ public class ExecutionServiceImplTest extends BaseIgniteAbstractTest { AtomicReference<AssertionError> errHolder = new AtomicReference<>(); ExecutionService execService = executionServices.get(0); - Function<QueryPrefetchCallback, BaseQueryContext> createCtx = (callback) -> BaseQueryContext.builder() - .queryId(UUID.randomUUID()) - .cancel(new QueryCancel()) - .prefetchCallback(callback) - .frameworkConfig( - Frameworks.newConfigBuilder(FRAMEWORK_CONFIG) - .defaultSchema(wrap(schema)) - .build() - ) - .build(); - QueryPrefetchCallback prefetchListener = new QueryPrefetchCallback() { @Override public void onPrefetchComplete(@Nullable Throwable err) { @@ -664,7 +653,7 @@ public class ExecutionServiceImplTest extends BaseIgniteAbstractTest { assertThat(sql, notNullValue()); - BaseQueryContext ctx = createCtx.apply(queriesQueue.isEmpty() ? null : this); + BaseQueryContext ctx = createContext(queriesQueue.isEmpty() ? null : this); InternalTransaction tx = new NoOpTransaction(nodeNames.get(0)); QueryPlan plan = prepare(sql, ctx); @@ -701,17 +690,7 @@ public class ExecutionServiceImplTest extends BaseIgniteAbstractTest { ExecutionService execService = executionServices.get(0); CompletableFuture<Void> prefetchFut = new CompletableFuture<>(); IgniteInternalException expectedException = new IgniteInternalException(Common.INTERNAL_ERR, "Expected exception"); - - BaseQueryContext ctx = BaseQueryContext.builder() - .queryId(UUID.randomUUID()) - .cancel(new QueryCancel()) - .prefetchCallback(prefetchFut::completeExceptionally) - .frameworkConfig( - Frameworks.newConfigBuilder(FRAMEWORK_CONFIG) - .defaultSchema(wrap(schema)) - .build() - ) - .build(); + BaseQueryContext ctx = createContext(prefetchFut::completeExceptionally); testCluster.node(nodeNames.get(2)).interceptor((nodeName, msg, original) -> { if (msg instanceof QueryStartRequest) { @@ -908,14 +887,20 @@ public class ExecutionServiceImplTest extends BaseIgniteAbstractTest { } private BaseQueryContext createContext() { + return createContext(null); + } + + private BaseQueryContext createContext(@Nullable QueryPrefetchCallback prefetchCallback) { return BaseQueryContext.builder() .queryId(UUID.randomUUID()) .cancel(new QueryCancel()) + .prefetchCallback(prefetchCallback) .frameworkConfig( Frameworks.newConfigBuilder(FRAMEWORK_CONFIG) .defaultSchema(wrap(schema)) .build() ) + .timeZoneId(SqlQueryProcessor.DEFAULT_TIME_ZONE_ID) .build(); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/RuntimeSortedIndexTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/RuntimeSortedIndexTest.java index 7ba54ecf29..d9ad4dc699 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/RuntimeSortedIndexTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/RuntimeSortedIndexTest.java @@ -35,6 +35,7 @@ import org.apache.calcite.rel.type.RelDataTypeField; import org.apache.calcite.util.ImmutableIntList; import org.apache.calcite.util.Pair; import org.apache.ignite.internal.network.ClusterNodeImpl; +import org.apache.ignite.internal.sql.engine.SqlQueryProcessor; import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler; import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory; import org.apache.ignite.internal.sql.engine.util.Commons; @@ -120,7 +121,8 @@ public class RuntimeSortedIndexTest extends IgniteAbstractTest { null, ArrayRowHandler.INSTANCE, Map.of(), - null + null, + SqlQueryProcessor.DEFAULT_TIME_ZONE_ID ), RelCollations.of(ImmutableIntList.copyOf(idxCols)), (o1, o2) -> { 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 596b510f0d..72d18a2cfd 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 @@ -25,6 +25,10 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.TimeZone; import java.util.function.Supplier; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem; @@ -32,6 +36,7 @@ import org.apache.ignite.lang.ErrorGroups.Sql; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; /** * Sql functions test. @@ -481,4 +486,33 @@ public class IgniteSqlFunctionsTest { public void testTruncate2LongType(long input, int scale, long result) { assertEquals(result, IgniteSqlFunctions.struncate(input, scale)); } + + @ParameterizedTest + @ValueSource(strings = { + "2023-10-29 02:01:01", + "2023-10-29 03:01:01", + "2023-10-29 04:01:01", + "2023-10-29 05:01:01", + "2024-03-31 02:01:01", + "2024-03-31 03:01:01", + "2024-03-31 04:01:01", + "2024-03-31 05:01:01", + }) + public void testSubtractTimeZoneOffset(String input) throws ParseException { + TimeZone cyprusTz = TimeZone.getTimeZone("Asia/Nicosia"); + TimeZone utcTz = TimeZone.getTimeZone("UTC"); + + SimpleDateFormat dateFormatTz = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + dateFormatTz.setTimeZone(cyprusTz); + + SimpleDateFormat dateFormatUtc = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + dateFormatUtc.setTimeZone(utcTz); + + long expMillis = dateFormatTz.parse(input).getTime(); + long utcMillis = dateFormatUtc.parse(input).getTime(); + + long actualTs = IgniteSqlFunctions.subtractTimeZoneOffset(utcMillis, cyprusTz); + + assertEquals(Instant.ofEpochMilli(expMillis), Instant.ofEpochMilli(actualTs)); + } } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractExecutionTest.java index 7ea0504463..2fae8e9bfb 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractExecutionTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractExecutionTest.java @@ -44,6 +44,7 @@ import org.apache.ignite.internal.network.ClusterNodeImpl; import org.apache.ignite.internal.schema.BinaryRowConverter; import org.apache.ignite.internal.schema.BinaryTuple; import org.apache.ignite.internal.schema.BinaryTupleSchema; +import org.apache.ignite.internal.sql.engine.SqlQueryProcessor; import org.apache.ignite.internal.sql.engine.exec.ExecutionContext; import org.apache.ignite.internal.sql.engine.exec.QueryTaskExecutorImpl; import org.apache.ignite.internal.sql.engine.exec.RowHandler; @@ -116,7 +117,8 @@ public abstract class AbstractExecutionTest<T> extends IgniteAbstractTest { fragmentDesc, rowHandler(), Map.of(), - TxAttributes.fromTx(new NoOpTransaction("fake-test-node")) + TxAttributes.fromTx(new NoOpTransaction("fake-test-node")), + SqlQueryProcessor.DEFAULT_TIME_ZONE_ID ); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/MergeJoinExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/MergeJoinExecutionTest.java index fe45def647..38d5e52a0a 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/MergeJoinExecutionTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/MergeJoinExecutionTest.java @@ -40,6 +40,7 @@ import org.apache.calcite.rel.core.JoinRelType; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.sql.validate.SqlConformanceEnum; import org.apache.calcite.util.ImmutableBitSet; +import org.apache.ignite.internal.sql.engine.SqlQueryProcessor; import org.apache.ignite.internal.sql.engine.exec.ExecutionContext; import org.apache.ignite.internal.sql.engine.exec.RowHandler; import org.apache.ignite.internal.sql.engine.exec.exp.ExpressionFactoryImpl; @@ -484,8 +485,8 @@ public class MergeJoinExecutionTest extends AbstractExecutionTest<Object[]> { ScanNode<Object[]> rightNode = new ScanNode<>(ctx, Arrays.asList(right)); ExecutionContext<Object[]> ectx = - new ExecutionContext<>(null, null, null, - null, null, ArrayRowHandler.INSTANCE, null, null); + new ExecutionContext<>(null, null, null, null, null, + ArrayRowHandler.INSTANCE, null, null, SqlQueryProcessor.DEFAULT_TIME_ZONE_ID); ExpressionFactoryImpl<Object[]> expFactory = new ExpressionFactoryImpl<>(ectx, SqlConformanceEnum.DEFAULT); diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java index bb5cf902a8..040f70f24e 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java @@ -64,6 +64,7 @@ import org.apache.ignite.internal.cluster.management.topology.api.LogicalNode; import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot; import org.apache.ignite.internal.hlc.HybridClockImpl; import org.apache.ignite.internal.metrics.MetricManager; +import org.apache.ignite.internal.sql.engine.SqlQueryProcessor; import org.apache.ignite.internal.sql.engine.exec.ExecutableTable; import org.apache.ignite.internal.sql.engine.exec.ExecutableTableRegistry; import org.apache.ignite.internal.sql.engine.exec.ExecutionContext; @@ -507,7 +508,8 @@ public class TestBuilders { description, ArrayRowHandler.INSTANCE, Map.of(), - TxAttributes.fromTx(new NoOpTransaction(node.name())) + TxAttributes.fromTx(new NoOpTransaction(node.name())), + SqlQueryProcessor.DEFAULT_TIME_ZONE_ID ); } } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestNode.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestNode.java index 44d6e0be4d..0aca997180 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestNode.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestNode.java @@ -32,6 +32,7 @@ import org.apache.ignite.internal.network.ClusterService; import org.apache.ignite.internal.network.MessagingService; import org.apache.ignite.internal.sql.engine.InternalSqlRow; import org.apache.ignite.internal.sql.engine.QueryCancel; +import org.apache.ignite.internal.sql.engine.SqlQueryProcessor; import org.apache.ignite.internal.sql.engine.SqlQueryType; import org.apache.ignite.internal.sql.engine.exec.ExchangeService; import org.apache.ignite.internal.sql.engine.exec.ExchangeServiceImpl; @@ -250,6 +251,7 @@ public class TestNode implements LifecycleAware { .defaultSchema(schemaManager.schema(Long.MAX_VALUE).getSubSchema(DEFAULT_SCHEMA_NAME)) .build() ) + .timeZoneId(SqlQueryProcessor.DEFAULT_TIME_ZONE_ID) .build(); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/CastResolutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/CastResolutionTest.java index 0b3aca2358..72195e1788 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/CastResolutionTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/CastResolutionTest.java @@ -120,6 +120,7 @@ public class CastResolutionTest extends AbstractPlannerTest { CHAR_AND_TS.addAll(CHAR_NAMES); CHAR_AND_TS.add(SqlTypeName.TIMESTAMP.getName()); + CHAR_AND_TS.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE.getName()); CHAR_AND_DT.addAll(DT_NAMES); CHAR_AND_DT.addAll(CHAR_NAMES); @@ -150,30 +151,28 @@ public class CastResolutionTest extends AbstractPlannerTest { String from = makeUsableIntervalFromType(fromInitial); for (String toType : toTypes) { - toType = makeUsableIntervalToType(toType); - - // TODO: https://issues.apache.org/jira/browse/IGNITE-19274 - if (toType.contains("LOCAL TIME")) { + // TODO: https://issues.apache.org/jira/browse/IGNITE-21555 + if (toType.equals(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE.getName())) { continue; } + toType = makeSpaceName(toType); + testItems.add(checkStatement().sql(format(template, from, toType)).ok(false)); } - testItems.add(checkStatement().sql(format(template, from, makeUsableIntervalToType(fromInitial))).ok()); + testItems.add(checkStatement().sql(format(template, from, makeSpaceName(fromInitial))).ok()); - testItems.add(checkStatement().sql(format(INTERVAL_TEMPLATE, "NULL", makeUsableIntervalToType(fromInitial))).ok()); + testItems.add(checkStatement().sql(format(INTERVAL_TEMPLATE, "NULL", makeSpaceName(fromInitial))).ok()); String finalFrom = from; Set<String> deprecatedCastTypes = allTypes.stream().filter(t -> !toTypes.contains(t) && !t.equals(finalFrom)) .collect(Collectors.toSet()); for (String toType : deprecatedCastTypes) { - boolean isInterval = toType.toLowerCase().contains("interval"); - - toType = isInterval ? makeUsableIntervalToType(toType) : toType; - - testItems.add(checkStatement().sql(format(template, from, toType)).fails(CAST_ERROR_MESSAGE)); + testItems.add( + checkStatement().sql(format(template, from, makeSpaceName(toType))).fails(CAST_ERROR_MESSAGE) + ); } } @@ -299,12 +298,16 @@ public class CastResolutionTest extends AbstractPlannerTest { } } - private static String makeUsableIntervalToType(String typeName) { + private static String makeSpaceName(String typeName) { return typeName.replace("_", " "); } private static String makeUsableIntervalFromType(String typeName) { - return typeName.replace("_", " 1 "); + if (typeName.toLowerCase().contains("interval")) { + return typeName.replace("_", " 1 "); + } + + return makeSpaceName(typeName); } private enum CastMatrix { @@ -342,15 +345,17 @@ public class CastResolutionTest extends AbstractPlannerTest { TIME(SqlTypeName.TIME.getName(), CHAR_AND_TS), + // TODO: https://issues.apache.org/jira/browse/IGNITE-21555 + //TIME_TZ(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE.getName(), CHAR_AND_DT); + TIMESTAMP(SqlTypeName.TIMESTAMP.getName(), CHAR_AND_DT), + TIMESTAMP_TZ(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE.getName(), CHAR_AND_DT), + INTERVAL_YEAR(SqlTypeName.INTERVAL_YEAR.getName(), CHAR_EXACT_AND_YM_INTERVAL), INTERVAL_HOUR(SqlTypeName.INTERVAL_HOUR.getName(), CHAR_EXACT_AND_DAY_INTERVAL); - // TODO: https://issues.apache.org/jira/browse/IGNITE-19274 - //TIMESTAMP_TS(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE.getName(), charAndDt); - private String from; private Set<String> toTypes; diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java index 8c07f714ab..54cdbc94de 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java @@ -31,7 +31,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.calcite.schema.ColumnStrategy; import org.apache.calcite.sql.SqlBasicCall; @@ -631,18 +630,14 @@ public class SqlDdlParserTest extends AbstractDdlParserTest { /** * Ensures that the user cannot use the TIME_WITH_LOCAL_TIME_ZONE and TIMESTAMP_WITH_LOCAL_TIME_ZONE types for table columns. */ - // TODO: Remove after https://issues.apache.org/jira/browse/IGNITE-19274 is implemented. + // TODO: Remove after https://issues.apache.org/jira/browse/IGNITE-21555 is implemented. @Test - public void timestampWithLocalTimeZoneIsNotSupported() { - Consumer<String> checker = (query) -> { - assertThrowsSqlException( - Sql.STMT_PARSE_ERR, - "Encountered \"WITH\"", - () -> parse(query)); - }; - - checker.accept("CREATE TABLE test (ts TIMESTAMP WITH LOCAL TIME ZONE)"); - checker.accept("CREATE TABLE test (ts TIME WITH LOCAL TIME ZONE)"); + public void timeWithLocalTimeZoneIsNotSupported() { + assertThrowsSqlException( + Sql.STMT_PARSE_ERR, + "Encountered \"WITH\"", + () -> parse("CREATE TABLE test (ts TIME WITH LOCAL TIME ZONE)") + ); } private IgniteSqlCreateTable parseCreateTable(String stmt) { diff --git a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java index 01908e1cec..195710df0e 100644 --- a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java +++ b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.fail; import java.lang.reflect.Array; import java.lang.reflect.Type; +import java.time.ZoneId; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; @@ -297,6 +298,8 @@ public interface QueryChecker { QueryChecker withParam(Object param); + QueryChecker withTimeZoneId(ZoneId timeZoneId); + QueryChecker disableRules(String... rules); QueryChecker returns(Object... res); diff --git a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java index 5afdfae3ce..b226145aa0 100644 --- a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java +++ b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; import java.lang.reflect.Type; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -47,6 +48,7 @@ import org.apache.ignite.internal.sql.engine.AsyncSqlCursor; import org.apache.ignite.internal.sql.engine.InternalSqlRow; import org.apache.ignite.internal.sql.engine.QueryProcessor; import org.apache.ignite.internal.sql.engine.QueryProperty; +import org.apache.ignite.internal.sql.engine.SqlQueryProcessor; import org.apache.ignite.internal.sql.engine.SqlQueryType; import org.apache.ignite.internal.sql.engine.hint.IgniteHint; import org.apache.ignite.internal.sql.engine.prepare.QueryMetadata; @@ -67,10 +69,6 @@ import org.jetbrains.annotations.Nullable; abstract class QueryCheckerImpl implements QueryChecker { private static final IgniteLogger LOG = Loggers.forClass(QueryCheckerImpl.class); - private static final SqlProperties PROPERTIES = SqlPropertiesHelper.newBuilder() - .set(QueryProperty.ALLOWED_QUERY_TYPES, SqlQueryType.SINGLE_STMT_TYPES) - .build(); - private final QueryTemplate queryTemplate; private final ArrayList<Matcher<String>> planMatchers = new ArrayList<>(); @@ -85,6 +83,8 @@ abstract class QueryCheckerImpl implements QueryChecker { private Object[] params = OBJECT_EMPTY_ARRAY; + private ZoneId timeZoneId = SqlQueryProcessor.DEFAULT_TIME_ZONE_ID; + private final @Nullable InternalTransaction tx; /** @@ -138,6 +138,19 @@ abstract class QueryCheckerImpl implements QueryChecker { return this.withParams(param); } + /** + * Set client time zone for query. + * + * @param zoneId Zone ID. + * @return This. + */ + @Override + public QueryChecker withTimeZoneId(ZoneId zoneId) { + this.timeZoneId = zoneId; + + return this; + } + /** * Disables rules. * @@ -283,14 +296,18 @@ abstract class QueryCheckerImpl implements QueryChecker { // Check plan. QueryProcessor qryProc = getEngine(); + SqlProperties properties = SqlPropertiesHelper.newBuilder() + .set(QueryProperty.ALLOWED_QUERY_TYPES, SqlQueryType.SINGLE_STMT_TYPES) + .set(QueryProperty.TIME_ZONE_ID, timeZoneId) + .build(); + String qry = queryTemplate.createQuery(); LOG.info("Executing query: {}", qry); if (!CollectionUtils.nullOrEmpty(planMatchers)) { - CompletableFuture<AsyncSqlCursor<InternalSqlRow>> explainCursors = qryProc.querySingleAsync( - PROPERTIES, transactions(), tx, "EXPLAIN PLAN FOR " + qry, params); + properties, transactions(), tx, "EXPLAIN PLAN FOR " + qry, params); AsyncSqlCursor<InternalSqlRow> explainCursor = await(explainCursors); List<InternalSqlRow> explainRes = getAllFromCursor(explainCursor); @@ -305,7 +322,7 @@ abstract class QueryCheckerImpl implements QueryChecker { // Check column metadata only. if (resultChecker == null && metadataMatchers != null) { - QueryMetadata queryMetadata = await(qryProc.prepareSingleAsync(PROPERTIES, tx, qry, params)); + QueryMetadata queryMetadata = await(qryProc.prepareSingleAsync(properties, tx, qry, params)); assertNotNull(queryMetadata); @@ -316,7 +333,7 @@ abstract class QueryCheckerImpl implements QueryChecker { // Check result. CompletableFuture<AsyncSqlCursor<InternalSqlRow>> cursors = - qryProc.querySingleAsync(PROPERTIES, transactions(), tx, qry, params); + qryProc.querySingleAsync(properties, transactions(), tx, qry, params); AsyncSqlCursor<InternalSqlRow> cur = await(cursors);