From 5d07c8be00b5b4d82ad23a0d323c0e085c04bab9 Mon Sep 17 00:00:00 2001 From: Ashhar Hasan Date: Wed, 20 Sep 2023 13:49:09 +0530 Subject: [PATCH] Add support for timezone parameter to JDBC driver --- .../main/java/io/trino/cli/ClientOptions.java | 3 +- .../client/uri/ConnectionProperties.java | 12 +++++++ .../io/trino/client/uri/PropertyName.java | 1 + .../java/io/trino/client/uri/TrinoUri.java | 20 +++++++++++ .../java/io/trino/jdbc/TrinoConnection.java | 2 +- .../java/io/trino/jdbc/TestTrinoDriver.java | 34 +++++++++++++++++++ .../io/trino/jdbc/TestTrinoDriverUri.java | 22 ++++++++++++ docs/src/main/sphinx/client/jdbc.md | 4 +++ 8 files changed, 96 insertions(+), 2 deletions(-) diff --git a/client/trino-cli/src/main/java/io/trino/cli/ClientOptions.java b/client/trino-cli/src/main/java/io/trino/cli/ClientOptions.java index c262cae63ef0..9168e2e623e0 100644 --- a/client/trino-cli/src/main/java/io/trino/cli/ClientOptions.java +++ b/client/trino-cli/src/main/java/io/trino/cli/ClientOptions.java @@ -325,7 +325,7 @@ public ClientSession toClientSession(TrinoUri uri) .clientInfo(clientInfo.orElse(null)) .catalog(uri.getCatalog().orElse(catalog.orElse(null))) .schema(uri.getSchema().orElse(schema.orElse(null))) - .timeZone(timeZone) + .timeZone(uri.getTimeZone()) .locale(Locale.getDefault()) .resourceEstimates(toResourceEstimates(resourceEstimates)) .properties(toProperties(sessionProperties)) @@ -409,6 +409,7 @@ public TrinoUri getTrinoUri(Map restrictedProperties) traceToken.ifPresent(builder::setTraceToken); socksProxy.ifPresent(builder::setSocksProxy); httpProxy.ifPresent(builder::setHttpProxy); + builder.setTimeZone(timeZone); builder.setDisableCompression(disableCompression); TrinoUri trinoUri; diff --git a/client/trino-client/src/main/java/io/trino/client/uri/ConnectionProperties.java b/client/trino-client/src/main/java/io/trino/client/uri/ConnectionProperties.java index 6bc6bdb0051e..aed3fe36b15d 100644 --- a/client/trino-client/src/main/java/io/trino/client/uri/ConnectionProperties.java +++ b/client/trino-client/src/main/java/io/trino/client/uri/ConnectionProperties.java @@ -24,6 +24,7 @@ import org.ietf.jgss.GSSCredential; import java.io.File; +import java.time.ZoneId; import java.util.List; import java.util.Map; import java.util.Optional; @@ -97,6 +98,7 @@ enum SslVerificationMode public static final ConnectionProperty> DNS_RESOLVER = new Resolver(); public static final ConnectionProperty DNS_RESOLVER_CONTEXT = new ResolverContext(); public static final ConnectionProperty HOSTNAME_IN_CERTIFICATE = new HostnameInCertificate(); + public static final ConnectionProperty TIMEZONE = new TimeZone(); private static final Set> ALL_PROPERTIES = ImmutableSet.>builder() .add(USER) @@ -141,6 +143,7 @@ enum SslVerificationMode .add(DNS_RESOLVER) .add(DNS_RESOLVER_CONTEXT) .add(HOSTNAME_IN_CERTIFICATE) + .add(TIMEZONE) .build(); private static final Map> KEY_LOOKUP = unmodifiableMap(ALL_PROPERTIES.stream() @@ -704,6 +707,15 @@ public HostnameInCertificate() } } + private static class TimeZone + extends AbstractConnectionProperty + { + public TimeZone() + { + super(PropertyName.TIMEZONE, NOT_REQUIRED, ALLOWED, ZoneId::of); + } + } + private static class MapPropertyParser { private static final CharMatcher PRINTABLE_ASCII = CharMatcher.inRange((char) 0x21, (char) 0x7E); diff --git a/client/trino-client/src/main/java/io/trino/client/uri/PropertyName.java b/client/trino-client/src/main/java/io/trino/client/uri/PropertyName.java index 6c344373638c..86143207eae5 100644 --- a/client/trino-client/src/main/java/io/trino/client/uri/PropertyName.java +++ b/client/trino-client/src/main/java/io/trino/client/uri/PropertyName.java @@ -64,6 +64,7 @@ public enum PropertyName DNS_RESOLVER("dnsResolver"), DNS_RESOLVER_CONTEXT("dnsResolverContext"), HOSTNAME_IN_CERTIFICATE("hostnameInCertificate"), + TIMEZONE("timezone"), // these two are not actual properties but parts of the path CATALOG("catalog"), SCHEMA("schema"); diff --git a/client/trino-client/src/main/java/io/trino/client/uri/TrinoUri.java b/client/trino-client/src/main/java/io/trino/client/uri/TrinoUri.java index 394a3d497090..3eda8238f234 100644 --- a/client/trino-client/src/main/java/io/trino/client/uri/TrinoUri.java +++ b/client/trino-client/src/main/java/io/trino/client/uri/TrinoUri.java @@ -36,6 +36,7 @@ import java.sql.DriverPropertyInfo; import java.sql.SQLException; import java.time.Duration; +import java.time.ZoneId; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -104,6 +105,7 @@ import static io.trino.client.uri.ConnectionProperties.SslVerificationMode.CA; import static io.trino.client.uri.ConnectionProperties.SslVerificationMode.FULL; import static io.trino.client.uri.ConnectionProperties.SslVerificationMode.NONE; +import static io.trino.client.uri.ConnectionProperties.TIMEZONE; import static io.trino.client.uri.ConnectionProperties.TRACE_TOKEN; import static io.trino.client.uri.ConnectionProperties.USER; import static java.lang.String.format; @@ -159,6 +161,7 @@ public class TrinoUri private Optional externalAuthenticationTokenCache; private Optional> extraCredentials; private Optional hostnameInCertificate; + private Optional timeZone; private Optional clientInfo; private Optional clientTags; private Optional traceToken; @@ -211,6 +214,7 @@ private TrinoUri( Optional externalAuthenticationTokenCache, Optional> extraCredentials, Optional hostnameInCertificate, + Optional timeZone, Optional clientInfo, Optional clientTags, Optional traceToken, @@ -262,6 +266,7 @@ private TrinoUri( this.externalAuthenticationTokenCache = EXTERNAL_AUTHENTICATION_TOKEN_CACHE.getValueOrDefault(urlProperties, externalAuthenticationTokenCache); this.extraCredentials = EXTRA_CREDENTIALS.getValueOrDefault(urlProperties, extraCredentials); this.hostnameInCertificate = HOSTNAME_IN_CERTIFICATE.getValueOrDefault(urlProperties, hostnameInCertificate); + this.timeZone = TIMEZONE.getValueOrDefault(urlProperties, timeZone); this.clientInfo = CLIENT_INFO.getValueOrDefault(urlProperties, clientInfo); this.clientTags = CLIENT_TAGS.getValueOrDefault(urlProperties, clientTags); this.traceToken = TRACE_TOKEN.getValueOrDefault(urlProperties, traceToken); @@ -347,6 +352,7 @@ private Properties buildProperties() .map(entry -> entry.getKey() + ":" + entry.getValue()) .collect(Collectors.joining(";")))); hostnameInCertificate.ifPresent(value -> properties.setProperty(PropertyName.HOSTNAME_IN_CERTIFICATE.toString(), value)); + timeZone.ifPresent(value -> properties.setProperty(PropertyName.TIMEZONE.toString(), value.getId())); clientInfo.ifPresent(value -> properties.setProperty(PropertyName.CLIENT_INFO.toString(), value)); clientTags.ifPresent(value -> properties.setProperty(PropertyName.CLIENT_TAGS.toString(), value)); traceToken.ifPresent(value -> properties.setProperty(PropertyName.TRACE_TOKEN.toString(), value)); @@ -404,6 +410,7 @@ protected TrinoUri(URI uri, Properties driverProperties) this.externalAuthenticationTokenCache = EXTERNAL_AUTHENTICATION_TOKEN_CACHE.getValue(properties); this.extraCredentials = EXTRA_CREDENTIALS.getValue(properties); this.hostnameInCertificate = HOSTNAME_IN_CERTIFICATE.getValue(properties); + this.timeZone = TIMEZONE.getValue(properties); this.clientInfo = CLIENT_INFO.getValue(properties); this.clientTags = CLIENT_TAGS.getValue(properties); this.traceToken = TRACE_TOKEN.getValue(properties); @@ -531,6 +538,11 @@ public boolean isAssumeLiteralUnderscoreInMetadataCallsForNonConformingClients() return assumeLiteralUnderscoreInMetadataCallsForNonConformingClients.orElse(false); } + public ZoneId getTimeZone() + { + return timeZone.orElseGet(ZoneId::systemDefault); + } + public Properties getProperties() { return properties; @@ -909,6 +921,7 @@ public static final class Builder private KnownTokenCache externalAuthenticationTokenCache; private Map extraCredentials; private String hostnameInCertificate; + private ZoneId timeZone; private String clientInfo; private String clientTags; private String traceToken; @@ -1166,6 +1179,12 @@ public Builder setHostnameInCertificate(String hostnameInCertificate) return this; } + public Builder setTimeZone(ZoneId timeZone) + { + this.timeZone = requireNonNull(timeZone, "timeZone is null"); + return this; + } + public Builder setClientInfo(String clientInfo) { this.clientInfo = requireNonNull(clientInfo, "clientInfo is null"); @@ -1239,6 +1258,7 @@ public TrinoUri build() Optional.ofNullable(externalAuthenticationTokenCache), Optional.ofNullable(extraCredentials), Optional.ofNullable(hostnameInCertificate), + Optional.ofNullable(timeZone), Optional.ofNullable(clientInfo), Optional.ofNullable(clientTags), Optional.ofNullable(traceToken), diff --git a/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoConnection.java b/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoConnection.java index 375e6013dacb..5bd1f4751687 100644 --- a/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoConnection.java +++ b/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoConnection.java @@ -141,7 +141,7 @@ public class TrinoConnection uri.getTraceToken().ifPresent(tags -> clientInfo.put(TRACE_TOKEN, tags)); roles.putAll(uri.getRoles()); - timeZoneId.set(ZoneId.systemDefault()); + timeZoneId.set(uri.getTimeZone()); locale.set(Locale.getDefault()); sessionProperties.putAll(uri.getSessionProperties()); } diff --git a/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDriver.java b/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDriver.java index 4533b229213b..1ea1081fd725 100644 --- a/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDriver.java +++ b/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDriver.java @@ -49,6 +49,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.zone.ZoneRulesException; import java.util.ArrayList; import java.util.GregorianCalendar; import java.util.List; @@ -666,6 +667,32 @@ public void testSetTimeZoneId() assertEquals(rs.getTimestamp("ts"), new Timestamp(new DateTime(2001, 2, 3, 3, 4, 5, defaultZone).getMillis())); } } + + try (Connection connection = createConnectionWithParameter("timezone=Asia/Kolkata")) { + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery(sql)) { + assertTrue(rs.next()); + assertEquals(rs.getString("zone"), "Asia/Kolkata"); + // setting the session timezone has no effect on the interpretation of timestamps in the JDBC driver + assertEquals(rs.getTimestamp("ts"), new Timestamp(new DateTime(2001, 2, 3, 3, 4, 5, defaultZone).getMillis())); + } + } + + try (Connection connection = createConnectionWithParameter("timezone=UTC+05:30")) { + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery(sql)) { + assertTrue(rs.next()); + assertEquals(rs.getString("zone"), "+05:30"); + // setting the session timezone has no effect on the interpretation of timestamps in the JDBC driver + assertEquals(rs.getTimestamp("ts"), new Timestamp(new DateTime(2001, 2, 3, 3, 4, 5, defaultZone).getMillis())); + } + } + + assertThatThrownBy(() -> createConnectionWithParameter("timezone=Asia/NOT_FOUND")) + .isInstanceOf(SQLException.class) + .hasMessage("Connection property timezone value is invalid: Asia/NOT_FOUND") + .hasRootCauseInstanceOf(ZoneRulesException.class) + .hasRootCauseMessage("Unknown time-zone ID: Asia/NOT_FOUND"); } @Test @@ -1200,6 +1227,13 @@ private Connection createConnection(String catalog, String schema) return DriverManager.getConnection(url, "test", null); } + private Connection createConnectionWithParameter(String parameter) + throws SQLException + { + String url = format("jdbc:trino://%s?%s", server.getAddress(), parameter); + return DriverManager.getConnection(url, "test", null); + } + private static Properties toProperties(Map map) { Properties properties = new Properties(); diff --git a/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDriverUri.java b/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDriverUri.java index 3a7bf7a8ac9c..9a82f9578998 100644 --- a/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDriverUri.java +++ b/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDriverUri.java @@ -17,6 +17,8 @@ import java.net.URI; import java.sql.SQLException; +import java.time.ZoneId; +import java.time.zone.ZoneRulesException; import java.util.Properties; import static io.trino.client.uri.PropertyName.CLIENT_TAGS; @@ -423,6 +425,26 @@ public void testAssumeLiteralUnderscoreInMetadataCallsForNonConformingClients() assertThat(parameters.isAssumeLiteralNamesInMetadataCallsForNonConformingClients()).isFalse(); } + @Test + public void testTimezone() + throws SQLException + { + TrinoDriverUri defaultParameters = createDriverUri("jdbc:trino://localhost:8080"); + assertThat(defaultParameters.getTimeZone()).isEqualTo(ZoneId.systemDefault()); + + TrinoDriverUri parameters = createDriverUri("jdbc:trino://localhost:8080?timezone=Asia/Kolkata"); + assertThat(parameters.getTimeZone()).isEqualTo(ZoneId.of("Asia/Kolkata")); + + TrinoDriverUri offsetParameters = createDriverUri("jdbc:trino://localhost:8080?timezone=UTC+05:30"); + assertThat(offsetParameters.getTimeZone()).isEqualTo(ZoneId.of("UTC+05:30")); + + assertThatThrownBy(() -> createDriverUri("jdbc:trino://localhost:8080?timezone=Asia/NOT_FOUND")) + .isInstanceOf(SQLException.class) + .hasMessage("Connection property timezone value is invalid: Asia/NOT_FOUND") + .hasRootCauseInstanceOf(ZoneRulesException.class) + .hasRootCauseMessage("Unknown time-zone ID: Asia/NOT_FOUND"); + } + private static void assertUriPortScheme(TrinoDriverUri parameters, int port, String scheme) { URI uri = parameters.getHttpUri(); diff --git a/docs/src/main/sphinx/client/jdbc.md b/docs/src/main/sphinx/client/jdbc.md index 6f6c4fe5a14f..76d4710d34a3 100644 --- a/docs/src/main/sphinx/client/jdbc.md +++ b/docs/src/main/sphinx/client/jdbc.md @@ -248,4 +248,8 @@ may not be specified using both methods. treated as underscores. You can use this as a workaround for applications that do not escape schema or table names when passing them to ``DatabaseMetaData`` methods as schema or table name patterns. + * - ``timezone`` + - Sets the time zone for the session using the `time zone passed + `_. Defaults + to the timezone of the JVM running the JDBC driver. ```