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 c47c746b69 IGNITE-19274 Sql. Jdbc client time zone propagation (#3558)
c47c746b69 is described below

commit c47c746b699589d3d58065846c3cd820ef72467b
Author: Pavel Pereslegin <[email protected]>
AuthorDate: Wed Apr 10 12:15:50 2024 +0300

    IGNITE-19274 Sql. Jdbc client time zone propagation (#3558)
---
 .../internal/jdbc/proto/JdbcQueryEventHandler.java |   5 +-
 .../client/handler/JdbcQueryEventHandlerImpl.java  |  23 ++-
 .../requests/jdbc/ClientJdbcConnectRequest.java    |   5 +-
 .../handler/JdbcQueryEventHandlerImplTest.java     |   5 +-
 .../apache/ignite/jdbc/ItJdbcBatchSelfTest.java    |   7 +-
 .../ignite/jdbc/ItJdbcClientTimeZoneTest.java      | 228 +++++++++++++++++++++
 .../ignite/internal/jdbc/ConnectionProperties.java |  15 ++
 .../internal/jdbc/ConnectionPropertiesImpl.java    |  67 +++++-
 .../internal/jdbc/JdbcClientQueryEventHandler.java |   7 +-
 .../ignite/internal/jdbc/JdbcConnection.java       |  62 +++---
 .../internal/jdbc/PreparedStatementParamsTest.java |  10 +-
 .../integrationTest/sql/types/time/test_time.test  |   4 +-
 .../sql/types/time/time_parsing.test               |   6 +-
 .../sql/types/timestamp/test_timestamp.test        |   4 +-
 .../sql/engine/exec/exp/IgniteSqlFunctions.java    |  70 +++++++
 .../sql/engine/exec/exp/RexToLixTranslator.java    |   6 +-
 .../internal/sql/engine/util/IgniteMethod.java     |   7 +-
 .../engine/exec/exp/IgniteSqlFunctionsTest.java    |  17 ++
 18 files changed, 473 insertions(+), 75 deletions(-)

diff --git 
a/modules/client-common/src/main/java/org/apache/ignite/internal/jdbc/proto/JdbcQueryEventHandler.java
 
b/modules/client-common/src/main/java/org/apache/ignite/internal/jdbc/proto/JdbcQueryEventHandler.java
index 07f1c9186f..0688704ce9 100644
--- 
a/modules/client-common/src/main/java/org/apache/ignite/internal/jdbc/proto/JdbcQueryEventHandler.java
+++ 
b/modules/client-common/src/main/java/org/apache/ignite/internal/jdbc/proto/JdbcQueryEventHandler.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.jdbc.proto;
 
 import java.sql.Connection;
+import java.time.ZoneId;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.jdbc.proto.event.JdbcBatchExecuteRequest;
 import org.apache.ignite.internal.jdbc.proto.event.JdbcBatchExecuteResult;
@@ -42,9 +43,11 @@ public interface JdbcQueryEventHandler {
     /**
      * Create connection context on a server and returns connection identity.
      *
+     * @param timeZoneId Client time zone ID.
+     *
      * @return A future representing result of the operation.
      */
-    CompletableFuture<JdbcConnectResult> connect();
+    CompletableFuture<JdbcConnectResult> connect(ZoneId timeZoneId);
 
     /**
      * {@link JdbcQueryExecuteRequest} command handler.
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
index 5c6a8335ff..c11c4e4844 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
@@ -26,6 +26,7 @@ import static 
org.apache.ignite.lang.ErrorGroups.Client.CONNECTION_ERR;
 
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import java.sql.Statement;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -121,10 +122,11 @@ public class JdbcQueryEventHandlerImpl implements 
JdbcQueryEventHandler {
 
     /** {@inheritDoc} */
     @Override
-    public CompletableFuture<JdbcConnectResult> connect() {
+    public CompletableFuture<JdbcConnectResult> connect(ZoneId timeZoneId) {
         try {
             JdbcConnectionContext connectionContext = new 
JdbcConnectionContext(
-                    igniteTransactions
+                    igniteTransactions,
+                    timeZoneId
             );
 
             long connectionId = resources.put(new ClientResource(
@@ -157,7 +159,7 @@ public class JdbcQueryEventHandlerImpl implements 
JdbcQueryEventHandler {
         }
 
         InternalTransaction tx = req.autoCommit() ? null : 
connectionContext.getOrStartTransaction();
-        SqlProperties properties = createProperties(req.getStmtType(), 
req.multiStatement());
+        SqlProperties properties = createProperties(req.getStmtType(), 
req.multiStatement(), connectionContext.timeZoneId());
 
         CompletableFuture<AsyncSqlCursor<InternalSqlRow>> result = 
processor.queryAsync(
                 properties,
@@ -177,7 +179,7 @@ public class JdbcQueryEventHandlerImpl implements 
JdbcQueryEventHandler {
                 });
     }
 
-    private static SqlProperties createProperties(JdbcStatementType stmtType, 
boolean multiStatement) {
+    private static SqlProperties createProperties(JdbcStatementType stmtType, 
boolean multiStatement, ZoneId timeZoneId) {
         Set<SqlQueryType> allowedTypes;
 
         switch (stmtType) {
@@ -196,6 +198,7 @@ public class JdbcQueryEventHandlerImpl implements 
JdbcQueryEventHandler {
 
         return SqlPropertiesHelper.newBuilder()
                 .set(QueryProperty.ALLOWED_QUERY_TYPES, allowedTypes)
+                .set(QueryProperty.TIME_ZONE_ID, timeZoneId)
                 .build();
     }
 
@@ -275,7 +278,7 @@ public class JdbcQueryEventHandlerImpl implements 
JdbcQueryEventHandler {
             return CompletableFuture.failedFuture(new 
IgniteInternalException(CONNECTION_ERR, "Connection is closed"));
         }
 
-        SqlProperties properties = 
createProperties(JdbcStatementType.UPDATE_STATEMENT_TYPE, false);
+        SqlProperties properties = 
createProperties(JdbcStatementType.UPDATE_STATEMENT_TYPE, false, 
context.timeZoneId());
 
         CompletableFuture<AsyncSqlCursor<InternalSqlRow>> result = 
processor.queryAsync(
                 properties,
@@ -458,12 +461,20 @@ public class JdbcQueryEventHandlerImpl implements 
JdbcQueryEventHandler {
 
         private final IgniteTransactions igniteTransactions;
 
+        private final ZoneId timeZoneId;
+
         private @Nullable InternalTransaction tx;
 
         JdbcConnectionContext(
-                IgniteTransactions igniteTransactions
+                IgniteTransactions igniteTransactions,
+                ZoneId timeZoneId
         ) {
             this.igniteTransactions = igniteTransactions;
+            this.timeZoneId = timeZoneId;
+        }
+
+        ZoneId timeZoneId() {
+            return timeZoneId;
         }
 
         /**
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/jdbc/ClientJdbcConnectRequest.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/jdbc/ClientJdbcConnectRequest.java
index 57ef1b086c..30e6ebe3bd 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/jdbc/ClientJdbcConnectRequest.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/jdbc/ClientJdbcConnectRequest.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.client.handler.requests.jdbc;
 
+import java.time.ZoneId;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.client.proto.ClientMessagePacker;
 import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
@@ -39,6 +40,8 @@ public class ClientJdbcConnectRequest {
             ClientMessagePacker out,
             JdbcQueryEventHandler handler
     ) {
-        return handler.connect().thenAccept(res -> res.writeBinary(out));
+        String timeZoneIdString = in.unpackString();
+
+        return handler.connect(ZoneId.of(timeZoneIdString)).thenAccept(res -> 
res.writeBinary(out));
     }
 }
diff --git 
a/modules/client-handler/src/test/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImplTest.java
 
b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImplTest.java
index 1ec4f6f40a..cc2867310b 100644
--- 
a/modules/client-handler/src/test/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImplTest.java
+++ 
b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImplTest.java
@@ -33,6 +33,7 @@ import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import java.time.ZoneId;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
@@ -102,7 +103,7 @@ class JdbcQueryEventHandlerImplTest extends 
BaseIgniteAbstractTest {
     void connectOnStoppingNode() {
         resourceRegistry.close();
 
-        JdbcConnectResult result = await(eventHandler.connect());
+        JdbcConnectResult result = 
await(eventHandler.connect(ZoneId.systemDefault()));
 
         assertThat(result, notNullValue());
         assertThat(result.status(), is(STATUS_FAILED));
@@ -198,7 +199,7 @@ class JdbcQueryEventHandlerImplTest extends 
BaseIgniteAbstractTest {
     }
 
     private long acquireConnectionId() {
-        JdbcConnectResult result = await(eventHandler.connect());
+        JdbcConnectResult result = 
await(eventHandler.connect(ZoneId.systemDefault()));
 
         assertThat(result, notNullValue());
         assertThat(result.status(), is(STATUS_SUCCESS));
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcBatchSelfTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcBatchSelfTest.java
index 305809f0d3..e531053e40 100644
--- 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcBatchSelfTest.java
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcBatchSelfTest.java
@@ -403,11 +403,12 @@ public class ItJdbcBatchSelfTest extends 
AbstractJdbcSelfTest {
                 + "tt_date date, "
                 + "tt_time time, "
                 + "tt_timestamp timestamp, "
+                + "tt_timestamp_tz timestamp with local time zone, "
                 + "PRIMARY KEY (tt_id));");
 
         PreparedStatement prepStmt = conn.prepareStatement(
-                "INSERT INTO timetypes(tt_id, tt_date, tt_time, tt_timestamp)"
-                        + " VALUES (?, ?, ?, ?)");
+                "INSERT INTO timetypes(tt_id, tt_date, tt_time, tt_timestamp, 
tt_timestamp_tz)"
+                        + " VALUES (?, ?, ?, ?, ?)");
 
         Date date = Date.valueOf(LocalDate.now());
         Time time = Time.valueOf(LocalTime.now());
@@ -417,6 +418,7 @@ public class ItJdbcBatchSelfTest extends 
AbstractJdbcSelfTest {
         prepStmt.setLong(idx++, 1);
         prepStmt.setDate(idx++, date);
         prepStmt.setTime(idx++, time);
+        prepStmt.setTimestamp(idx++, ts);
         prepStmt.setTimestamp(idx, ts);
         prepStmt.addBatch();
 
@@ -428,6 +430,7 @@ public class ItJdbcBatchSelfTest extends 
AbstractJdbcSelfTest {
         assertEquals(date, res.getDate(2));
         assertEquals(time, res.getTime(3));
         assertEquals(ts, res.getTimestamp(4));
+        assertEquals(ts, res.getTimestamp(5));
 
         stmt0.execute("DROP TABLE timetypes");
         stmt0.close();
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcClientTimeZoneTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcClientTimeZoneTest.java
new file mode 100644
index 0000000000..17595cb964
--- /dev/null
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcClientTimeZoneTest.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.jdbc;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.zone.ZoneRulesException;
+import java.util.Objects;
+import java.util.TimeZone;
+import org.apache.ignite.internal.jdbc.ConnectionProperties;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test checks the client time zone propagation from the jdbc client to the 
server node.
+ */
+@SuppressWarnings("CallToDriverManagerGetConnection")
+public class ItJdbcClientTimeZoneTest extends AbstractJdbcSelfTest {
+    private static final String TIMESTAMP_STR = "1970-01-01 00:00:00";
+
+    private ZoneId origin;
+
+    @BeforeAll
+    static void createTable() throws SQLException {
+        try (Statement stmt = conn.createStatement()) {
+            stmt.execute("CREATE TABLE test(id INT PRIMARY KEY, ts TIMESTAMP, 
ts_tz TIMESTAMP WITH LOCAL TIME ZONE)");
+        }
+    }
+
+    @BeforeEach
+    void saveTimeZoneAndClearTable() throws SQLException {
+        origin = TimeZone.getDefault().toZoneId();
+
+        stmt.execute("DELETE FROM test");
+    }
+
+    @AfterEach
+    void restoreTimezone() {
+        ZoneId current = TimeZone.getDefault().toZoneId();
+
+        if (!Objects.equals(origin, current)) {
+            TimeZone.setDefault(TimeZone.getTimeZone(origin));
+        }
+    }
+
+    /** Ensures that the default JVM time zone is passed to the server. */
+    @Test
+    public void jvmTimeZonePassedToServer() throws SQLException {
+        ZoneId serverTimezone = TimeZone.getTimeZone("GMT+1").toZoneId();
+
+        // Client time zone.
+        TimeZone.setDefault(TimeZone.getTimeZone("GMT+02:00"));
+
+        withNewConnection(URL, stmt -> {
+            // Set server timezone.
+            TimeZone.setDefault(TimeZone.getTimeZone(serverTimezone));
+
+            stmt.executeUpdate(format("INSERT INTO test VALUES(0, '{}', 
'{}')", TIMESTAMP_STR, TIMESTAMP_STR));
+
+            validateSingleRow("SELECT ts::VARCHAR, ts_tz::VARCHAR FROM test", 
stmt,
+                    "1970-01-01 00:00:00", "1970-01-01 00:00:00 GMT+02:00");
+        });
+
+        TimeZone.setDefault(TimeZone.getTimeZone("GMT+03:00"));
+
+        withNewConnection(URL, stmt -> {
+            // Set server timezone.
+            TimeZone.setDefault(TimeZone.getTimeZone(serverTimezone));
+
+            validateSingleRow("SELECT ts::VARCHAR, ts_tz::VARCHAR FROM test", 
stmt,
+                    "1970-01-01 00:00:00", "1970-01-01 01:00:00 GMT+03:00");
+        });
+    }
+
+    /**
+     * Ensures that session time zone can be changed using
+     * connection property {@link 
ConnectionProperties#setConnectionTimeZone(ZoneId)}.
+     */
+    @Test
+    public void timeZoneCanBeSetUsingProperty() throws SQLException {
+        String originTimeZone = TimeZone.getDefault().getID();
+
+        {
+            String timeZone = "GMT+02:00";
+
+            withNewConnection(URL + "?connectionTimeZone=" + timeZone, stmt -> 
{
+                stmt.executeUpdate(format("INSERT INTO test VALUES(0, '{}', 
'{}')", TIMESTAMP_STR, TIMESTAMP_STR));
+
+                validateSingleRow("SELECT ts::VARCHAR, ts_tz::VARCHAR FROM 
test", stmt,
+                        "1970-01-01 00:00:00", "1970-01-01 00:00:00 " + 
timeZone);
+            });
+        }
+
+        {
+            String timeZone = "GMT+03:00";
+
+            withNewConnection(URL + "?connectionTimeZone=" + timeZone, stmt -> 
{
+                validateSingleRow("SELECT ts::VARCHAR, ts_tz::VARCHAR FROM 
test", stmt,
+                        "1970-01-01 00:00:00", "1970-01-01 01:00:00 " + 
timeZone);
+            });
+        }
+
+        {
+            String timeZone = "invalid/timezone";
+
+            SQLException ex = assertThrows(SQLException.class,
+                    () -> DriverManager.getConnection(URL + 
"?connectionTimeZone=" + timeZone));
+
+            assertThat(ex.getCause(), instanceOf(ZoneRulesException.class));
+        }
+
+        assertEquals(TimeZone.getDefault().getID(), originTimeZone);
+    }
+
+    /** Ensures that the value passed using a dynamic parameter respects the 
client's time zone. */
+    @Test
+    public void dynamicParamRespectsTimeZone() throws SQLException {
+        // Client time zone.
+        TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
+
+        Timestamp ts = timestamp("1970-01-01T00:00:00");
+
+        // Session time zone is "GMT+1".
+        try (Connection conn = DriverManager.getConnection(URL + 
"?connectionTimeZone=GMT+1")) {
+            try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO 
test VALUES(?, ?, ?)")) {
+                stmt.setInt(1, 1);
+                stmt.setTimestamp(2, ts);
+                // The UTC value must be adjusted according to
+                // session time zone and must be "1969-12-31 23:00:00 UTC".
+                stmt.setTimestamp(3, ts);
+
+                stmt.executeUpdate();
+            }
+
+            try (Statement stmt = conn.createStatement()) {
+                try (ResultSet rs = stmt.executeQuery("SELECT ts, ts_tz FROM 
test where id=1")) {
+                    assertTrue(rs.next());
+
+                    // Session time zone was "GMT+1".
+                    // Client time zone is "GMT".
+                    {
+                        assertEquals(timestamp("1970-01-01T00:00:00"), 
rs.getTimestamp(1));
+                        // Since client time zone is GMT, timestamp will 
contain original UTC value from store.
+                        assertEquals(timestamp("1969-12-31T23:00:00"), 
rs.getTimestamp(2));
+                    }
+
+                    // Session and client time zone are same ("GMT+1").
+                    {
+                        TimeZone.setDefault(TimeZone.getTimeZone("GMT+1"));
+
+                        assertEquals(timestamp("1970-01-01T00:00:00"), 
rs.getTimestamp(1));
+                        assertEquals(timestamp("1970-01-01T00:00:00"), 
rs.getTimestamp(2));
+                    }
+
+                    // Session time zone was "GMT+1".
+                    // Client time zone is "GMT+2".
+                    {
+                        TimeZone.setDefault(TimeZone.getTimeZone("GMT+2"));
+
+                        assertEquals(timestamp("1970-01-01T00:00:00"), 
rs.getTimestamp(1));
+                        assertEquals(timestamp("1970-01-01T01:00:00"), 
rs.getTimestamp(2));
+                    }
+                }
+            }
+        }
+    }
+
+    private static Timestamp timestamp(String dateTimeString) {
+        return Timestamp.valueOf(LocalDateTime.parse(dateTimeString));
+    }
+
+    private static void withNewConnection(String url, ConsumerX<Statement> 
consumer) throws SQLException {
+        try (Connection conn = DriverManager.getConnection(url)) {
+            try (Statement stmt = conn.createStatement()) {
+                consumer.accept(stmt);
+            }
+        }
+    }
+
+    private static void validateSingleRow(String query, Statement stmt, Object 
... expected) throws SQLException {
+        try (ResultSet rs = stmt.executeQuery(query)) {
+            assertTrue(rs.next());
+
+            for (int i = 0; i < expected.length; i++) {
+                assertEquals(expected[i], rs.getObject(i + 1));
+            }
+
+            assertFalse(rs.next());
+        }
+    }
+
+    @FunctionalInterface
+    private interface ConsumerX<T> {
+        void accept(T obj) throws SQLException;
+    }
+}
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionProperties.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionProperties.java
index 1a72ddcbae..8797c9ba91 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionProperties.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionProperties.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.jdbc;
 
 import java.sql.SQLException;
+import java.time.ZoneId;
 import org.apache.ignite.client.ClientAuthenticationMode;
 import org.apache.ignite.internal.client.HostAndPort;
 
@@ -253,4 +254,18 @@ public interface ConnectionProperties {
      * @param password Password.
      */
     void setPassword(String password);
+
+    /**
+     * Get connection time zone ID.
+     *
+     * @return Connection time zone ID.
+     */
+    ZoneId getConnectionTimeZone();
+
+    /**
+     * Set connection time zone ID.
+     *
+     * @param timeZoneId Connection time zone ID.
+     */
+    void setConnectionTimeZone(ZoneId timeZoneId);
 }
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionPropertiesImpl.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionPropertiesImpl.java
index a34499b9c1..d39b04dc8d 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionPropertiesImpl.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionPropertiesImpl.java
@@ -20,9 +20,12 @@ package org.apache.ignite.internal.jdbc;
 import java.io.Serializable;
 import java.sql.DriverPropertyInfo;
 import java.sql.SQLException;
+import java.time.DateTimeException;
+import java.time.ZoneId;
 import java.util.Arrays;
 import java.util.Properties;
 import java.util.StringTokenizer;
+import java.util.TimeZone;
 import java.util.stream.Collectors;
 import org.apache.ignite.client.ClientAuthenticationMode;
 import org.apache.ignite.client.IgniteClientConfiguration;
@@ -124,11 +127,15 @@ public class ConnectionPropertiesImpl implements 
ConnectionProperties, Serializa
     private final StringProperty password = new StringProperty("password",
             "Password", null, null, false, null);
 
+    /** Client connection time zone ID. This property can be used by the 
client to change the time zone of the "session" on the server. */
+    private final TimeZoneProperty connectionTimeZone = new 
TimeZoneProperty("connectionTimeZone",
+            "Client connection time zone ID", 
TimeZone.getDefault().toZoneId(), null, false, null);
+
     /** Properties array. */
     private final ConnectionProperty[] propsArray = {
             qryTimeout, connTimeout, trustStorePath, trustStorePassword,
             sslEnabled, clientAuth, ciphers, keyStorePath, keyStorePassword,
-            username, password
+            username, password, connectionTimeZone
     };
 
     /** {@inheritDoc} */
@@ -344,6 +351,16 @@ public class ConnectionPropertiesImpl implements 
ConnectionProperties, Serializa
         this.password.setValue(password);
     }
 
+    @Override
+    public ZoneId getConnectionTimeZone() {
+        return connectionTimeZone.value();
+    }
+
+    @Override
+    public void setConnectionTimeZone(ZoneId timeZoneId) {
+        connectionTimeZone.setValue(timeZoneId);
+    }
+
     /**
      * Init connection properties.
      *
@@ -1049,6 +1066,54 @@ public class ConnectionPropertiesImpl implements 
ConnectionProperties, Serializa
         }
     }
 
+    /**
+     * Time zone property.
+     */
+    private static class TimeZoneProperty extends ConnectionProperty {
+        private static final long serialVersionUID = 0L;
+
+        private ZoneId val;
+
+        TimeZoneProperty(String name, String desc, ZoneId dfltVal, String[] 
choices, boolean required,
+                PropertyValidator validator) {
+            super(name, desc, dfltVal, choices, required, validator);
+
+            val = dfltVal;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        void init(String str) throws SQLException {
+            if (str == null) {
+                val = dfltVal != null ? (ZoneId) dfltVal : null;
+
+                return;
+            }
+
+            try {
+                val = ZoneId.of(str);
+            } catch (DateTimeException e) {
+                throw new SQLException("Failed to set time zone property 
[value=" + str + ']', SqlStateCode.CLIENT_CONNECTION_FAILED, e);
+            }
+        }
+
+        /** Sets the property value. */
+        void setValue(ZoneId val) {
+            this.val = val;
+        }
+
+        /** Returns property value. */
+        ZoneId value() {
+            return val;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        String valueObject() {
+            return String.valueOf(val);
+        }
+    }
+
     /**
      * Get the driver properties.
      *
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcClientQueryEventHandler.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcClientQueryEventHandler.java
index 25c1d3949d..bfbdd46335 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcClientQueryEventHandler.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcClientQueryEventHandler.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.jdbc;
 
+import java.time.ZoneId;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.client.TcpIgniteClient;
 import org.apache.ignite.internal.client.proto.ClientOp;
@@ -55,8 +56,10 @@ public class JdbcClientQueryEventHandler implements 
JdbcQueryEventHandler {
 
     /** {@inheritDoc} */
     @Override
-    public CompletableFuture<JdbcConnectResult> connect() {
-        return client.sendRequestAsync(ClientOp.JDBC_CONNECT, w -> { }, r -> {
+    public CompletableFuture<JdbcConnectResult> connect(ZoneId timeZoneId) {
+        return client.sendRequestAsync(ClientOp.JDBC_CONNECT, w -> {
+            w.out().packString(timeZoneId.getId());
+        }, r -> {
             JdbcConnectResult res = new JdbcConnectResult();
 
             res.readBinary(r.in());
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
index 1aa69c4ad8..daca60ff68 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
@@ -65,6 +65,7 @@ import 
org.apache.ignite.internal.jdbc.proto.event.JdbcConnectResult;
 import org.apache.ignite.internal.jdbc.proto.event.JdbcFinishTxResult;
 import org.apache.ignite.internal.jdbc.proto.event.Response;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.TestOnly;
 
 /**
  * JDBC connection implementation.
@@ -119,44 +120,6 @@ public class JdbcConnection implements Connection {
     /** Jdbc metadata. Cache the JDBC object on the first access */
     private JdbcDatabaseMetadata metadata;
 
-    /**
-     * Constructor.
-     *
-     * @param handler Handler.
-     * @param props   Properties.
-     */
-    public JdbcConnection(JdbcQueryEventHandler handler, ConnectionProperties 
props) throws SQLException {
-        this.connProps = props;
-        this.handler = handler;
-
-        try {
-            JdbcConnectResult result = handler.connect().get();
-
-            if (!result.hasResults()) {
-                throw 
IgniteQueryErrorCode.createJdbcSqlException(result.err(), result.status());
-            }
-
-            connectionId = result.connectionId();
-        } catch (InterruptedException e) {
-            throw new SQLException("Thread was interrupted.", e);
-        } catch (ExecutionException e) {
-            throw new SQLException("Failed to initialize connection.", e);
-        } catch (CancellationException e) {
-            throw new SQLException("Connection initialization canceled.", e);
-        }
-
-        autoCommit = true;
-
-        netTimeout = connProps.getConnectionTimeout();
-        qryTimeout = connProps.getQueryTimeout();
-
-        holdability = HOLD_CURSORS_OVER_COMMIT;
-
-        schema = DEFAULT_SCHEMA_NAME;
-
-        client = null;
-    }
-
     /**
      * Creates new connection.
      *
@@ -192,7 +155,7 @@ public class JdbcConnection implements Connection {
         this.handler = new JdbcClientQueryEventHandler(client);
 
         try {
-            JdbcConnectResult result = handler.connect().get();
+            JdbcConnectResult result = 
handler.connect(connProps.getConnectionTimeZone()).get();
 
             if (!result.hasResults()) {
                 throw 
IgniteQueryErrorCode.createJdbcSqlException(result.err(), result.status());
@@ -214,6 +177,27 @@ public class JdbcConnection implements Connection {
         holdability = HOLD_CURSORS_OVER_COMMIT;
     }
 
+    /**
+     * Constructor used for testing purposes.
+     */
+    @TestOnly
+    public JdbcConnection(JdbcQueryEventHandler handler, ConnectionProperties 
props) {
+        this.connProps = props;
+        this.handler = handler;
+
+        autoCommit = true;
+
+        netTimeout = connProps.getConnectionTimeout();
+        qryTimeout = connProps.getQueryTimeout();
+
+        holdability = HOLD_CURSORS_OVER_COMMIT;
+
+        schema = DEFAULT_SCHEMA_NAME;
+
+        client = null;
+        connectionId = -1;
+    }
+
     private static @Nullable SslConfiguration 
extractSslConfiguration(ConnectionProperties connProps) {
         if (connProps.isSslEnabled()) {
             return SslConfiguration.builder()
diff --git 
a/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc/PreparedStatementParamsTest.java
 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc/PreparedStatementParamsTest.java
index 1234d85706..29a550e3dd 100644
--- 
a/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc/PreparedStatementParamsTest.java
+++ 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc/PreparedStatementParamsTest.java
@@ -25,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.mockito.Mockito.when;
 
 import java.lang.reflect.Array;
 import java.math.BigDecimal;
@@ -45,10 +44,8 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.TreeSet;
 import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
 import java.util.stream.Stream;
 import org.apache.ignite.internal.jdbc.proto.JdbcQueryEventHandler;
-import org.apache.ignite.internal.jdbc.proto.event.JdbcConnectResult;
 import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
@@ -90,11 +87,8 @@ public class PreparedStatementParamsTest extends 
BaseIgniteAbstractTest {
     private JdbcConnection conn;
 
     @BeforeEach
-    public void initConnection() throws SQLException {
-        JdbcQueryEventHandler handler = 
Mockito.mock(JdbcQueryEventHandler.class);
-        
when(handler.connect()).thenReturn(CompletableFuture.completedFuture(new 
JdbcConnectResult(1)));
-
-        conn = new JdbcConnection(handler, new ConnectionPropertiesImpl());
+    public void initConnection() {
+        conn = new JdbcConnection(Mockito.mock(JdbcQueryEventHandler.class), 
new ConnectionPropertiesImpl());
     }
 
     /** {@link PreparedStatement#clearParameters()} clears parameter list. */
diff --git 
a/modules/sql-engine/src/integrationTest/sql/types/time/test_time.test 
b/modules/sql-engine/src/integrationTest/sql/types/time/test_time.test
index a9194c0036..69bad00a86 100644
--- a/modules/sql-engine/src/integrationTest/sql/types/time/test_time.test
+++ b/modules/sql-engine/src/integrationTest/sql/types/time/test_time.test
@@ -27,9 +27,9 @@ NULL
 query T rowsort
 SELECT cast(i AS VARCHAR) FROM times
 ----
-00:01:20.000
+00:01:20
 20:08:10.001
-20:08:10.330
+20:08:10.33
 20:08:10.998
 NULL
 
diff --git 
a/modules/sql-engine/src/integrationTest/sql/types/time/time_parsing.test 
b/modules/sql-engine/src/integrationTest/sql/types/time/time_parsing.test
index 9dbb1523d5..794647f9b6 100644
--- a/modules/sql-engine/src/integrationTest/sql/types/time/time_parsing.test
+++ b/modules/sql-engine/src/integrationTest/sql/types/time/time_parsing.test
@@ -22,8 +22,8 @@ SELECT '14:42:04.999'::TIME(3)::VARCHAR
 ----
 14:42:04.999
 
+# trailing zeros get truncated
 query I
-SELECT '14:42:04.999'::TIME(6)::VARCHAR
+SELECT '14:42:04.999000'::TIME(6)::VARCHAR
 ----
-14:42:04.999000
-
+14:42:04.999
diff --git 
a/modules/sql-engine/src/integrationTest/sql/types/timestamp/test_timestamp.test
 
b/modules/sql-engine/src/integrationTest/sql/types/timestamp/test_timestamp.test
index d96a48a8d2..359298ee0b 100644
--- 
a/modules/sql-engine/src/integrationTest/sql/types/timestamp/test_timestamp.test
+++ 
b/modules/sql-engine/src/integrationTest/sql/types/timestamp/test_timestamp.test
@@ -107,13 +107,13 @@ SELECT TIMESTAMP '2008-01-01 00:00:01.999'::VARCHAR
 query T
 SELECT '2008-01-01 00:00:01.999'::TIMESTAMP::VARCHAR
 ----
-2008-01-01 00:00:01.999000
+2008-01-01 00:00:01.999
 
 # Value must be rounded up.
 query T
 SELECT '2008-01-01 00:00:01.9995'::TIMESTAMP(3)::VARCHAR
 ----
-2008-01-01 00:00:02.000
+2008-01-01 00:00:02
 
 # Value must be rounded down.
 query T
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 eb4639c45e..bdff76032c 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
@@ -52,6 +52,7 @@ import org.apache.calcite.schema.Statistic;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.ignite.internal.lang.IgniteStringBuilder;
 import org.apache.ignite.internal.sql.engine.sql.fun.IgniteSqlOperatorTable;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem;
 import org.apache.ignite.internal.sql.engine.util.Commons;
@@ -574,6 +575,75 @@ public class IgniteSqlFunctions {
         return timestamp - offset;
     }
 
+    /**
+     * Helper for CAST({timestamp} AS VARCHAR(n)).
+     *
+     * <p>Note: this method is a copy of the avatica {@link 
DateTimeUtils#unixTimestampToString(long, int)} method,
+     *          with the only difference being that it does not add trailing 
zeros.
+     */
+    public static String unixTimestampToString(long timestamp, int precision) {
+        IgniteStringBuilder buf = new IgniteStringBuilder(17);
+        int date = (int) (timestamp / DateTimeUtils.MILLIS_PER_DAY);
+        int time = (int) (timestamp % DateTimeUtils.MILLIS_PER_DAY);
+
+        if (time < 0) {
+            --date;
+
+            time += (int) DateTimeUtils.MILLIS_PER_DAY;
+        }
+
+        buf.app(DateTimeUtils.unixDateToString(date)).app(' ');
+
+        unixTimeToString(buf, time, precision);
+
+        return buf.toString();
+    }
+
+    /**
+     * Helper for CAST({time} AS VARCHAR(n)).
+     *
+     * <p>Note: this method is a copy of the avatica {@link 
DateTimeUtils#unixTimestampToString(long, int)} method,
+     *          with the only difference being that it does not add trailing 
zeros.
+     */
+    public static String unixTimeToString(int time, int precision) {
+        IgniteStringBuilder buf = new IgniteStringBuilder(8 + (precision > 0 ? 
1 + precision : 0));
+
+        unixTimeToString(buf, time, precision);
+
+        return buf.toString();
+    }
+
+    private static void unixTimeToString(IgniteStringBuilder buf, int time, 
int precision) {
+        int h = time / 3600000;
+        int time2 = time % 3600000;
+        int m = time2 / 60000;
+        int time3 = time2 % 60000;
+        int s = time3 / 1000;
+        int ms = time3 % 1000;
+
+        buf.app((char) ('0' + (h / 10) % 10))
+                .app((char) ('0' + h % 10))
+                .app(':')
+                .app((char) ('0' + (m / 10) % 10))
+                .app((char) ('0' + m % 10))
+                .app(':')
+                .app((char) ('0' + (s / 10) % 10))
+                .app((char) ('0' + s % 10));
+
+        if (precision == 0 || ms == 0) {
+            return;
+        }
+
+        buf.app('.');
+        do {
+            buf.app((char) ('0' + (ms / 100)));
+
+            ms = ms % 100;
+            ms = ms * 10;
+            --precision;
+        } while (ms > 0 && precision > 0);
+    }
+
     private static @Nullable Object leastOrGreatest(boolean least, Object 
arg0, Object arg1) {
         if (arg0 == null || arg1 == null) {
             return null;
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
index 6ed985d7d2..30e8ae436e 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,9 +355,6 @@ 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;
@@ -402,6 +399,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;
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 c72481024f..926509f62d 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
@@ -28,7 +28,6 @@ import java.util.TimeZone;
 import java.util.UUID;
 import org.apache.calcite.DataContext;
 import org.apache.calcite.avatica.util.ByteString;
-import org.apache.calcite.avatica.util.DateTimeUtils;
 import org.apache.calcite.linq4j.tree.Types;
 import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.sql.SqlIntervalQualifier;
@@ -137,13 +136,15 @@ public enum IgniteMethod {
 
     /**
      * Conversion of timestamp to string (precision aware).
+     * See {@link IgniteSqlFunctions#unixTimestampToString(long, int)}.
      */
-    UNIX_TIMESTAMP_TO_STRING_PRECISION_AWARE(DateTimeUtils.class, 
"unixTimestampToString", long.class, int.class),
+    UNIX_TIMESTAMP_TO_STRING_PRECISION_AWARE(IgniteSqlFunctions.class, 
"unixTimestampToString", long.class, int.class),
 
     /**
      * Conversion of time to string (precision aware).
+     * See {@link IgniteSqlFunctions#unixTimeToString(int, int)}.
      */
-    UNIX_TIME_TO_STRING_PRECISION_AWARE(DateTimeUtils.class, 
"unixTimeToString", int.class, int.class),
+    UNIX_TIME_TO_STRING_PRECISION_AWARE(IgniteSqlFunctions.class, 
"unixTimeToString", int.class, int.class),
     ;
 
     private final Method method;
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
index 813ffb9532..1e8058e9cf 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
@@ -19,6 +19,8 @@ package org.apache.ignite.internal.sql.engine.exec.exp;
 
 import static 
org.apache.ignite.internal.sql.engine.prepare.IgniteSqlValidator.NUMERIC_FIELD_OVERFLOW_ERROR;
 import static 
org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
@@ -539,4 +541,19 @@ public class IgniteSqlFunctionsTest {
             assertThrows(ArithmeticException.class, () -> 
IgniteSqlFunctions.decimalDivide(num, denum, 4, 2));
         }
     }
+
+    @ParameterizedTest
+    @CsvSource({
+            "1970-01-01 00:00:00,     0, 0",
+            "1970-01-01 00:00:00.12,  2, 123",
+            "1970-01-01 00:00:00.123, 3, 123",
+            "1970-01-01 00:00:00.123, 6, 123",
+            "1970-02-01 23:59:59,     0, 2764799000",
+            "1970-02-01 23:59:59.04,  2, 2764799040",
+            "1969-12-31 23:59:59.999, 3, -1",
+            "1969-12-31 23:59:59.98,  2, -11",
+    })
+    public void testTimestampToString(String expectedDate, int precision, long 
millis) {
+        assertThat(IgniteSqlFunctions.unixTimestampToString(millis, 
precision), is(expectedDate));
+    }
 }


Reply via email to