diff --git a/plugin/trino-base-jdbc/src/main/java/io/trino/plugin/jdbc/StandardColumnMappings.java b/plugin/trino-base-jdbc/src/main/java/io/trino/plugin/jdbc/StandardColumnMappings.java index 123c062c28eb..576d7088514f 100644 --- a/plugin/trino-base-jdbc/src/main/java/io/trino/plugin/jdbc/StandardColumnMappings.java +++ b/plugin/trino-base-jdbc/src/main/java/io/trino/plugin/jdbc/StandardColumnMappings.java @@ -20,6 +20,7 @@ import io.trino.spi.type.CharType; import io.trino.spi.type.DecimalType; import io.trino.spi.type.Decimals; +import io.trino.spi.type.LongTimestamp; import io.trino.spi.type.TimeType; import io.trino.spi.type.TimestampType; import io.trino.spi.type.Type; @@ -62,7 +63,6 @@ import static io.trino.spi.type.RealType.REAL; import static io.trino.spi.type.SmallintType.SMALLINT; import static io.trino.spi.type.TimeType.TIME; -import static io.trino.spi.type.TimestampType.MAX_SHORT_PRECISION; import static io.trino.spi.type.TimestampType.TIMESTAMP_MILLIS; import static io.trino.spi.type.Timestamps.MICROSECONDS_PER_SECOND; import static io.trino.spi.type.Timestamps.NANOSECONDS_PER_DAY; @@ -91,6 +91,8 @@ public final class StandardColumnMappings { + private static final int MAX_LOCAL_DATE_TIME_PRECISION = 9; + private StandardColumnMappings() {} public static ColumnMapping booleanColumnMapping() @@ -439,7 +441,7 @@ public static LongWriteFunction timeWriteFunction(int precision) public static ColumnMapping timestampColumnMappingUsingSqlTimestamp(TimestampType timestampType) { // TODO support higher precision - checkArgument(timestampType.getPrecision() <= MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); + checkArgument(timestampType.getPrecision() <= TimestampType.MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); return ColumnMapping.longMapping( timestampType, (resultSet, columnIndex) -> { @@ -457,19 +459,34 @@ public static ColumnMapping timestampColumnMapping() public static ColumnMapping timestampColumnMapping(TimestampType timestampType) { - checkArgument(timestampType.getPrecision() <= MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); - return ColumnMapping.longMapping( + if (timestampType.getPrecision() <= TimestampType.MAX_SHORT_PRECISION) { + return ColumnMapping.longMapping( + timestampType, + timestampReadFunction(timestampType), + timestampWriteFunction(timestampType)); + } + checkArgument(timestampType.getPrecision() <= MAX_LOCAL_DATE_TIME_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); + return ColumnMapping.objectMapping( timestampType, - timestampReadFunction(timestampType), - timestampWriteFunction(timestampType)); + longTimestampReadFunction(timestampType), + longTimestampWriteFunction(timestampType)); } public static LongReadFunction timestampReadFunction(TimestampType timestampType) { - checkArgument(timestampType.getPrecision() <= MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); + checkArgument(timestampType.getPrecision() <= TimestampType.MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); return (resultSet, columnIndex) -> toPrestoTimestamp(timestampType, resultSet.getObject(columnIndex, LocalDateTime.class)); } + private static ObjectReadFunction longTimestampReadFunction(TimestampType timestampType) + { + checkArgument(timestampType.getPrecision() > TimestampType.MAX_SHORT_PRECISION && timestampType.getPrecision() < MAX_LOCAL_DATE_TIME_PRECISION, + "Precision is out of range: %s", timestampType.getPrecision()); + return ObjectReadFunction.of( + LongTimestamp.class, + (resultSet, columnIndex) -> toLongTimestamp(timestampType, resultSet.getObject(columnIndex, LocalDateTime.class))); + } + /** * @deprecated This method uses {@link java.sql.Timestamp} and the class cannot represent date-time value when JVM zone had * forward offset change (a 'gap'). This includes regular DST changes (e.g. Europe/Warsaw) and one-time policy changes @@ -479,22 +496,51 @@ public static LongReadFunction timestampReadFunction(TimestampType timestampType @Deprecated public static LongWriteFunction timestampWriteFunctionUsingSqlTimestamp(TimestampType timestampType) { - checkArgument(timestampType.getPrecision() <= MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); + checkArgument(timestampType.getPrecision() <= TimestampType.MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); return (statement, index, value) -> statement.setTimestamp(index, Timestamp.valueOf(fromPrestoTimestamp(value))); } public static LongWriteFunction timestampWriteFunction(TimestampType timestampType) { - checkArgument(timestampType.getPrecision() <= MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); + checkArgument(timestampType.getPrecision() <= TimestampType.MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); return (statement, index, value) -> statement.setObject(index, fromPrestoTimestamp(value)); } + public static ObjectWriteFunction longTimestampWriteFunction(TimestampType timestampType) + { + checkArgument(timestampType.getPrecision() > TimestampType.MAX_SHORT_PRECISION, "Precision is out of range: %s", timestampType.getPrecision()); + return ObjectWriteFunction.of( + LongTimestamp.class, + (statement, index, value) -> statement.setObject(index, fromLongTimestamp(value, timestampType.getPrecision()))); + } + public static long toPrestoTimestamp(TimestampType timestampType, LocalDateTime localDateTime) { long precision = timestampType.getPrecision(); - checkArgument(precision <= MAX_SHORT_PRECISION, "Precision is out of range: %s", precision); + checkArgument(precision <= TimestampType.MAX_SHORT_PRECISION, "Precision is out of range: %s", precision); Instant instant = localDateTime.atZone(UTC).toInstant(); - return instant.getEpochSecond() * MICROSECONDS_PER_SECOND + roundDiv(instant.getNano(), NANOSECONDS_PER_MICROSECOND); + long epochMicros = instant.getEpochSecond() * MICROSECONDS_PER_SECOND + roundDiv(instant.getNano(), NANOSECONDS_PER_MICROSECOND); + verify( + epochMicros == round(epochMicros, TimestampType.MAX_SHORT_PRECISION - timestampType.getPrecision()), + "Invalid value of epochMicros for precision %s: %s", + timestampType.getPrecision(), + epochMicros); + return epochMicros; + } + + public static LongTimestamp toLongTimestamp(TimestampType timestampType, LocalDateTime localDateTime) + { + long precision = timestampType.getPrecision(); + checkArgument(precision > TimestampType.MAX_SHORT_PRECISION, "Precision is out of range: %s", precision); + Instant instant = localDateTime.atZone(UTC).toInstant(); + long epochMicros = instant.getEpochSecond() * MICROSECONDS_PER_SECOND + floorDiv(instant.getNano(), NANOSECONDS_PER_MICROSECOND); + int picosOfMicro = (instant.getNano() % NANOSECONDS_PER_MICROSECOND) * PICOSECONDS_PER_NANOSECOND; + verify( + picosOfMicro == round(picosOfMicro, TimestampType.MAX_PRECISION - timestampType.getPrecision()), + "Invalid value of picosOfMicro for precision %s: %s", + timestampType.getPrecision(), + picosOfMicro); + return new LongTimestamp(epochMicros, picosOfMicro); } public static LocalDateTime fromPrestoTimestamp(long epochMicros) @@ -505,6 +551,17 @@ public static LocalDateTime fromPrestoTimestamp(long epochMicros) return LocalDateTime.ofInstant(instant, UTC); } + public static LocalDateTime fromLongTimestamp(LongTimestamp timestamp, int precision) + { + long epochSeconds = floorDiv(timestamp.getEpochMicros(), MICROSECONDS_PER_SECOND); + int microsOfSecond = floorMod(timestamp.getEpochMicros(), MICROSECONDS_PER_SECOND); + long picosOfMicro = round(timestamp.getPicosOfMicro(), TimestampType.MAX_PRECISION - precision); + int nanosOfSecond = (microsOfSecond * NANOSECONDS_PER_MICROSECOND) + toIntExact(picosOfMicro / PICOSECONDS_PER_NANOSECOND); + + Instant instant = Instant.ofEpochSecond(epochSeconds, nanosOfSecond); + return LocalDateTime.ofInstant(instant, UTC); + } + public static LocalTime fromPrestoTime(long value) { // value can round up to NANOSECONDS_PER_DAY, so we need to do % to keep it in the desired range diff --git a/plugin/trino-sqlserver/src/main/java/io/trino/plugin/sqlserver/SqlServerClient.java b/plugin/trino-sqlserver/src/main/java/io/trino/plugin/sqlserver/SqlServerClient.java index 8a4eb4f8e858..d69b0539a008 100644 --- a/plugin/trino-sqlserver/src/main/java/io/trino/plugin/sqlserver/SqlServerClient.java +++ b/plugin/trino-sqlserver/src/main/java/io/trino/plugin/sqlserver/SqlServerClient.java @@ -50,6 +50,7 @@ import io.trino.spi.type.CharType; import io.trino.spi.type.DecimalType; import io.trino.spi.type.Decimals; +import io.trino.spi.type.TimestampType; import io.trino.spi.type.Type; import io.trino.spi.type.VarbinaryType; import io.trino.spi.type.VarcharType; @@ -86,11 +87,13 @@ import static io.trino.plugin.jdbc.StandardColumnMappings.doubleWriteFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.integerColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.integerWriteFunction; +import static io.trino.plugin.jdbc.StandardColumnMappings.longTimestampWriteFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.realColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.smallintColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.smallintWriteFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.timeColumnMapping; -import static io.trino.plugin.jdbc.StandardColumnMappings.timestampColumnMappingUsingSqlTimestamp; +import static io.trino.plugin.jdbc.StandardColumnMappings.timestampColumnMapping; +import static io.trino.plugin.jdbc.StandardColumnMappings.timestampWriteFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.tinyintColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.tinyintWriteFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.varcharWriteFunction; @@ -106,10 +109,12 @@ import static io.trino.spi.type.IntegerType.INTEGER; import static io.trino.spi.type.SmallintType.SMALLINT; import static io.trino.spi.type.TimeType.TIME; -import static io.trino.spi.type.TimestampType.TIMESTAMP; +import static io.trino.spi.type.TimestampType.MAX_SHORT_PRECISION; +import static io.trino.spi.type.TimestampType.createTimestampType; import static io.trino.spi.type.TinyintType.TINYINT; import static io.trino.spi.type.VarbinaryType.VARBINARY; import static java.lang.Math.max; +import static java.lang.Math.min; import static java.lang.String.format; import static java.lang.String.join; import static java.math.RoundingMode.UNNECESSARY; @@ -131,6 +136,8 @@ public class SqlServerClient private final AggregateFunctionRewriter aggregateFunctionRewriter; + private static final int MAX_SUPPORTED_TIMESTAMP_PRECISION = 7; + @Inject public SqlServerClient(BaseJdbcConfig config, ConnectionFactory connectionFactory) { @@ -233,7 +240,7 @@ public Optional toColumnMapping(ConnectorSession session, Connect case Types.NUMERIC: case Types.DECIMAL: { int columnSize = typeHandle.getRequiredColumnSize(); - int decimalDigits = typeHandle.getDecimalDigits().orElseThrow(() -> new IllegalStateException("decimal digits not present")); + int decimalDigits = typeHandle.getRequiredDecimalDigits(); // TODO does sql server support negative scale? int precision = columnSize + max(-decimalDigits, 0); // Map decimal(p, -s) (negative scale) to decimal(p+s, 0). if (precision > Decimals.MAX_PRECISION) { @@ -262,7 +269,8 @@ public Optional toColumnMapping(ConnectorSession session, Connect return Optional.of(timeColumnMapping(TIME)); case Types.TIMESTAMP: - return Optional.of(timestampColumnMappingUsingSqlTimestamp(TIMESTAMP)); + int precision = typeHandle.getRequiredDecimalDigits(); + return Optional.of(timestampColumnMapping(createTimestampType(precision))); } // TODO (https://github.com/trinodb/trino/issues/4593) implement proper type mapping @@ -325,6 +333,15 @@ public WriteMapping toWriteMapping(ConnectorSession session, Type type) return WriteMapping.longMapping("date", dateWriteFunction()); } + if (type instanceof TimestampType) { + TimestampType timestampType = (TimestampType) type; + String dataType = format("datetime2(%d)", min(timestampType.getPrecision(), MAX_SUPPORTED_TIMESTAMP_PRECISION)); + if (timestampType.getPrecision() <= MAX_SHORT_PRECISION) { + return WriteMapping.longMapping(dataType, timestampWriteFunction(timestampType)); + } + return WriteMapping.objectMapping(dataType, longTimestampWriteFunction(timestampType)); + } + // TODO implement proper type mapping return legacyToWriteMapping(session, type); } diff --git a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerDistributedQueries.java b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerDistributedQueries.java index e157d9e210d3..2fda8826340c 100644 --- a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerDistributedQueries.java +++ b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerDistributedQueries.java @@ -93,7 +93,6 @@ protected Optional filterDataMappingSmokeTestData(DataMapp { String typeName = dataMappingTestSetup.getTrinoTypeName(); if (typeName.equals("time") - || typeName.equals("timestamp") || typeName.equals("timestamp(3) with time zone")) { return Optional.of(dataMappingTestSetup.asUnsupported()); } diff --git a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerTypeMapping.java b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerTypeMapping.java index 90fd0c8c738a..f8f1da9b050b 100644 --- a/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerTypeMapping.java +++ b/plugin/trino-sqlserver/src/test/java/io/trino/plugin/sqlserver/TestSqlServerTypeMapping.java @@ -16,16 +16,26 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import io.trino.Session; +import io.trino.spi.type.TimeZoneKey; import io.trino.testing.AbstractTestQueryFramework; import io.trino.testing.QueryRunner; +import io.trino.testing.TestingSession; +import io.trino.testing.datatype.CreateAndInsertDataSetup; import io.trino.testing.datatype.CreateAsSelectDataSetup; import io.trino.testing.datatype.DataSetup; import io.trino.testing.datatype.SqlDataTypeTest; +import io.trino.testing.sql.JdbcSqlExecutor; import io.trino.testing.sql.TrinoSqlExecutor; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.time.ZoneId; +import java.util.Properties; + import static io.trino.plugin.sqlserver.SqlServerQueryRunner.createSqlServerQueryRunner; +import static io.trino.spi.type.TimestampType.createTimestampType; import static io.trino.spi.type.VarbinaryType.VARBINARY; +import static java.time.ZoneOffset.UTC; public class TestSqlServerTypeMapping extends AbstractTestQueryFramework @@ -59,6 +69,161 @@ public void testVarbinary() .execute(getQueryRunner(), trinoCreateAsSelect("test_varbinary")); } + @Test(dataProvider = "testTimestampDataProvider") + public void testTimestamp(ZoneId sessionZone) + { + SqlDataTypeTest tests = SqlDataTypeTest.create() + + // before epoch + .addRoundTrip("timestamp(3)", "TIMESTAMP '1958-01-01 13:18:03.123'", createTimestampType(3), "TIMESTAMP '1958-01-01 13:18:03.123'") + // after epoch + .addRoundTrip("timestamp(3)", "TIMESTAMP '2019-03-18 10:01:17.987'", createTimestampType(3), "TIMESTAMP '2019-03-18 10:01:17.987'") + // time doubled in JVM zone + .addRoundTrip("timestamp(3)", "TIMESTAMP '2018-10-28 01:33:17.456'", createTimestampType(3), "TIMESTAMP '2018-10-28 01:33:17.456'") + // time double in Vilnius + .addRoundTrip("timestamp(3)", "TIMESTAMP '2018-10-28 03:33:33.333'", createTimestampType(3), "TIMESTAMP '2018-10-28 03:33:33.333'") + // epoch + .addRoundTrip("timestamp(3)", "TIMESTAMP '1970-01-01 00:00:00.000'", createTimestampType(3), "TIMESTAMP '1970-01-01 00:00:00.000'") + // time gap in JVM zone + .addRoundTrip("timestamp(3)", "TIMESTAMP '1970-01-01 00:13:42.000'", createTimestampType(3), "TIMESTAMP '1970-01-01 00:13:42.000'") + .addRoundTrip("timestamp(3)", "TIMESTAMP '2018-04-01 02:13:55.123'", createTimestampType(3), "TIMESTAMP '2018-04-01 02:13:55.123'") + // time gap in Vilnius + .addRoundTrip("timestamp(3)", "TIMESTAMP '2018-03-25 03:17:17.000'", createTimestampType(3), "TIMESTAMP '2018-03-25 03:17:17.000'") + // time gap in Kathmandu + .addRoundTrip("timestamp(3)", "TIMESTAMP '1986-01-01 00:13:07.000'", createTimestampType(3), "TIMESTAMP '1986-01-01 00:13:07.000'") + + // same as above but with higher precision + .addRoundTrip("timestamp(7)", "TIMESTAMP '1958-01-01 13:18:03.1230000'", createTimestampType(7), "TIMESTAMP '1958-01-01 13:18:03.1230000'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '2019-03-18 10:01:17.9870000'", createTimestampType(7), "TIMESTAMP '2019-03-18 10:01:17.9870000'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '2018-10-28 01:33:17.4560000'", createTimestampType(7), "TIMESTAMP '2018-10-28 01:33:17.4560000'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '2018-10-28 03:33:33.3330000'", createTimestampType(7), "TIMESTAMP '2018-10-28 03:33:33.3330000'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '1970-01-01 00:00:00.0000000'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.0000000'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '1970-01-01 00:13:42.0000000'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:13:42.0000000'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '2018-04-01 02:13:55.1230000'", createTimestampType(7), "TIMESTAMP '2018-04-01 02:13:55.1230000'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '2018-03-25 03:17:17.0000000'", createTimestampType(7), "TIMESTAMP '2018-03-25 03:17:17.0000000'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '1986-01-01 00:13:07.0000000'", createTimestampType(7), "TIMESTAMP '1986-01-01 00:13:07.0000000'") + + // test arbitrary time for all supported precisions + .addRoundTrip("timestamp(0)", "TIMESTAMP '1970-01-01 00:00:00'", createTimestampType(0), "TIMESTAMP '1970-01-01 00:00:00'") + .addRoundTrip("timestamp(1)", "TIMESTAMP '1970-01-01 00:00:00.1'", createTimestampType(1), "TIMESTAMP '1970-01-01 00:00:00.1'") + .addRoundTrip("timestamp(2)", "TIMESTAMP '1970-01-01 00:00:00.12'", createTimestampType(2), "TIMESTAMP '1970-01-01 00:00:00.12'") + .addRoundTrip("timestamp(3)", "TIMESTAMP '1970-01-01 00:00:00.123'", createTimestampType(3), "TIMESTAMP '1970-01-01 00:00:00.123'") + .addRoundTrip("timestamp(4)", "TIMESTAMP '1970-01-01 00:00:00.1234'", createTimestampType(4), "TIMESTAMP '1970-01-01 00:00:00.1234'") + .addRoundTrip("timestamp(5)", "TIMESTAMP '1970-01-01 00:00:00.12345'", createTimestampType(5), "TIMESTAMP '1970-01-01 00:00:00.12345'") + .addRoundTrip("timestamp(6)", "TIMESTAMP '1970-01-01 00:00:00.123456'", createTimestampType(6), "TIMESTAMP '1970-01-01 00:00:00.123456'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '1970-01-01 00:00:00.1234567'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1234567'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '1970-01-01 00:00:00.12345670'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1234567'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '1970-01-01 00:00:00.123456749999'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1234567'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '1970-01-01 00:00:00.12345675'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1234568'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '1970-01-01 00:00:00.12345679'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1234568'") + + // before epoch with second fraction + .addRoundTrip("timestamp(7)", "TIMESTAMP '1969-12-31 23:59:59.1230000'", createTimestampType(7), "TIMESTAMP '1969-12-31 23:59:59.1230000'") + .addRoundTrip("timestamp(7)", "TIMESTAMP '1969-12-31 23:59:59.1234567'", createTimestampType(7), "TIMESTAMP '1969-12-31 23:59:59.1234567'") + + // precision 0 ends up as precision 0 + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00'", "TIMESTAMP '1970-01-01 00:00:00'") + + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.1'", "TIMESTAMP '1970-01-01 00:00:00.1'") + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.9'", "TIMESTAMP '1970-01-01 00:00:00.9'") + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.123'", "TIMESTAMP '1970-01-01 00:00:00.123'") + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.123000'", "TIMESTAMP '1970-01-01 00:00:00.123000'") + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.999'", "TIMESTAMP '1970-01-01 00:00:00.999'") + // max supported precision + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.1234567'", "TIMESTAMP '1970-01-01 00:00:00.1234567'") + + .addRoundTrip("TIMESTAMP '2020-09-27 12:34:56.1'", "TIMESTAMP '2020-09-27 12:34:56.1'") + .addRoundTrip("TIMESTAMP '2020-09-27 12:34:56.9'", "TIMESTAMP '2020-09-27 12:34:56.9'") + .addRoundTrip("TIMESTAMP '2020-09-27 12:34:56.123'", "TIMESTAMP '2020-09-27 12:34:56.123'") + .addRoundTrip("TIMESTAMP '2020-09-27 12:34:56.123000'", "TIMESTAMP '2020-09-27 12:34:56.123000'") + .addRoundTrip("TIMESTAMP '2020-09-27 12:34:56.999'", "TIMESTAMP '2020-09-27 12:34:56.999'") + // max supported precision + .addRoundTrip("TIMESTAMP '2020-09-27 12:34:56.1234567'", "TIMESTAMP '2020-09-27 12:34:56.1234567'") + + // round down + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.12345671'", "TIMESTAMP '1970-01-01 00:00:00.1234567'") + + // nanos round up, end result rounds down + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.1234567499'", "TIMESTAMP '1970-01-01 00:00:00.1234567'") + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.123456749999'", "TIMESTAMP '1970-01-01 00:00:00.1234567'") + + // round up + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.12345675'", "TIMESTAMP '1970-01-01 00:00:00.1234568'") + + // max precision + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.111222333444'", "TIMESTAMP '1970-01-01 00:00:00.1112223'") + + // round up to next second + .addRoundTrip("TIMESTAMP '1970-01-01 00:00:00.99999995'", "TIMESTAMP '1970-01-01 00:00:01.0000000'") + + // round up to next day + .addRoundTrip("TIMESTAMP '1970-01-01 23:59:59.99999995'", "TIMESTAMP '1970-01-02 00:00:00.0000000'") + + // negative epoch + .addRoundTrip("TIMESTAMP '1969-12-31 23:59:59.99999995'", "TIMESTAMP '1970-01-01 00:00:00.0000000'") + .addRoundTrip("TIMESTAMP '1969-12-31 23:59:59.999999949999'", "TIMESTAMP '1969-12-31 23:59:59.9999999'") + .addRoundTrip("TIMESTAMP '1969-12-31 23:59:59.99999994'", "TIMESTAMP '1969-12-31 23:59:59.9999999'"); + + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) + .build(); + + tests.execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_timestamp")); + tests.execute(getQueryRunner(), session, trinoCreateAsSelect(getSession(), "test_timestamp")); + tests.execute(getQueryRunner(), session, trinoCreateAndInsert(session, "test_timestamp")); + } + + @Test + public void testSqlServerDatetime2() + { + SqlDataTypeTest.create() + // literal values with higher precision are NOT rounded and cause an error + .addRoundTrip("DATETIME2(0)", "'1970-01-01 00:00:00'", createTimestampType(0), "TIMESTAMP '1970-01-01 00:00:00'") + .addRoundTrip("DATETIME2(1)", "'1970-01-01 00:00:00.1'", createTimestampType(1), "TIMESTAMP '1970-01-01 00:00:00.1'") + .addRoundTrip("DATETIME2(1)", "'1970-01-01 00:00:00.9'", createTimestampType(1), "TIMESTAMP '1970-01-01 00:00:00.9'") + .addRoundTrip("DATETIME2(3)", "'1970-01-01 00:00:00.123'", createTimestampType(3), "TIMESTAMP '1970-01-01 00:00:00.123'") + .addRoundTrip("DATETIME2(6)", "'1970-01-01 00:00:00.123000'", createTimestampType(6), "TIMESTAMP '1970-01-01 00:00:00.123000'") + .addRoundTrip("DATETIME2(3)", "'1970-01-01 00:00:00.999'", createTimestampType(3), "TIMESTAMP '1970-01-01 00:00:00.999'") + .addRoundTrip("DATETIME2(7)", "'1970-01-01 00:00:00.1234567'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1234567'") + .addRoundTrip("DATETIME2(1)", "'2020-09-27 12:34:56.1'", createTimestampType(1), "TIMESTAMP '2020-09-27 12:34:56.1'") + .addRoundTrip("DATETIME2(1)", "'2020-09-27 12:34:56.9'", createTimestampType(1), "TIMESTAMP '2020-09-27 12:34:56.9'") + .addRoundTrip("DATETIME2(3)", "'2020-09-27 12:34:56.123'", createTimestampType(3), "TIMESTAMP '2020-09-27 12:34:56.123'") + .addRoundTrip("DATETIME2(6)", "'2020-09-27 12:34:56.123000'", createTimestampType(6), "TIMESTAMP '2020-09-27 12:34:56.123000'") + .addRoundTrip("DATETIME2(3)", "'2020-09-27 12:34:56.999'", createTimestampType(3), "TIMESTAMP '2020-09-27 12:34:56.999'") + .addRoundTrip("DATETIME2(7)", "'2020-09-27 12:34:56.1234567'", createTimestampType(7), "TIMESTAMP '2020-09-27 12:34:56.1234567'") + + .addRoundTrip("DATETIME2(7)", "'1970-01-01 00:00:00'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.0000000'") + .addRoundTrip("DATETIME2(7)", "'1970-01-01 00:00:00.1'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1000000'") + .addRoundTrip("DATETIME2(7)", "'1970-01-01 00:00:00.9'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.9000000'") + .addRoundTrip("DATETIME2(7)", "'1970-01-01 00:00:00.123'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1230000'") + .addRoundTrip("DATETIME2(7)", "'1970-01-01 00:00:00.123000'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1230000'") + .addRoundTrip("DATETIME2(7)", "'1970-01-01 00:00:00.999'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.9990000'") + .addRoundTrip("DATETIME2(7)", "'1970-01-01 00:00:00.1234567'", createTimestampType(7), "TIMESTAMP '1970-01-01 00:00:00.1234567'") + .addRoundTrip("DATETIME2(7)", "'2020-09-27 12:34:56.1'", createTimestampType(7), "TIMESTAMP '2020-09-27 12:34:56.1000000'") + .addRoundTrip("DATETIME2(7)", "'2020-09-27 12:34:56.9'", createTimestampType(7), "TIMESTAMP '2020-09-27 12:34:56.9000000'") + .addRoundTrip("DATETIME2(7)", "'2020-09-27 12:34:56.123'", createTimestampType(7), "TIMESTAMP '2020-09-27 12:34:56.1230000'") + .addRoundTrip("DATETIME2(7)", "'2020-09-27 12:34:56.123000'", createTimestampType(7), "TIMESTAMP '2020-09-27 12:34:56.1230000'") + .addRoundTrip("DATETIME2(7)", "'2020-09-27 12:34:56.999'", createTimestampType(7), "TIMESTAMP '2020-09-27 12:34:56.9990000'") + .addRoundTrip("DATETIME2(7)", "'2020-09-27 12:34:56.1234567'", createTimestampType(7), "TIMESTAMP '2020-09-27 12:34:56.1234567'") + + .execute(getQueryRunner(), sqlServerCreateAndInsert("test_sqlserver_timestamp")); + } + + @DataProvider + public Object[][] testTimestampDataProvider() + { + return new Object[][] { + {UTC}, + {ZoneId.systemDefault()}, + // using two non-JVM zones so that we don't need to worry what SQL Server system zone is + // no DST in 1970, but has DST in later years (e.g. 2018) + {ZoneId.of("Europe/Vilnius")}, + // minutes offset change since 1970-01-01, no DST + {ZoneId.of("Asia/Kathmandu")}, + {ZoneId.of(TestingSession.DEFAULT_TIME_ZONE_KEY.getId())}, + }; + } + private DataSetup trinoCreateAsSelect(String tableNamePrefix) { return trinoCreateAsSelect(getSession(), tableNamePrefix); @@ -68,4 +233,17 @@ private DataSetup trinoCreateAsSelect(Session session, String tableNamePrefix) { return new CreateAsSelectDataSetup(new TrinoSqlExecutor(getQueryRunner(), session), tableNamePrefix); } + + private DataSetup trinoCreateAndInsert(Session session, String tableNamePrefix) + { + return new CreateAndInsertDataSetup(new TrinoSqlExecutor(getQueryRunner(), session), tableNamePrefix); + } + + private DataSetup sqlServerCreateAndInsert(String tableNamePrefix) + { + Properties properties = new Properties(); + properties.setProperty("user", sqlServer.getUsername()); + properties.setProperty("password", sqlServer.getPassword()); + return new CreateAndInsertDataSetup(new JdbcSqlExecutor(sqlServer.getJdbcUrl(), properties), tableNamePrefix); + } } diff --git a/pom.xml b/pom.xml index da734eb61b2e..b75caed10e36 100644 --- a/pom.xml +++ b/pom.xml @@ -1063,7 +1063,7 @@ com.microsoft.sqlserver mssql-jdbc - 7.0.0.jre8 + 8.4.1.jre8