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));
+ }
}