From ea523643fbae4e80834cdccb1cc54e95b025025d Mon Sep 17 00:00:00 2001 From: "mykhailo.skliar" Date: Thu, 17 Jul 2025 19:46:12 +0200 Subject: [PATCH 1/4] Add support for TIMESTAMP and TIMESTAMP WITH TIME ZONE types in Exasol connector - implemented additional timestamp with precision data type for ExasolClient - added correspondent tests for timestamp with precision data type - started implementation of additional timestamp with local time zone data type for ExasolClient - updated exasol db version to 8.34.0 in integration tests - finished implementation of additional timestamp with local time zone data type for ExasolClient - implemented special addRoundTrip method with additional column expression parameter for the predicate to fix the test predicate assumption - all timestamp with timezone tests use input literal timestamp string, which is interpreted as a JVM timestamp string ("America/Bahia_Banderas") but expected literal strings are interpreted as UTC strings. Therefore the difference between these timestamps is 6 hours (with DST) or 5 (without DST); for some historical values, like 1970 for example, the difference can be 7, which is expected behaviour for timestamp with local time zone - implemented ColumnMapper, TimestampColumnMapper and TimestampWithTimeZoneColumnMapper to encapsulate mapping logic in separate classes and prevent ExasolClient from becoming too big - removed mapper classes and moved the logic to ExasolClient, based on the PR suggesions - removed unnecessary javadocs - improved javadocs and comments for testing timestamp with time zone by explaining why only jvm time zone is currently used for testing timestamp with time zone - removed using * import in ExasolClient based on the PR suggestions - removed TestExasolTimestampMapping and moved testing timestamp and timestamp with timezone to TestExasolTypeMapping, based on the PR suggestions - fixed checkstyle violations after enabling checkstyle plugin --- .../io/trino/plugin/exasol/ExasolClient.java | 296 ++++++++++++++++++ .../exasol/TestExasolConnectorTest.java | 32 +- .../plugin/exasol/TestExasolTypeMapping.java | 138 ++++++++ .../plugin/exasol/TestingExasolServer.java | 2 +- .../env/environment/EnvMultinodeExasol.java | 2 +- 5 files changed, 452 insertions(+), 18 deletions(-) diff --git a/plugin/trino-exasol/src/main/java/io/trino/plugin/exasol/ExasolClient.java b/plugin/trino-exasol/src/main/java/io/trino/plugin/exasol/ExasolClient.java index 434c3454e5f2..d8057d977b13 100644 --- a/plugin/trino-exasol/src/main/java/io/trino/plugin/exasol/ExasolClient.java +++ b/plugin/trino-exasol/src/main/java/io/trino/plugin/exasol/ExasolClient.java @@ -28,6 +28,8 @@ import io.trino.plugin.jdbc.JdbcTypeHandle; import io.trino.plugin.jdbc.LongReadFunction; import io.trino.plugin.jdbc.LongWriteFunction; +import io.trino.plugin.jdbc.ObjectReadFunction; +import io.trino.plugin.jdbc.ObjectWriteFunction; import io.trino.plugin.jdbc.QueryBuilder; import io.trino.plugin.jdbc.WriteFunction; import io.trino.plugin.jdbc.WriteMapping; @@ -39,42 +41,81 @@ import io.trino.spi.connector.ColumnPosition; import io.trino.spi.connector.ConnectorSession; import io.trino.spi.connector.ConnectorTableMetadata; +import io.trino.spi.type.LongTimestamp; +import io.trino.spi.type.LongTimestampWithTimeZone; +import io.trino.spi.type.TimestampType; +import io.trino.spi.type.TimestampWithTimeZoneType; import io.trino.spi.type.Type; import java.sql.Connection; import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; import java.sql.Types; +import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; import java.util.Set; +import static com.google.common.base.Preconditions.checkArgument; +import static io.trino.plugin.jdbc.PredicatePushdownController.FULL_PUSHDOWN; import static io.trino.plugin.jdbc.StandardColumnMappings.bigintColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.booleanColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.decimalColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.defaultCharColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.defaultVarcharColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.doubleColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.fromLongTrinoTimestamp; +import static io.trino.plugin.jdbc.StandardColumnMappings.fromTrinoTimestamp; import static io.trino.plugin.jdbc.StandardColumnMappings.integerColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.smallintColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.toLongTrinoTimestamp; +import static io.trino.plugin.jdbc.StandardColumnMappings.toTrinoTimestamp; import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling; import static io.trino.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR; import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; import static io.trino.spi.connector.ConnectorMetadata.MODIFYING_ROWS_MESSAGE; +import static io.trino.spi.type.DateTimeEncoding.packDateTimeWithZone; +import static io.trino.spi.type.DateTimeEncoding.unpackMillisUtc; import static io.trino.spi.type.DateType.DATE; import static io.trino.spi.type.DecimalType.createDecimalType; +import static io.trino.spi.type.TimeZoneKey.UTC_KEY; +import static io.trino.spi.type.TimestampType.createTimestampType; +import static io.trino.spi.type.TimestampWithTimeZoneType.createTimestampWithTimeZoneType; +import static io.trino.spi.type.Timestamps.PICOSECONDS_PER_NANOSECOND; +import static java.lang.String.format; +import static java.time.ZoneOffset.UTC; import static java.util.Locale.ENGLISH; public class ExasolClient extends BaseJdbcClient { + private static final int EXASOL_TIMESTAMP_WITH_TIMEZONE = 124; + private static final Set INTERNAL_SCHEMAS = ImmutableSet.builder() .add("EXA_STATISTICS") .add("SYS") .build(); + private static final int MAX_EXASOL_TIMESTAMP_PRECISION = 9; + + private static final DateTimeFormatter TIMESTAMP_NANO_OPTIONAL_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("uuuu-MM-dd HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) + .optionalEnd() + .toFormatter(); + @Inject public ExasolClient( BaseJdbcConfig config, @@ -229,6 +270,10 @@ public Optional toColumnMapping(ConnectorSession session, Connect return Optional.of(defaultVarcharColumnMapping(typeHandle.requiredColumnSize(), true)); case Types.DATE: return Optional.of(dateColumnMapping()); + case Types.TIMESTAMP: + return Optional.of(timestampColumnMapping(typeHandle)); + case EXASOL_TIMESTAMP_WITH_TIMEZONE: + return Optional.of(timestampWithTimeZoneColumnMapping(typeHandle)); } if (getUnsupportedTypeHandling(session) == CONVERT_TO_VARCHAR) { @@ -237,6 +282,257 @@ public Optional toColumnMapping(ConnectorSession session, Connect return Optional.empty(); } + private static ColumnMapping timestampColumnMapping(JdbcTypeHandle typeHandle) + { + int timestampPrecision = typeHandle.requiredDecimalDigits(); + TimestampType timestampType = createTimestampType(timestampPrecision); + if (timestampType.isShort()) { + return ColumnMapping.longMapping( + timestampType, + longTimestampReadFunction(timestampType), + longTimestampWriteFunction(timestampType), + FULL_PUSHDOWN); + } + return ColumnMapping.objectMapping( + timestampType, + objectTimestampReadFunction(timestampType), + objectTimestampWriteFunction(timestampType), + FULL_PUSHDOWN); + } + + private static LongReadFunction longTimestampReadFunction(TimestampType timestampType) + { + return (resultSet, columnIndex) -> { + Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class); + return toTrinoTimestamp(timestampType, timestamp.toLocalDateTime()); + }; + } + + private static ObjectReadFunction objectTimestampReadFunction(TimestampType timestampType) + { + verifyObjectTimestampPrecision(timestampType); + return ObjectReadFunction.of( + LongTimestamp.class, + (resultSet, columnIndex) -> { + Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class); + return toLongTrinoTimestamp(timestampType, timestamp.toLocalDateTime()); + }); + } + + private static void verifyObjectTimestampPrecision(TimestampType timestampType) + { + int precision = timestampType.getPrecision(); + checkArgument(precision > TimestampType.MAX_SHORT_PRECISION && precision <= MAX_EXASOL_TIMESTAMP_PRECISION, + "Precision is out of range: %s", precision); + } + + private static ObjectWriteFunction objectTimestampWriteFunction(TimestampType timestampType) + { + int precision = timestampType.getPrecision(); + verifyObjectTimestampPrecision(timestampType); + + return new ObjectWriteFunction() { + @Override + public Class getJavaType() + { + return LongTimestamp.class; + } + + @Override + public void set(PreparedStatement statement, int index, Object value) + throws SQLException + { + LocalDateTime timestamp = fromLongTrinoTimestamp((LongTimestamp) value, precision); + statement.setString(index, TIMESTAMP_NANO_OPTIONAL_FORMATTER.format(timestamp)); + } + + @Override + public String getBindExpression() + { + return getTimestampBindExpression(precision); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static LongWriteFunction longTimestampWriteFunction(TimestampType timestampType) + { + return new LongWriteFunction() + { + @Override + public String getBindExpression() + { + return getTimestampBindExpression(timestampType.getPrecision()); + } + + @Override + public void set(PreparedStatement statement, int index, long epochMicros) + throws SQLException + { + LocalDateTime localDateTime = fromTrinoTimestamp(epochMicros); + Timestamp timestampValue = Timestamp.valueOf(localDateTime); + statement.setObject(index, timestampValue); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static ColumnMapping timestampWithTimeZoneColumnMapping(JdbcTypeHandle typeHandle) + { + int timestampPrecision = typeHandle.requiredDecimalDigits(); + TimestampWithTimeZoneType timestampWithTimeZoneType = createTimestampWithTimeZoneType(timestampPrecision); + + if (timestampWithTimeZoneType.isShort()) { + return ColumnMapping.longMapping( + timestampWithTimeZoneType, + longTimestampWithTimeZoneReadFunction(), + longTimestampWithTimeZoneWriteFunction(timestampWithTimeZoneType), + FULL_PUSHDOWN); + } + return ColumnMapping.objectMapping( + timestampWithTimeZoneType, + objectTimestampWithTimeZoneReadFunction(timestampWithTimeZoneType), + objectTimestampWithTimeZoneWriteFunction(timestampWithTimeZoneType), + FULL_PUSHDOWN); + } + + private static LongReadFunction longTimestampWithTimeZoneReadFunction() + { + return (resultSet, columnIndex) -> { + Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class); + return packDateTimeWithZone(timestamp.getTime(), UTC_KEY); + }; + } + + private static ObjectReadFunction objectTimestampWithTimeZoneReadFunction( + TimestampWithTimeZoneType timestampType) + { + verifyObjectTimestampWithTimeZonePrecision(timestampType); + return ObjectReadFunction.of( + LongTimestampWithTimeZone.class, + (resultSet, columnIndex) -> { + Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class); + if (timestamp == null) { + return null; + } + + long millisUtc = timestamp.getTime(); + long nanosUtc = millisUtc * 1_000_000L + timestamp.getNanos() % 1_000_000; + int picosOfMilli = (int) ((nanosUtc - millisUtc * 1_000_000) * 1_000); + + return LongTimestampWithTimeZone.fromEpochMillisAndFraction( + millisUtc, + picosOfMilli, + UTC_KEY); + }); + } + + private static void verifyObjectTimestampWithTimeZonePrecision(TimestampWithTimeZoneType timestampType) + { + int precision = timestampType.getPrecision(); + checkArgument(precision > TimestampWithTimeZoneType.MAX_SHORT_PRECISION && precision <= MAX_EXASOL_TIMESTAMP_PRECISION, + "Precision is out of range: %s", precision); + } + + private static ObjectWriteFunction objectTimestampWithTimeZoneWriteFunction(TimestampWithTimeZoneType timestampType) + { + int precision = timestampType.getPrecision(); + verifyObjectTimestampWithTimeZonePrecision(timestampType); + + return new ObjectWriteFunction() { + @Override + public Class getJavaType() + { + return LongTimestampWithTimeZone.class; + } + + @Override + public void set(PreparedStatement statement, int index, Object value) + throws SQLException + { + if (value == null) { + statement.setNull(index, Types.VARCHAR); + return; + } + + LongTimestampWithTimeZone timestampValue = (LongTimestampWithTimeZone) value; + + long epochMillis = timestampValue.getEpochMillis(); + int picosOfMilli = timestampValue.getPicosOfMilli(); + int nanos = picosOfMilli / PICOSECONDS_PER_NANOSECOND; + + Instant instant = Instant.ofEpochMilli(epochMillis).plusNanos(nanos); + ZonedDateTime zonedDateTime = instant.atZone(UTC); + + String formatted = TIMESTAMP_NANO_OPTIONAL_FORMATTER.format(zonedDateTime); + statement.setString(index, formatted); + } + + @Override + public String getBindExpression() + { + return getTimestampBindExpression(precision); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static LongWriteFunction longTimestampWithTimeZoneWriteFunction(TimestampWithTimeZoneType timestampType) + { + return new LongWriteFunction() + { + @Override + public String getBindExpression() + { + return getTimestampBindExpression(timestampType.getPrecision()); + } + + @Override + public void set(PreparedStatement statement, int index, long epochMicros) + throws SQLException + { + Instant instant = Instant.ofEpochMilli(unpackMillisUtc(epochMicros)); + LocalDateTime localDateTime = instant.atZone(ZoneOffset.UTC).toLocalDateTime(); + statement.setString(index, TIMESTAMP_NANO_OPTIONAL_FORMATTER.format(localDateTime)); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static String getTimestampBindExpression(int precision) + { + if (precision <= 0) { + return "TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS')"; + } + else { + return format("TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS.FF%d')", precision); + } + } + private static ColumnMapping dateColumnMapping() { // Exasol driver does not support LocalDate diff --git a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolConnectorTest.java b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolConnectorTest.java index 2d005a115de0..78bd60624391 100644 --- a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolConnectorTest.java +++ b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolConnectorTest.java @@ -61,25 +61,25 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) return switch (connectorBehavior) { // Tests requires write access which is not implemented case SUPPORTS_AGGREGATION_PUSHDOWN, - SUPPORTS_JOIN_PUSHDOWN, - SUPPORTS_LIMIT_PUSHDOWN, - SUPPORTS_TOPN_PUSHDOWN -> false; + SUPPORTS_JOIN_PUSHDOWN, + SUPPORTS_LIMIT_PUSHDOWN, + SUPPORTS_TOPN_PUSHDOWN -> false; // Parallel writing is not supported due to restrictions of the Exasol JDBC driver. case SUPPORTS_ADD_COLUMN, - SUPPORTS_ARRAY, - SUPPORTS_COMMENT_ON_TABLE, - SUPPORTS_CREATE_SCHEMA, - SUPPORTS_CREATE_TABLE, - SUPPORTS_DELETE, - SUPPORTS_INSERT, - SUPPORTS_MAP_TYPE, - SUPPORTS_NEGATIVE_DATE, // min date is 0001-01-01 - SUPPORTS_RENAME_COLUMN, - SUPPORTS_RENAME_TABLE, - SUPPORTS_ROW_TYPE, - SUPPORTS_SET_COLUMN_TYPE, - SUPPORTS_UPDATE -> false; + SUPPORTS_ARRAY, + SUPPORTS_COMMENT_ON_TABLE, + SUPPORTS_CREATE_SCHEMA, + SUPPORTS_CREATE_TABLE, + SUPPORTS_DELETE, + SUPPORTS_INSERT, + SUPPORTS_MAP_TYPE, + SUPPORTS_NEGATIVE_DATE, // min date is 0001-01-01 + SUPPORTS_RENAME_COLUMN, + SUPPORTS_RENAME_TABLE, + SUPPORTS_ROW_TYPE, + SUPPORTS_SET_COLUMN_TYPE, + SUPPORTS_UPDATE -> false; default -> super.hasBehavior(connectorBehavior); }; diff --git a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java index b049119cfc66..d3045a1748e1 100644 --- a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java +++ b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java @@ -39,6 +39,8 @@ import static io.trino.spi.type.DateType.DATE; import static io.trino.spi.type.DecimalType.createDecimalType; import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.TimestampType.createTimestampType; +import static io.trino.spi.type.TimestampWithTimeZoneType.createTimestampWithTimeZoneType; import static io.trino.spi.type.VarcharType.createVarcharType; import static java.lang.String.format; import static java.time.ZoneOffset.UTC; @@ -275,6 +277,142 @@ private void testDate(ZoneId sessionZone) .execute(getQueryRunner(), session, exasolCreateAndInsert(TEST_SCHEMA + "." + "test_date")); } + @Test + void testTimestamp() + { + testTimestamp(UTC); + testTimestamp(jvmZone); + // using two non-JVM zones so that we don't need to worry what Exasol system zone is + testTimestamp(vilnius); + testTimestamp(kathmandu); + testTimestamp(TestingSession.DEFAULT_TIME_ZONE_KEY.getZoneId()); + } + + @Test + public void testTimestampWithTimeZone() + { + //Only JVM Time Zone is currently used for testing timestamp with time zone + //Adding test cases for other time zones would require improving the test to forcibly apply session changes for timestamp with timezone + testTimestampWithTimeZone(ZoneId.systemDefault()); + } + + @Test + public void testUnsupportedTimestampValues() + { + try (TestTable table = new TestTable(exasolServer::execute, "tpch.test_unsupported_timestamp", "(col TIMESTAMP)")) { + // Too early + assertExasolQueryFails( + "INSERT INTO " + table.getName() + " VALUES (TIMESTAMP '0000-12-31 23:59:59.999999')", + "data exception - invalid date value"); + + // Too late + assertExasolQueryFails( + "INSERT INTO " + table.getName() + " VALUES (TIMESTAMP '10000-01-01 00:00:00.000000')", + "data exception - invalid character value for cast"); + + // Precision > 9 + assertExasolQueryFails( + "INSERT INTO " + table.getName() + " VALUES (TIMESTAMP '2024-01-01 12:34:56.1234567890')", + "data exception - invalid character value for cast"); + } + } + + private void assertExasolQueryFails(String sql, String expectedMessage) + { + assertThatThrownBy(() -> exasolServer.execute(sql)) + .cause() + .hasMessageContaining(expectedMessage); + } + + /** + *

+ * Important timezone note: The input timestamp string literal is interpreted as a timestamp + * in the current JVM time zone ("America/Bahia_Banderas" in this test environment). + * The expected timestamp literal is interpreted as a UTC timestamp. + * Because of this, the expected literal is offset by 6 hours (during daylight saving time), + * 5 hours (outside DST), or 7 hours for some historical dates in 1970 due to timezone rules. + * This difference is intentional and verifies correct conversion between zones. + *

+ */ + private void testTimestampWithTimeZone(ZoneId sessionZone) + { + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) + .build(); + + SqlDataTypeTest test = SqlDataTypeTest.create() + // timestamp with precision 3 examples + .addRoundTrip("cast(col_0 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2019-03-18 10:01:17.987'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2019-03-18 16:01:17.987 UTC'") + .addRoundTrip("cast(col_1 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2018-10-28 01:33:17.456'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2018-10-28 07:33:17.456 UTC'") + .addRoundTrip("cast(col_2 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2018-10-28 03:33:33.333'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2018-10-28 09:33:33.333 UTC'") + .addRoundTrip("cast(col_3 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '1970-01-01 00:13:42.000'", createTimestampWithTimeZoneType(3), "TIMESTAMP '1970-01-01 07:13:42.000 UTC'") + .addRoundTrip("cast(col_4 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2018-04-01 02:13:55.123'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2018-04-01 08:13:55.123 UTC'") + .addRoundTrip("cast(col_5 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.999'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2020-09-27 17:34:56.999 UTC'") + .addRoundTrip("cast(col_6 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2018-03-25 03:17:17.000'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2018-03-25 09:17:17.000 UTC'") + .addRoundTrip("cast(col_7 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '1986-01-01 00:13:07.000'", createTimestampWithTimeZoneType(3), "TIMESTAMP '1986-01-01 07:13:07.000 UTC'") + + // timestamp with precision 6 examples + .addRoundTrip("cast(col_8 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2019-03-18 10:01:17.987654'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2019-03-18 16:01:17.987654 UTC'") + .addRoundTrip("cast(col_9 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2018-10-28 01:33:17.456789'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2018-10-28 07:33:17.456789 UTC'") + .addRoundTrip("cast(col_10 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2018-10-28 03:33:33.333333'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2018-10-28 09:33:33.333333 UTC'") + .addRoundTrip("cast(col_11 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '1970-01-01 00:13:42.000000'", createTimestampWithTimeZoneType(6), "TIMESTAMP '1970-01-01 07:13:42.000000 UTC'") + .addRoundTrip("cast(col_12 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2018-04-01 02:13:55.123456'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2018-04-01 08:13:55.123456 UTC'") + .addRoundTrip("cast(col_13 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2018-03-25 03:17:17.000000'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2018-03-25 09:17:17.000000 UTC'") + .addRoundTrip("cast(col_14 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '1986-01-01 00:13:07.000000'", createTimestampWithTimeZoneType(6), "TIMESTAMP '1986-01-01 07:13:07.000000 UTC'") + + // tests for other precisions (0-5 and some 1's) + .addRoundTrip("cast(col_15 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(0) with local time zone", "TIMESTAMP '1970-01-01 00:00:01'", createTimestampWithTimeZoneType(0), "TIMESTAMP '1970-01-01 07:00:01 UTC'") + .addRoundTrip("cast(col_16 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(1) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.1'", createTimestampWithTimeZoneType(1), "TIMESTAMP '1970-01-01 07:00:01.1 UTC'") + .addRoundTrip("cast(col_17 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(1) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.9'", createTimestampWithTimeZoneType(1), "TIMESTAMP '1970-01-01 07:00:01.9 UTC'") + .addRoundTrip("cast(col_18 AT TIME ZONE 'UTC' AS timestamp(2) with time zone)", "timestamp(2) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.12'", createTimestampWithTimeZoneType(2), "TIMESTAMP '1970-01-01 07:00:01.12 UTC'") + .addRoundTrip("cast(col_19 AT TIME ZONE 'UTC' AS timestamp(3) with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.123'", createTimestampWithTimeZoneType(3), "TIMESTAMP '1970-01-01 07:00:01.123 UTC'") + .addRoundTrip("cast(col_20 AT TIME ZONE 'UTC' AS timestamp(3) with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.999'", createTimestampWithTimeZoneType(3), "TIMESTAMP '1970-01-01 07:00:01.999 UTC'") + .addRoundTrip("cast(col_21 AT TIME ZONE 'UTC' AS timestamp(4) with time zone)", "timestamp(4) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.1234'", createTimestampWithTimeZoneType(4), "TIMESTAMP '1970-01-01 07:00:01.1234 UTC'") + .addRoundTrip("cast(col_22 AT TIME ZONE 'UTC' AS timestamp(5) with time zone)", "timestamp(5) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.12345'", createTimestampWithTimeZoneType(5), "TIMESTAMP '1970-01-01 07:00:01.12345 UTC'") + .addRoundTrip("cast(col_23 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(1) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.1'", createTimestampWithTimeZoneType(1), "TIMESTAMP '2020-09-27 17:34:56.1 UTC'") + .addRoundTrip("cast(col_24 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(1) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.9'", createTimestampWithTimeZoneType(1), "TIMESTAMP '2020-09-27 17:34:56.9 UTC'") + .addRoundTrip("cast(col_25 AT TIME ZONE 'UTC' AS timestamp(3) with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.123'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2020-09-27 17:34:56.123 UTC'") + .addRoundTrip("cast(col_26 AT TIME ZONE 'UTC' AS timestamp(3) with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.999'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2020-09-27 17:34:56.999 UTC'") + .addRoundTrip("cast(col_27 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.123456'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2020-09-27 17:34:56.123456 UTC'"); + + test.execute(getQueryRunner(), session, exasolCreateAndInsert(TEST_SCHEMA + "." + "test_timestamp")); + } + + private void testTimestamp(ZoneId sessionZone) + { + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) + .build(); + + SqlDataTypeTest.create() + .addRoundTrip("timestamp", "NULL", createTimestampType(3), "CAST(NULL AS TIMESTAMP)") + .addRoundTrip("timestamp", "TIMESTAMP '2013-03-11 17:30:15.123'", createTimestampType(3), "TIMESTAMP '2013-03-11 17:30:15.123'") + .addRoundTrip("timestamp", "TIMESTAMP '2018-10-28 01:33:17.456'", createTimestampType(3), "TIMESTAMP '2018-10-28 01:33:17.456'") + .addRoundTrip("timestamp", "TIMESTAMP '2018-10-28 03:33:33.333'", createTimestampType(3), "TIMESTAMP '2018-10-28 03:33:33.333'") + .addRoundTrip("timestamp", "TIMESTAMP '1970-01-01 00:13:42.000'", createTimestampType(3), "TIMESTAMP '1970-01-01 00:13:42.000'") + .addRoundTrip("timestamp", "TIMESTAMP '2018-04-01 03:13:55.123'", createTimestampType(3), "TIMESTAMP '2018-04-01 03:13:55.123'") + .addRoundTrip("timestamp", "TIMESTAMP '2020-09-27 12:34:56.999'", createTimestampType(3), "TIMESTAMP '2020-09-27 12:34:56.999'") + .addRoundTrip("timestamp", "TIMESTAMP '2018-03-25 03:17:17.000'", createTimestampType(3), "TIMESTAMP '2018-03-25 03:17:17.000'") + .addRoundTrip("timestamp", "TIMESTAMP '1986-01-01 00:13:07.000'", createTimestampType(3), "TIMESTAMP '1986-01-01 00:13:07.000'") + .addRoundTrip("timestamp(6)", "TIMESTAMP '2013-03-11 17:30:15.123456'", createTimestampType(6), "TIMESTAMP '2013-03-11 17:30:15.123456'") + .addRoundTrip("timestamp(9)", "TIMESTAMP '2013-03-11 17:30:15.123456789'", createTimestampType(9), "TIMESTAMP '2013-03-11 17:30:15.123456789'") + .addRoundTrip("timestamp(1)", "TIMESTAMP '2016-08-19 19:28:05.0'", createTimestampType(1), "TIMESTAMP '2016-08-19 19:28:05.0'") + .addRoundTrip("timestamp(2)", "TIMESTAMP '2016-08-19 19:28:05.01'", createTimestampType(2), "TIMESTAMP '2016-08-19 19:28:05.01'") + .addRoundTrip("timestamp", "TIMESTAMP '3030-03-03 12:34:56.123'", createTimestampType(3), "TIMESTAMP '3030-03-03 12:34:56.123'") + .addRoundTrip("timestamp(5)", "TIMESTAMP '3030-03-03 12:34:56.12345'", createTimestampType(5), "TIMESTAMP '3030-03-03 12:34:56.12345'") + .addRoundTrip("timestamp(9)", "TIMESTAMP '3030-03-03 12:34:56.123456789'", createTimestampType(9), "TIMESTAMP '3030-03-03 12:34:56.123456789'") + .addRoundTrip("timestamp", "TIMESTAMP '3030-03-03 12:34:56.123'", createTimestampType(3), "TIMESTAMP '3030-03-03 12:34:56.123'") + .addRoundTrip("timestamp(5)", "TIMESTAMP '3030-03-03 12:34:56.12345'", createTimestampType(5), "TIMESTAMP '3030-03-03 12:34:56.12345'") + .addRoundTrip("timestamp(9)", "TIMESTAMP '3030-03-03 12:34:56.123456789'", createTimestampType(9), "TIMESTAMP '3030-03-03 12:34:56.123456789'") + .addRoundTrip("timestamp(0)", "TIMESTAMP '2017-07-01'", createTimestampType(0), "TIMESTAMP '2017-07-01'") // summer on northern hemisphere (possible DST) + .addRoundTrip("timestamp(0)", "TIMESTAMP '2017-01-01'", createTimestampType(0), "TIMESTAMP '2017-01-01'") // winter on northern hemisphere (possible DST on southern hemisphere) + .addRoundTrip("timestamp(0)", "TIMESTAMP '1970-01-01'", createTimestampType(0), "TIMESTAMP '1970-01-01'") // change forward at midnight in JVM + .addRoundTrip("timestamp(0)", "TIMESTAMP '1983-04-01'", createTimestampType(0), "TIMESTAMP '1983-04-01'") // change forward at midnight in Vilnius + .addRoundTrip("timestamp(0)", "TIMESTAMP '1983-10-01'", createTimestampType(0), "TIMESTAMP '1983-10-01'") // change backward at midnight in Vilnius + .addRoundTrip("timestamp(0)", "TIMESTAMP '9999-12-31'", createTimestampType(0), "TIMESTAMP '9999-12-31'") // max value in Exasol + .execute(getQueryRunner(), session, exasolCreateAndInsert(TEST_SCHEMA + "." + "test_timestamp")); + } + @Test void testHashtype() { diff --git a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java index 5b01fc786e39..b3021a8b2f43 100644 --- a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java +++ b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java @@ -47,7 +47,7 @@ public class TestingExasolServer public TestingExasolServer() { - container = new ExasolContainer<>("8.32.0") + container = new ExasolContainer<>("8.34.0") .withExposedPorts(8563) .withRequiredServices(ExasolService.JDBC); cleanup = startOrReuse(container); diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java index 0777b207d9e9..27fb250a8eca 100644 --- a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java @@ -56,7 +56,7 @@ public void extendEnvironment(Builder builder) private DockerContainer createExasol() { - DockerContainer container = new DockerContainer("exasol/docker-db:8.27.0", "exasol") + DockerContainer container = new DockerContainer("exasol/docker-db:8.34.0", "exasol") .withStartupCheckStrategy(new IsRunningStartupCheckStrategy()) .waitingFor(forSelectedPorts(EXASOL_PORT)); container.setPrivilegedMode(true); From 8c84f65f7c92ac38cfc7b735b57315c31a06c2f0 Mon Sep 17 00:00:00 2001 From: "mykhailo.skliar" Date: Thu, 17 Jul 2025 19:46:12 +0200 Subject: [PATCH 2/4] Add support for TIMESTAMP and TIMESTAMP WITH TIME ZONE types in Exasol connector - implemented additional timestamp with precision data type for ExasolClient - added correspondent tests for timestamp with precision data type - started implementation of additional timestamp with local time zone data type for ExasolClient - updated exasol db version to 8.34.0 in integration tests - finished implementation of additional timestamp with local time zone data type for ExasolClient - implemented special addRoundTrip method with additional column expression parameter for the predicate to fix the test predicate assumption - all timestamp with timezone tests use input literal timestamp string, which is interpreted as a JVM timestamp string ("America/Bahia_Banderas") but expected literal strings are interpreted as UTC strings. Therefore the difference between these timestamps is 6 hours (with DST) or 5 (without DST); for some historical values, like 1970 for example, the difference can be 7, which is expected behaviour for timestamp with local time zone - implemented ColumnMapper, TimestampColumnMapper and TimestampWithTimeZoneColumnMapper to encapsulate mapping logic in separate classes and prevent ExasolClient from becoming too big - removed mapper classes and moved the logic to ExasolClient, based on the PR suggesions - removed unnecessary javadocs - improved javadocs and comments for testing timestamp with time zone by explaining why only jvm time zone is currently used for testing timestamp with time zone - removed using * import in ExasolClient based on the PR suggestions - removed TestExasolTimestampMapping and moved testing timestamp and timestamp with timezone to TestExasolTypeMapping, based on the PR suggestions - fixed checkstyle violations after enabling checkstyle plugin --- .../io/trino/plugin/exasol/ExasolClient.java | 296 ++++++++++++++++++ .../exasol/TestExasolConnectorTest.java | 32 +- .../plugin/exasol/TestExasolTypeMapping.java | 138 ++++++++ .../plugin/exasol/TestingExasolServer.java | 2 +- .../env/environment/EnvMultinodeExasol.java | 2 +- 5 files changed, 452 insertions(+), 18 deletions(-) diff --git a/plugin/trino-exasol/src/main/java/io/trino/plugin/exasol/ExasolClient.java b/plugin/trino-exasol/src/main/java/io/trino/plugin/exasol/ExasolClient.java index 434c3454e5f2..d8057d977b13 100644 --- a/plugin/trino-exasol/src/main/java/io/trino/plugin/exasol/ExasolClient.java +++ b/plugin/trino-exasol/src/main/java/io/trino/plugin/exasol/ExasolClient.java @@ -28,6 +28,8 @@ import io.trino.plugin.jdbc.JdbcTypeHandle; import io.trino.plugin.jdbc.LongReadFunction; import io.trino.plugin.jdbc.LongWriteFunction; +import io.trino.plugin.jdbc.ObjectReadFunction; +import io.trino.plugin.jdbc.ObjectWriteFunction; import io.trino.plugin.jdbc.QueryBuilder; import io.trino.plugin.jdbc.WriteFunction; import io.trino.plugin.jdbc.WriteMapping; @@ -39,42 +41,81 @@ import io.trino.spi.connector.ColumnPosition; import io.trino.spi.connector.ConnectorSession; import io.trino.spi.connector.ConnectorTableMetadata; +import io.trino.spi.type.LongTimestamp; +import io.trino.spi.type.LongTimestampWithTimeZone; +import io.trino.spi.type.TimestampType; +import io.trino.spi.type.TimestampWithTimeZoneType; import io.trino.spi.type.Type; import java.sql.Connection; import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; import java.sql.Types; +import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; import java.util.Set; +import static com.google.common.base.Preconditions.checkArgument; +import static io.trino.plugin.jdbc.PredicatePushdownController.FULL_PUSHDOWN; import static io.trino.plugin.jdbc.StandardColumnMappings.bigintColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.booleanColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.decimalColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.defaultCharColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.defaultVarcharColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.doubleColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.fromLongTrinoTimestamp; +import static io.trino.plugin.jdbc.StandardColumnMappings.fromTrinoTimestamp; import static io.trino.plugin.jdbc.StandardColumnMappings.integerColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.smallintColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.toLongTrinoTimestamp; +import static io.trino.plugin.jdbc.StandardColumnMappings.toTrinoTimestamp; import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling; import static io.trino.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR; import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; import static io.trino.spi.connector.ConnectorMetadata.MODIFYING_ROWS_MESSAGE; +import static io.trino.spi.type.DateTimeEncoding.packDateTimeWithZone; +import static io.trino.spi.type.DateTimeEncoding.unpackMillisUtc; import static io.trino.spi.type.DateType.DATE; import static io.trino.spi.type.DecimalType.createDecimalType; +import static io.trino.spi.type.TimeZoneKey.UTC_KEY; +import static io.trino.spi.type.TimestampType.createTimestampType; +import static io.trino.spi.type.TimestampWithTimeZoneType.createTimestampWithTimeZoneType; +import static io.trino.spi.type.Timestamps.PICOSECONDS_PER_NANOSECOND; +import static java.lang.String.format; +import static java.time.ZoneOffset.UTC; import static java.util.Locale.ENGLISH; public class ExasolClient extends BaseJdbcClient { + private static final int EXASOL_TIMESTAMP_WITH_TIMEZONE = 124; + private static final Set INTERNAL_SCHEMAS = ImmutableSet.builder() .add("EXA_STATISTICS") .add("SYS") .build(); + private static final int MAX_EXASOL_TIMESTAMP_PRECISION = 9; + + private static final DateTimeFormatter TIMESTAMP_NANO_OPTIONAL_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("uuuu-MM-dd HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) + .optionalEnd() + .toFormatter(); + @Inject public ExasolClient( BaseJdbcConfig config, @@ -229,6 +270,10 @@ public Optional toColumnMapping(ConnectorSession session, Connect return Optional.of(defaultVarcharColumnMapping(typeHandle.requiredColumnSize(), true)); case Types.DATE: return Optional.of(dateColumnMapping()); + case Types.TIMESTAMP: + return Optional.of(timestampColumnMapping(typeHandle)); + case EXASOL_TIMESTAMP_WITH_TIMEZONE: + return Optional.of(timestampWithTimeZoneColumnMapping(typeHandle)); } if (getUnsupportedTypeHandling(session) == CONVERT_TO_VARCHAR) { @@ -237,6 +282,257 @@ public Optional toColumnMapping(ConnectorSession session, Connect return Optional.empty(); } + private static ColumnMapping timestampColumnMapping(JdbcTypeHandle typeHandle) + { + int timestampPrecision = typeHandle.requiredDecimalDigits(); + TimestampType timestampType = createTimestampType(timestampPrecision); + if (timestampType.isShort()) { + return ColumnMapping.longMapping( + timestampType, + longTimestampReadFunction(timestampType), + longTimestampWriteFunction(timestampType), + FULL_PUSHDOWN); + } + return ColumnMapping.objectMapping( + timestampType, + objectTimestampReadFunction(timestampType), + objectTimestampWriteFunction(timestampType), + FULL_PUSHDOWN); + } + + private static LongReadFunction longTimestampReadFunction(TimestampType timestampType) + { + return (resultSet, columnIndex) -> { + Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class); + return toTrinoTimestamp(timestampType, timestamp.toLocalDateTime()); + }; + } + + private static ObjectReadFunction objectTimestampReadFunction(TimestampType timestampType) + { + verifyObjectTimestampPrecision(timestampType); + return ObjectReadFunction.of( + LongTimestamp.class, + (resultSet, columnIndex) -> { + Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class); + return toLongTrinoTimestamp(timestampType, timestamp.toLocalDateTime()); + }); + } + + private static void verifyObjectTimestampPrecision(TimestampType timestampType) + { + int precision = timestampType.getPrecision(); + checkArgument(precision > TimestampType.MAX_SHORT_PRECISION && precision <= MAX_EXASOL_TIMESTAMP_PRECISION, + "Precision is out of range: %s", precision); + } + + private static ObjectWriteFunction objectTimestampWriteFunction(TimestampType timestampType) + { + int precision = timestampType.getPrecision(); + verifyObjectTimestampPrecision(timestampType); + + return new ObjectWriteFunction() { + @Override + public Class getJavaType() + { + return LongTimestamp.class; + } + + @Override + public void set(PreparedStatement statement, int index, Object value) + throws SQLException + { + LocalDateTime timestamp = fromLongTrinoTimestamp((LongTimestamp) value, precision); + statement.setString(index, TIMESTAMP_NANO_OPTIONAL_FORMATTER.format(timestamp)); + } + + @Override + public String getBindExpression() + { + return getTimestampBindExpression(precision); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static LongWriteFunction longTimestampWriteFunction(TimestampType timestampType) + { + return new LongWriteFunction() + { + @Override + public String getBindExpression() + { + return getTimestampBindExpression(timestampType.getPrecision()); + } + + @Override + public void set(PreparedStatement statement, int index, long epochMicros) + throws SQLException + { + LocalDateTime localDateTime = fromTrinoTimestamp(epochMicros); + Timestamp timestampValue = Timestamp.valueOf(localDateTime); + statement.setObject(index, timestampValue); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static ColumnMapping timestampWithTimeZoneColumnMapping(JdbcTypeHandle typeHandle) + { + int timestampPrecision = typeHandle.requiredDecimalDigits(); + TimestampWithTimeZoneType timestampWithTimeZoneType = createTimestampWithTimeZoneType(timestampPrecision); + + if (timestampWithTimeZoneType.isShort()) { + return ColumnMapping.longMapping( + timestampWithTimeZoneType, + longTimestampWithTimeZoneReadFunction(), + longTimestampWithTimeZoneWriteFunction(timestampWithTimeZoneType), + FULL_PUSHDOWN); + } + return ColumnMapping.objectMapping( + timestampWithTimeZoneType, + objectTimestampWithTimeZoneReadFunction(timestampWithTimeZoneType), + objectTimestampWithTimeZoneWriteFunction(timestampWithTimeZoneType), + FULL_PUSHDOWN); + } + + private static LongReadFunction longTimestampWithTimeZoneReadFunction() + { + return (resultSet, columnIndex) -> { + Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class); + return packDateTimeWithZone(timestamp.getTime(), UTC_KEY); + }; + } + + private static ObjectReadFunction objectTimestampWithTimeZoneReadFunction( + TimestampWithTimeZoneType timestampType) + { + verifyObjectTimestampWithTimeZonePrecision(timestampType); + return ObjectReadFunction.of( + LongTimestampWithTimeZone.class, + (resultSet, columnIndex) -> { + Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class); + if (timestamp == null) { + return null; + } + + long millisUtc = timestamp.getTime(); + long nanosUtc = millisUtc * 1_000_000L + timestamp.getNanos() % 1_000_000; + int picosOfMilli = (int) ((nanosUtc - millisUtc * 1_000_000) * 1_000); + + return LongTimestampWithTimeZone.fromEpochMillisAndFraction( + millisUtc, + picosOfMilli, + UTC_KEY); + }); + } + + private static void verifyObjectTimestampWithTimeZonePrecision(TimestampWithTimeZoneType timestampType) + { + int precision = timestampType.getPrecision(); + checkArgument(precision > TimestampWithTimeZoneType.MAX_SHORT_PRECISION && precision <= MAX_EXASOL_TIMESTAMP_PRECISION, + "Precision is out of range: %s", precision); + } + + private static ObjectWriteFunction objectTimestampWithTimeZoneWriteFunction(TimestampWithTimeZoneType timestampType) + { + int precision = timestampType.getPrecision(); + verifyObjectTimestampWithTimeZonePrecision(timestampType); + + return new ObjectWriteFunction() { + @Override + public Class getJavaType() + { + return LongTimestampWithTimeZone.class; + } + + @Override + public void set(PreparedStatement statement, int index, Object value) + throws SQLException + { + if (value == null) { + statement.setNull(index, Types.VARCHAR); + return; + } + + LongTimestampWithTimeZone timestampValue = (LongTimestampWithTimeZone) value; + + long epochMillis = timestampValue.getEpochMillis(); + int picosOfMilli = timestampValue.getPicosOfMilli(); + int nanos = picosOfMilli / PICOSECONDS_PER_NANOSECOND; + + Instant instant = Instant.ofEpochMilli(epochMillis).plusNanos(nanos); + ZonedDateTime zonedDateTime = instant.atZone(UTC); + + String formatted = TIMESTAMP_NANO_OPTIONAL_FORMATTER.format(zonedDateTime); + statement.setString(index, formatted); + } + + @Override + public String getBindExpression() + { + return getTimestampBindExpression(precision); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static LongWriteFunction longTimestampWithTimeZoneWriteFunction(TimestampWithTimeZoneType timestampType) + { + return new LongWriteFunction() + { + @Override + public String getBindExpression() + { + return getTimestampBindExpression(timestampType.getPrecision()); + } + + @Override + public void set(PreparedStatement statement, int index, long epochMicros) + throws SQLException + { + Instant instant = Instant.ofEpochMilli(unpackMillisUtc(epochMicros)); + LocalDateTime localDateTime = instant.atZone(ZoneOffset.UTC).toLocalDateTime(); + statement.setString(index, TIMESTAMP_NANO_OPTIONAL_FORMATTER.format(localDateTime)); + } + + @Override + public void setNull(PreparedStatement statement, int index) + throws SQLException + { + statement.setNull(index, Types.VARCHAR); + } + }; + } + + private static String getTimestampBindExpression(int precision) + { + if (precision <= 0) { + return "TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS')"; + } + else { + return format("TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS.FF%d')", precision); + } + } + private static ColumnMapping dateColumnMapping() { // Exasol driver does not support LocalDate diff --git a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolConnectorTest.java b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolConnectorTest.java index 2d005a115de0..78bd60624391 100644 --- a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolConnectorTest.java +++ b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolConnectorTest.java @@ -61,25 +61,25 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) return switch (connectorBehavior) { // Tests requires write access which is not implemented case SUPPORTS_AGGREGATION_PUSHDOWN, - SUPPORTS_JOIN_PUSHDOWN, - SUPPORTS_LIMIT_PUSHDOWN, - SUPPORTS_TOPN_PUSHDOWN -> false; + SUPPORTS_JOIN_PUSHDOWN, + SUPPORTS_LIMIT_PUSHDOWN, + SUPPORTS_TOPN_PUSHDOWN -> false; // Parallel writing is not supported due to restrictions of the Exasol JDBC driver. case SUPPORTS_ADD_COLUMN, - SUPPORTS_ARRAY, - SUPPORTS_COMMENT_ON_TABLE, - SUPPORTS_CREATE_SCHEMA, - SUPPORTS_CREATE_TABLE, - SUPPORTS_DELETE, - SUPPORTS_INSERT, - SUPPORTS_MAP_TYPE, - SUPPORTS_NEGATIVE_DATE, // min date is 0001-01-01 - SUPPORTS_RENAME_COLUMN, - SUPPORTS_RENAME_TABLE, - SUPPORTS_ROW_TYPE, - SUPPORTS_SET_COLUMN_TYPE, - SUPPORTS_UPDATE -> false; + SUPPORTS_ARRAY, + SUPPORTS_COMMENT_ON_TABLE, + SUPPORTS_CREATE_SCHEMA, + SUPPORTS_CREATE_TABLE, + SUPPORTS_DELETE, + SUPPORTS_INSERT, + SUPPORTS_MAP_TYPE, + SUPPORTS_NEGATIVE_DATE, // min date is 0001-01-01 + SUPPORTS_RENAME_COLUMN, + SUPPORTS_RENAME_TABLE, + SUPPORTS_ROW_TYPE, + SUPPORTS_SET_COLUMN_TYPE, + SUPPORTS_UPDATE -> false; default -> super.hasBehavior(connectorBehavior); }; diff --git a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java index b049119cfc66..d3045a1748e1 100644 --- a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java +++ b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java @@ -39,6 +39,8 @@ import static io.trino.spi.type.DateType.DATE; import static io.trino.spi.type.DecimalType.createDecimalType; import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.TimestampType.createTimestampType; +import static io.trino.spi.type.TimestampWithTimeZoneType.createTimestampWithTimeZoneType; import static io.trino.spi.type.VarcharType.createVarcharType; import static java.lang.String.format; import static java.time.ZoneOffset.UTC; @@ -275,6 +277,142 @@ private void testDate(ZoneId sessionZone) .execute(getQueryRunner(), session, exasolCreateAndInsert(TEST_SCHEMA + "." + "test_date")); } + @Test + void testTimestamp() + { + testTimestamp(UTC); + testTimestamp(jvmZone); + // using two non-JVM zones so that we don't need to worry what Exasol system zone is + testTimestamp(vilnius); + testTimestamp(kathmandu); + testTimestamp(TestingSession.DEFAULT_TIME_ZONE_KEY.getZoneId()); + } + + @Test + public void testTimestampWithTimeZone() + { + //Only JVM Time Zone is currently used for testing timestamp with time zone + //Adding test cases for other time zones would require improving the test to forcibly apply session changes for timestamp with timezone + testTimestampWithTimeZone(ZoneId.systemDefault()); + } + + @Test + public void testUnsupportedTimestampValues() + { + try (TestTable table = new TestTable(exasolServer::execute, "tpch.test_unsupported_timestamp", "(col TIMESTAMP)")) { + // Too early + assertExasolQueryFails( + "INSERT INTO " + table.getName() + " VALUES (TIMESTAMP '0000-12-31 23:59:59.999999')", + "data exception - invalid date value"); + + // Too late + assertExasolQueryFails( + "INSERT INTO " + table.getName() + " VALUES (TIMESTAMP '10000-01-01 00:00:00.000000')", + "data exception - invalid character value for cast"); + + // Precision > 9 + assertExasolQueryFails( + "INSERT INTO " + table.getName() + " VALUES (TIMESTAMP '2024-01-01 12:34:56.1234567890')", + "data exception - invalid character value for cast"); + } + } + + private void assertExasolQueryFails(String sql, String expectedMessage) + { + assertThatThrownBy(() -> exasolServer.execute(sql)) + .cause() + .hasMessageContaining(expectedMessage); + } + + /** + *

+ * Important timezone note: The input timestamp string literal is interpreted as a timestamp + * in the current JVM time zone ("America/Bahia_Banderas" in this test environment). + * The expected timestamp literal is interpreted as a UTC timestamp. + * Because of this, the expected literal is offset by 6 hours (during daylight saving time), + * 5 hours (outside DST), or 7 hours for some historical dates in 1970 due to timezone rules. + * This difference is intentional and verifies correct conversion between zones. + *

+ */ + private void testTimestampWithTimeZone(ZoneId sessionZone) + { + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) + .build(); + + SqlDataTypeTest test = SqlDataTypeTest.create() + // timestamp with precision 3 examples + .addRoundTrip("cast(col_0 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2019-03-18 10:01:17.987'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2019-03-18 16:01:17.987 UTC'") + .addRoundTrip("cast(col_1 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2018-10-28 01:33:17.456'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2018-10-28 07:33:17.456 UTC'") + .addRoundTrip("cast(col_2 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2018-10-28 03:33:33.333'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2018-10-28 09:33:33.333 UTC'") + .addRoundTrip("cast(col_3 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '1970-01-01 00:13:42.000'", createTimestampWithTimeZoneType(3), "TIMESTAMP '1970-01-01 07:13:42.000 UTC'") + .addRoundTrip("cast(col_4 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2018-04-01 02:13:55.123'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2018-04-01 08:13:55.123 UTC'") + .addRoundTrip("cast(col_5 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.999'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2020-09-27 17:34:56.999 UTC'") + .addRoundTrip("cast(col_6 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2018-03-25 03:17:17.000'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2018-03-25 09:17:17.000 UTC'") + .addRoundTrip("cast(col_7 AT TIME ZONE 'UTC' AS timestamp with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '1986-01-01 00:13:07.000'", createTimestampWithTimeZoneType(3), "TIMESTAMP '1986-01-01 07:13:07.000 UTC'") + + // timestamp with precision 6 examples + .addRoundTrip("cast(col_8 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2019-03-18 10:01:17.987654'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2019-03-18 16:01:17.987654 UTC'") + .addRoundTrip("cast(col_9 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2018-10-28 01:33:17.456789'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2018-10-28 07:33:17.456789 UTC'") + .addRoundTrip("cast(col_10 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2018-10-28 03:33:33.333333'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2018-10-28 09:33:33.333333 UTC'") + .addRoundTrip("cast(col_11 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '1970-01-01 00:13:42.000000'", createTimestampWithTimeZoneType(6), "TIMESTAMP '1970-01-01 07:13:42.000000 UTC'") + .addRoundTrip("cast(col_12 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2018-04-01 02:13:55.123456'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2018-04-01 08:13:55.123456 UTC'") + .addRoundTrip("cast(col_13 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2018-03-25 03:17:17.000000'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2018-03-25 09:17:17.000000 UTC'") + .addRoundTrip("cast(col_14 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '1986-01-01 00:13:07.000000'", createTimestampWithTimeZoneType(6), "TIMESTAMP '1986-01-01 07:13:07.000000 UTC'") + + // tests for other precisions (0-5 and some 1's) + .addRoundTrip("cast(col_15 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(0) with local time zone", "TIMESTAMP '1970-01-01 00:00:01'", createTimestampWithTimeZoneType(0), "TIMESTAMP '1970-01-01 07:00:01 UTC'") + .addRoundTrip("cast(col_16 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(1) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.1'", createTimestampWithTimeZoneType(1), "TIMESTAMP '1970-01-01 07:00:01.1 UTC'") + .addRoundTrip("cast(col_17 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(1) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.9'", createTimestampWithTimeZoneType(1), "TIMESTAMP '1970-01-01 07:00:01.9 UTC'") + .addRoundTrip("cast(col_18 AT TIME ZONE 'UTC' AS timestamp(2) with time zone)", "timestamp(2) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.12'", createTimestampWithTimeZoneType(2), "TIMESTAMP '1970-01-01 07:00:01.12 UTC'") + .addRoundTrip("cast(col_19 AT TIME ZONE 'UTC' AS timestamp(3) with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.123'", createTimestampWithTimeZoneType(3), "TIMESTAMP '1970-01-01 07:00:01.123 UTC'") + .addRoundTrip("cast(col_20 AT TIME ZONE 'UTC' AS timestamp(3) with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.999'", createTimestampWithTimeZoneType(3), "TIMESTAMP '1970-01-01 07:00:01.999 UTC'") + .addRoundTrip("cast(col_21 AT TIME ZONE 'UTC' AS timestamp(4) with time zone)", "timestamp(4) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.1234'", createTimestampWithTimeZoneType(4), "TIMESTAMP '1970-01-01 07:00:01.1234 UTC'") + .addRoundTrip("cast(col_22 AT TIME ZONE 'UTC' AS timestamp(5) with time zone)", "timestamp(5) with local time zone", "TIMESTAMP '1970-01-01 00:00:01.12345'", createTimestampWithTimeZoneType(5), "TIMESTAMP '1970-01-01 07:00:01.12345 UTC'") + .addRoundTrip("cast(col_23 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(1) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.1'", createTimestampWithTimeZoneType(1), "TIMESTAMP '2020-09-27 17:34:56.1 UTC'") + .addRoundTrip("cast(col_24 AT TIME ZONE 'UTC' AS timestamp(1) with time zone)", "timestamp(1) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.9'", createTimestampWithTimeZoneType(1), "TIMESTAMP '2020-09-27 17:34:56.9 UTC'") + .addRoundTrip("cast(col_25 AT TIME ZONE 'UTC' AS timestamp(3) with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.123'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2020-09-27 17:34:56.123 UTC'") + .addRoundTrip("cast(col_26 AT TIME ZONE 'UTC' AS timestamp(3) with time zone)", "timestamp(3) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.999'", createTimestampWithTimeZoneType(3), "TIMESTAMP '2020-09-27 17:34:56.999 UTC'") + .addRoundTrip("cast(col_27 AT TIME ZONE 'UTC' AS timestamp(6) with time zone)", "timestamp(6) with local time zone", "TIMESTAMP '2020-09-27 12:34:56.123456'", createTimestampWithTimeZoneType(6), "TIMESTAMP '2020-09-27 17:34:56.123456 UTC'"); + + test.execute(getQueryRunner(), session, exasolCreateAndInsert(TEST_SCHEMA + "." + "test_timestamp")); + } + + private void testTimestamp(ZoneId sessionZone) + { + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) + .build(); + + SqlDataTypeTest.create() + .addRoundTrip("timestamp", "NULL", createTimestampType(3), "CAST(NULL AS TIMESTAMP)") + .addRoundTrip("timestamp", "TIMESTAMP '2013-03-11 17:30:15.123'", createTimestampType(3), "TIMESTAMP '2013-03-11 17:30:15.123'") + .addRoundTrip("timestamp", "TIMESTAMP '2018-10-28 01:33:17.456'", createTimestampType(3), "TIMESTAMP '2018-10-28 01:33:17.456'") + .addRoundTrip("timestamp", "TIMESTAMP '2018-10-28 03:33:33.333'", createTimestampType(3), "TIMESTAMP '2018-10-28 03:33:33.333'") + .addRoundTrip("timestamp", "TIMESTAMP '1970-01-01 00:13:42.000'", createTimestampType(3), "TIMESTAMP '1970-01-01 00:13:42.000'") + .addRoundTrip("timestamp", "TIMESTAMP '2018-04-01 03:13:55.123'", createTimestampType(3), "TIMESTAMP '2018-04-01 03:13:55.123'") + .addRoundTrip("timestamp", "TIMESTAMP '2020-09-27 12:34:56.999'", createTimestampType(3), "TIMESTAMP '2020-09-27 12:34:56.999'") + .addRoundTrip("timestamp", "TIMESTAMP '2018-03-25 03:17:17.000'", createTimestampType(3), "TIMESTAMP '2018-03-25 03:17:17.000'") + .addRoundTrip("timestamp", "TIMESTAMP '1986-01-01 00:13:07.000'", createTimestampType(3), "TIMESTAMP '1986-01-01 00:13:07.000'") + .addRoundTrip("timestamp(6)", "TIMESTAMP '2013-03-11 17:30:15.123456'", createTimestampType(6), "TIMESTAMP '2013-03-11 17:30:15.123456'") + .addRoundTrip("timestamp(9)", "TIMESTAMP '2013-03-11 17:30:15.123456789'", createTimestampType(9), "TIMESTAMP '2013-03-11 17:30:15.123456789'") + .addRoundTrip("timestamp(1)", "TIMESTAMP '2016-08-19 19:28:05.0'", createTimestampType(1), "TIMESTAMP '2016-08-19 19:28:05.0'") + .addRoundTrip("timestamp(2)", "TIMESTAMP '2016-08-19 19:28:05.01'", createTimestampType(2), "TIMESTAMP '2016-08-19 19:28:05.01'") + .addRoundTrip("timestamp", "TIMESTAMP '3030-03-03 12:34:56.123'", createTimestampType(3), "TIMESTAMP '3030-03-03 12:34:56.123'") + .addRoundTrip("timestamp(5)", "TIMESTAMP '3030-03-03 12:34:56.12345'", createTimestampType(5), "TIMESTAMP '3030-03-03 12:34:56.12345'") + .addRoundTrip("timestamp(9)", "TIMESTAMP '3030-03-03 12:34:56.123456789'", createTimestampType(9), "TIMESTAMP '3030-03-03 12:34:56.123456789'") + .addRoundTrip("timestamp", "TIMESTAMP '3030-03-03 12:34:56.123'", createTimestampType(3), "TIMESTAMP '3030-03-03 12:34:56.123'") + .addRoundTrip("timestamp(5)", "TIMESTAMP '3030-03-03 12:34:56.12345'", createTimestampType(5), "TIMESTAMP '3030-03-03 12:34:56.12345'") + .addRoundTrip("timestamp(9)", "TIMESTAMP '3030-03-03 12:34:56.123456789'", createTimestampType(9), "TIMESTAMP '3030-03-03 12:34:56.123456789'") + .addRoundTrip("timestamp(0)", "TIMESTAMP '2017-07-01'", createTimestampType(0), "TIMESTAMP '2017-07-01'") // summer on northern hemisphere (possible DST) + .addRoundTrip("timestamp(0)", "TIMESTAMP '2017-01-01'", createTimestampType(0), "TIMESTAMP '2017-01-01'") // winter on northern hemisphere (possible DST on southern hemisphere) + .addRoundTrip("timestamp(0)", "TIMESTAMP '1970-01-01'", createTimestampType(0), "TIMESTAMP '1970-01-01'") // change forward at midnight in JVM + .addRoundTrip("timestamp(0)", "TIMESTAMP '1983-04-01'", createTimestampType(0), "TIMESTAMP '1983-04-01'") // change forward at midnight in Vilnius + .addRoundTrip("timestamp(0)", "TIMESTAMP '1983-10-01'", createTimestampType(0), "TIMESTAMP '1983-10-01'") // change backward at midnight in Vilnius + .addRoundTrip("timestamp(0)", "TIMESTAMP '9999-12-31'", createTimestampType(0), "TIMESTAMP '9999-12-31'") // max value in Exasol + .execute(getQueryRunner(), session, exasolCreateAndInsert(TEST_SCHEMA + "." + "test_timestamp")); + } + @Test void testHashtype() { diff --git a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java index 5b01fc786e39..b3021a8b2f43 100644 --- a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java +++ b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java @@ -47,7 +47,7 @@ public class TestingExasolServer public TestingExasolServer() { - container = new ExasolContainer<>("8.32.0") + container = new ExasolContainer<>("8.34.0") .withExposedPorts(8563) .withRequiredServices(ExasolService.JDBC); cleanup = startOrReuse(container); diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java index 0777b207d9e9..27fb250a8eca 100644 --- a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java @@ -56,7 +56,7 @@ public void extendEnvironment(Builder builder) private DockerContainer createExasol() { - DockerContainer container = new DockerContainer("exasol/docker-db:8.27.0", "exasol") + DockerContainer container = new DockerContainer("exasol/docker-db:8.34.0", "exasol") .withStartupCheckStrategy(new IsRunningStartupCheckStrategy()) .waitingFor(forSelectedPorts(EXASOL_PORT)); container.setPrivilegedMode(true); From 93f7eef39279e75aa53d8e0f20560bbbd72d9f22 Mon Sep 17 00:00:00 2001 From: "mykhailo.skliar" Date: Tue, 22 Jul 2025 11:18:34 +0200 Subject: [PATCH 3/4] Add support for TIMESTAMP types in Exasol connector - implemented additional timestamp with precision data type for ExasolClient - added correspondent tests for timestamp with precision data type - implemented additional timestamp with local time zone data type for ExasolClient - implemented special addRoundTrip method with additional column expression parameter for the predicate to fix the test predicate assumption - all timestamp with timezone tests use input literal timestamp string, which is interpreted as a JVM timestamp string ("America/Bahia_Banderas") but expected literal strings are interpreted as UTC strings. Therefore the difference between these timestamps is 6 hours (with DST) or 5 (without DST); for some historical values, like 1970 for example, the difference can be 7, which is expected behaviour for timestamp with local time zone - removed mapper classes and moved the logic to ExasolClient, based on the PR suggesions - removed unnecessary javadocs - improved javadocs and comments for testing timestamp with time zone by explaining why only jvm time zone is currently used for testing timestamp with time zone - removed using * import in ExasolClient based on the PR suggestions - removed TestExasolTimestampMapping and moved testing timestamp and timestamp with timezone to TestExasolTypeMapping, based on the PR suggestions --- .../java/io/trino/plugin/exasol/TestExasolTypeMapping.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java index d3045a1748e1..cd58baf2ce69 100644 --- a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java +++ b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestExasolTypeMapping.java @@ -289,7 +289,7 @@ void testTimestamp() } @Test - public void testTimestampWithTimeZone() + void testTimestampWithTimeZone() { //Only JVM Time Zone is currently used for testing timestamp with time zone //Adding test cases for other time zones would require improving the test to forcibly apply session changes for timestamp with timezone @@ -297,7 +297,7 @@ public void testTimestampWithTimeZone() } @Test - public void testUnsupportedTimestampValues() + void testUnsupportedTimestampValues() { try (TestTable table = new TestTable(exasolServer::execute, "tpch.test_unsupported_timestamp", "(col TIMESTAMP)")) { // Too early From 68ec52e491a7a1a05fbcecc6ec3704173f5ab268 Mon Sep 17 00:00:00 2001 From: "mykhailo.skliar" Date: Tue, 22 Jul 2025 11:35:50 +0200 Subject: [PATCH 4/4] Update exasol db version to 8.34.0 in tests --- .../test/java/io/trino/plugin/exasol/TestingExasolServer.java | 2 +- .../product/launcher/env/environment/EnvMultinodeExasol.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java index 5b01fc786e39..b3021a8b2f43 100644 --- a/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java +++ b/plugin/trino-exasol/src/test/java/io/trino/plugin/exasol/TestingExasolServer.java @@ -47,7 +47,7 @@ public class TestingExasolServer public TestingExasolServer() { - container = new ExasolContainer<>("8.32.0") + container = new ExasolContainer<>("8.34.0") .withExposedPorts(8563) .withRequiredServices(ExasolService.JDBC); cleanup = startOrReuse(container); diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java index 0777b207d9e9..27fb250a8eca 100644 --- a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/EnvMultinodeExasol.java @@ -56,7 +56,7 @@ public void extendEnvironment(Builder builder) private DockerContainer createExasol() { - DockerContainer container = new DockerContainer("exasol/docker-db:8.27.0", "exasol") + DockerContainer container = new DockerContainer("exasol/docker-db:8.34.0", "exasol") .withStartupCheckStrategy(new IsRunningStartupCheckStrategy()) .waitingFor(forSelectedPorts(EXASOL_PORT)); container.setPrivilegedMode(true);