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 60bc4b487ae8..df4a7f56a8b0 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 @@ -574,7 +574,7 @@ public static LongReadFunction timestampReadFunction(TimestampType timestampType return (resultSet, columnIndex) -> toTrinoTimestamp(timestampType, resultSet.getObject(columnIndex, LocalDateTime.class)); } - private static ObjectReadFunction longTimestampReadFunction(TimestampType timestampType) + public 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()); diff --git a/plugin/trino-clickhouse/src/main/java/io/trino/plugin/clickhouse/ClickHouseClient.java b/plugin/trino-clickhouse/src/main/java/io/trino/plugin/clickhouse/ClickHouseClient.java index d577be7a3d4d..a7eb267ecc85 100644 --- a/plugin/trino-clickhouse/src/main/java/io/trino/plugin/clickhouse/ClickHouseClient.java +++ b/plugin/trino-clickhouse/src/main/java/io/trino/plugin/clickhouse/ClickHouseClient.java @@ -15,6 +15,7 @@ import com.clickhouse.client.ClickHouseColumn; import com.clickhouse.client.ClickHouseDataType; +import com.clickhouse.client.ClickHouseVersion; import com.google.common.base.Enums; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; @@ -80,13 +81,13 @@ import java.sql.Types; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.OptionalLong; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import static com.google.common.base.Preconditions.checkArgument; @@ -101,6 +102,11 @@ import static io.trino.plugin.clickhouse.ClickHouseTableProperties.PARTITION_BY_PROPERTY; import static io.trino.plugin.clickhouse.ClickHouseTableProperties.PRIMARY_KEY_PROPERTY; import static io.trino.plugin.clickhouse.ClickHouseTableProperties.SAMPLE_BY_PROPERTY; +import static io.trino.plugin.clickhouse.TrinoToClickHouseWriteChecker.DATETIME; +import static io.trino.plugin.clickhouse.TrinoToClickHouseWriteChecker.UINT16; +import static io.trino.plugin.clickhouse.TrinoToClickHouseWriteChecker.UINT32; +import static io.trino.plugin.clickhouse.TrinoToClickHouseWriteChecker.UINT64; +import static io.trino.plugin.clickhouse.TrinoToClickHouseWriteChecker.UINT8; import static io.trino.plugin.jdbc.DecimalConfig.DecimalMapping.ALLOW_OVERFLOW; import static io.trino.plugin.jdbc.DecimalSessionSessionProperties.getDecimalDefaultScale; import static io.trino.plugin.jdbc.DecimalSessionSessionProperties.getDecimalRounding; @@ -133,7 +139,6 @@ import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling; import static io.trino.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR; import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR; -import static io.trino.spi.StandardErrorCode.INVALID_ARGUMENTS; import static io.trino.spi.StandardErrorCode.INVALID_TABLE_PROPERTY; import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; import static io.trino.spi.type.BigintType.BIGINT; @@ -169,26 +174,7 @@ public class ClickHouseClient { private static final Splitter TABLE_PROPERTY_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults(); - private static final long UINT8_MIN_VALUE = 0L; - private static final long UINT8_MAX_VALUE = 255L; - - private static final long UINT16_MIN_VALUE = 0L; - private static final long UINT16_MAX_VALUE = 65535L; - - private static final long UINT32_MIN_VALUE = 0L; - private static final long UINT32_MAX_VALUE = 4294967295L; - private static final DecimalType UINT64_TYPE = createDecimalType(20, 0); - private static final BigDecimal UINT64_MIN_VALUE = BigDecimal.ZERO; - private static final BigDecimal UINT64_MAX_VALUE = new BigDecimal("18446744073709551615"); - - private static final long MIN_SUPPORTED_DATE_EPOCH = LocalDate.parse("1970-01-01").toEpochDay(); - private static final long MAX_SUPPORTED_DATE_EPOCH = LocalDate.parse("2106-02-07").toEpochDay(); // The max date is '2148-12-31' in new ClickHouse version - - private static final LocalDateTime MIN_SUPPORTED_TIMESTAMP = LocalDateTime.parse("1970-01-01T00:00:00"); - private static final LocalDateTime MAX_SUPPORTED_TIMESTAMP = LocalDateTime.parse("2105-12-31T23:59:59"); - private static final long MIN_SUPPORTED_TIMESTAMP_EPOCH = MIN_SUPPORTED_TIMESTAMP.toEpochSecond(UTC); - private static final long MAX_SUPPORTED_TIMESTAMP_EPOCH = MAX_SUPPORTED_TIMESTAMP.toEpochSecond(UTC); // An empty character means that the table doesn't have a comment in ClickHouse private static final String NO_COMMENT = ""; @@ -197,6 +183,7 @@ public class ClickHouseClient private final AggregateFunctionRewriter aggregateFunctionRewriter; private final Type uuidType; private final Type ipAddressType; + private final AtomicReference clickHouseVersion = new AtomicReference<>(); @Inject public ClickHouseClient( @@ -511,16 +498,16 @@ public Optional toColumnMapping(ConnectorSession session, Connect ClickHouseDataType columnDataType = column.getDataType(); switch (columnDataType) { case UInt8: - return Optional.of(ColumnMapping.longMapping(SMALLINT, ResultSet::getShort, uInt8WriteFunction())); + return Optional.of(ColumnMapping.longMapping(SMALLINT, ResultSet::getShort, uInt8WriteFunction(getClickHouseServerVersion(session)))); case UInt16: - return Optional.of(ColumnMapping.longMapping(INTEGER, ResultSet::getInt, uInt16WriteFunction())); + return Optional.of(ColumnMapping.longMapping(INTEGER, ResultSet::getInt, uInt16WriteFunction(getClickHouseServerVersion(session)))); case UInt32: - return Optional.of(ColumnMapping.longMapping(BIGINT, ResultSet::getLong, uInt32WriteFunction())); + return Optional.of(ColumnMapping.longMapping(BIGINT, ResultSet::getLong, uInt32WriteFunction(getClickHouseServerVersion(session)))); case UInt64: return Optional.of(ColumnMapping.objectMapping( UINT64_TYPE, longDecimalReadFunction(UINT64_TYPE, UNNECESSARY), - uInt64WriteFunction())); + uInt64WriteFunction(getClickHouseServerVersion(session)))); case IPv4: return Optional.of(ipAddressColumnMapping("IPv4StringToNum(?)")); case IPv6: @@ -595,7 +582,7 @@ public Optional toColumnMapping(ConnectorSession session, Connect DISABLE_PUSHDOWN)); case Types.DATE: - return Optional.of(dateColumnMappingUsingLocalDate()); + return Optional.of(dateColumnMappingUsingLocalDate(getClickHouseServerVersion(session))); case Types.TIMESTAMP: if (columnDataType == ClickHouseDataType.DateTime) { @@ -603,7 +590,7 @@ public Optional toColumnMapping(ConnectorSession session, Connect return Optional.of(ColumnMapping.longMapping( TIMESTAMP_SECONDS, timestampReadFunction(TIMESTAMP_SECONDS), - timestampSecondsWriteFunction())); + timestampSecondsWriteFunction(getClickHouseServerVersion(session)))); } // TODO (https://github.com/trinodb/trino/issues/10537) Add support for Datetime64 type return Optional.of(timestampColumnMappingUsingSqlTimestampWithRounding(TIMESTAMP_MILLIS)); @@ -658,10 +645,10 @@ public WriteMapping toWriteMapping(ConnectorSession session, Type type) return WriteMapping.sliceMapping("String", varbinaryWriteFunction()); } if (type == DATE) { - return WriteMapping.longMapping("Date", dateWriteFunctionUsingLocalDate()); + return WriteMapping.longMapping("Date", dateWriteFunctionUsingLocalDate(getClickHouseServerVersion(session))); } if (type == TIMESTAMP_SECONDS) { - return WriteMapping.longMapping("DateTime", timestampSecondsWriteFunction()); + return WriteMapping.longMapping("DateTime", timestampSecondsWriteFunction(getClickHouseServerVersion(session))); } if (type.equals(uuidType)) { return WriteMapping.sliceMapping("UUID", uuidWriteFunction()); @@ -669,6 +656,27 @@ public WriteMapping toWriteMapping(ConnectorSession session, Type type) throw new TrinoException(NOT_SUPPORTED, "Unsupported column type: " + type); } + private ClickHouseVersion getClickHouseServerVersion(ConnectorSession session) + { + return clickHouseVersion.updateAndGet(current -> { + if (current != null) { + return current; + } + + try (Connection connection = connectionFactory.openConnection(session); + PreparedStatement statement = connection.prepareStatement("SELECT version()"); + ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + current = ClickHouseVersion.of(resultSet.getString(1)); + } + return current; + } + catch (SQLException e) { + throw new TrinoException(JDBC_ERROR, e); + } + }); + } + /** * format property to match ClickHouse create table statement * @@ -688,40 +696,34 @@ private Optional formatProperty(List prop) return Optional.of("(" + String.join(",", prop) + ")"); } - private static LongWriteFunction uInt8WriteFunction() + private static LongWriteFunction uInt8WriteFunction(ClickHouseVersion version) { return (statement, index, value) -> { // ClickHouse stores incorrect results when the values are out of supported range. - if (value < UINT8_MIN_VALUE || value > UINT8_MAX_VALUE) { - throw new TrinoException(INVALID_ARGUMENTS, format("Value must be between %s and %s in ClickHouse: %s", UINT8_MIN_VALUE, UINT8_MAX_VALUE, value)); - } + UINT8.validate(version, value); statement.setShort(index, Shorts.checkedCast(value)); }; } - private static LongWriteFunction uInt16WriteFunction() + private static LongWriteFunction uInt16WriteFunction(ClickHouseVersion version) { return (statement, index, value) -> { // ClickHouse stores incorrect results when the values are out of supported range. - if (value < UINT16_MIN_VALUE || value > UINT16_MAX_VALUE) { - throw new TrinoException(INVALID_ARGUMENTS, format("Value must be between %s and %s in ClickHouse: %s", UINT16_MIN_VALUE, UINT16_MAX_VALUE, value)); - } + UINT16.validate(version, value); statement.setInt(index, toIntExact(value)); }; } - private static LongWriteFunction uInt32WriteFunction() + private static LongWriteFunction uInt32WriteFunction(ClickHouseVersion version) { return (preparedStatement, parameterIndex, value) -> { // ClickHouse stores incorrect results when the values are out of supported range. - if (value < UINT32_MIN_VALUE || value > UINT32_MAX_VALUE) { - throw new TrinoException(INVALID_ARGUMENTS, format("Value must be between %s and %s in ClickHouse: %s", UINT32_MIN_VALUE, UINT32_MAX_VALUE, value)); - } + UINT32.validate(version, value); preparedStatement.setLong(parameterIndex, value); }; } - private static ObjectWriteFunction uInt64WriteFunction() + private static ObjectWriteFunction uInt64WriteFunction(ClickHouseVersion version) { return ObjectWriteFunction.of( Int128.class, @@ -729,56 +731,42 @@ private static ObjectWriteFunction uInt64WriteFunction() BigInteger unscaledValue = value.toBigInteger(); BigDecimal bigDecimal = new BigDecimal(unscaledValue, UINT64_TYPE.getScale(), new MathContext(UINT64_TYPE.getPrecision())); // ClickHouse stores incorrect results when the values are out of supported range. - if (bigDecimal.compareTo(UINT64_MIN_VALUE) < 0 || bigDecimal.compareTo(UINT64_MAX_VALUE) > 0) { - throw new TrinoException(INVALID_ARGUMENTS, format("Value must be between %s and %s in ClickHouse: %s", UINT64_MIN_VALUE, UINT64_MAX_VALUE, bigDecimal)); - } + UINT64.validate(version, bigDecimal); statement.setBigDecimal(index, bigDecimal); }); } - private static ColumnMapping dateColumnMappingUsingLocalDate() + private static ColumnMapping dateColumnMappingUsingLocalDate(ClickHouseVersion version) { return ColumnMapping.longMapping( DATE, dateReadFunctionUsingLocalDate(), - dateWriteFunctionUsingLocalDate()); + dateWriteFunctionUsingLocalDate(version)); } - private static LongWriteFunction dateWriteFunctionUsingLocalDate() + private static LongWriteFunction dateWriteFunctionUsingLocalDate(ClickHouseVersion version) { return (statement, index, value) -> { - verifySupportedDate(value); - statement.setObject(index, LocalDate.ofEpochDay(value)); + LocalDate date = LocalDate.ofEpochDay(value); + // Deny unsupported dates eagerly to prevent unexpected results. ClickHouse stores '1970-01-01' when the date is out of supported range. + TrinoToClickHouseWriteChecker.DATE.validate(version, date); + statement.setObject(index, date); }; } - private static void verifySupportedDate(long value) - { - // Deny unsupported dates eagerly to prevent unexpected results. ClickHouse stores '1970-01-01' when the date is out of supported range. - if (value < MIN_SUPPORTED_DATE_EPOCH || value > MAX_SUPPORTED_DATE_EPOCH) { - throw new TrinoException(INVALID_ARGUMENTS, format("Date must be between %s and %s in ClickHouse: %s", LocalDate.ofEpochDay(MIN_SUPPORTED_DATE_EPOCH), LocalDate.ofEpochDay(MAX_SUPPORTED_DATE_EPOCH), LocalDate.ofEpochDay(value))); - } - } - - private static LongWriteFunction timestampSecondsWriteFunction() + private static LongWriteFunction timestampSecondsWriteFunction(ClickHouseVersion version) { return (statement, index, value) -> { long epochSecond = floorDiv(value, MICROSECONDS_PER_SECOND); int nanoFraction = floorMod(value, MICROSECONDS_PER_SECOND) * NANOSECONDS_PER_MICROSECOND; verify(nanoFraction == 0, "Nanos of second must be zero: '%s'", value); - verifySupportedTimestamp(epochSecond); - statement.setObject(index, LocalDateTime.ofEpochSecond(epochSecond, 0, UTC)); + LocalDateTime timestamp = LocalDateTime.ofEpochSecond(epochSecond, 0, UTC); + // ClickHouse stores incorrect results when the values are out of supported range. + DATETIME.validate(version, timestamp); + statement.setObject(index, timestamp); }; } - private static void verifySupportedTimestamp(long epochSecond) - { - if (epochSecond < MIN_SUPPORTED_TIMESTAMP_EPOCH || epochSecond > MAX_SUPPORTED_TIMESTAMP_EPOCH) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss"); - throw new TrinoException(INVALID_ARGUMENTS, format("Timestamp must be between %s and %s in ClickHouse: %s", MIN_SUPPORTED_TIMESTAMP.format(formatter), MAX_SUPPORTED_TIMESTAMP.format(formatter), LocalDateTime.ofEpochSecond(epochSecond, 0, UTC).format(formatter))); - } - } - private ColumnMapping ipAddressColumnMapping(String writeBindExpression) { return ColumnMapping.sliceMapping( diff --git a/plugin/trino-clickhouse/src/main/java/io/trino/plugin/clickhouse/TrinoToClickHouseWriteChecker.java b/plugin/trino-clickhouse/src/main/java/io/trino/plugin/clickhouse/TrinoToClickHouseWriteChecker.java new file mode 100644 index 000000000000..98b240cec92d --- /dev/null +++ b/plugin/trino-clickhouse/src/main/java/io/trino/plugin/clickhouse/TrinoToClickHouseWriteChecker.java @@ -0,0 +1,222 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.clickhouse; + +import com.clickhouse.client.ClickHouseVersion; +import com.google.common.collect.ImmutableList; +import io.trino.spi.TrinoException; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.util.List; +import java.util.function.Predicate; + +import static com.google.common.base.Predicates.alwaysTrue; +import static io.trino.spi.StandardErrorCode.INVALID_ARGUMENTS; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class TrinoToClickHouseWriteChecker +{ + // Different versions of ClickHouse may support different min/max values for the + // same data type, you can refer to the table below: + // + // | version | column type | min value | max value | + // |---------|-------------|---------------------|----------------------| + // | any | UInt8 | 0 | 255 | + // | any | UInt16 | 0 | 65535 | + // | any | UInt32 | 0 | 4294967295 | + // | any | UInt64 | 0 | 18446744073709551615 | + // | < 21.4 | Date | 1970-01-01 | 2106-02-07 | + // | < 21.4 | DateTime | 1970-01-01 00:00:00 | 2106-02-06 06:28:15 | + // | >= 21.4 | Date | 1970-01-01 | 2149-06-06 | + // | >= 21.4 | DateTime | 1970-01-01 00:00:00 | 2106-02-07 06:28:15 | + // + // And when the value written to ClickHouse is out of range, ClickHouse will store + // the incorrect result, so we need to check the range of the written value to + // prevent ClickHouse from storing the incorrect value. + + public static final TrinoToClickHouseWriteChecker UINT8 = new TrinoToClickHouseWriteChecker<>(ImmutableList.of(new LongWriteValueChecker(alwaysTrue(), new Range<>(0L, 255L)))); + public static final TrinoToClickHouseWriteChecker UINT16 = new TrinoToClickHouseWriteChecker<>(ImmutableList.of(new LongWriteValueChecker(alwaysTrue(), new Range<>(0L, 65535L)))); + public static final TrinoToClickHouseWriteChecker UINT32 = new TrinoToClickHouseWriteChecker<>(ImmutableList.of(new LongWriteValueChecker(alwaysTrue(), new Range<>(0L, 4294967295L)))); + public static final TrinoToClickHouseWriteChecker UINT64 = new TrinoToClickHouseWriteChecker<>( + ImmutableList.of(new BigDecimalWriteValueChecker(alwaysTrue(), new Range<>(BigDecimal.ZERO, new BigDecimal("18446744073709551615"))))); + public static final TrinoToClickHouseWriteChecker DATE = new TrinoToClickHouseWriteChecker<>( + ImmutableList.of( + new DateWriteValueChecker(version -> version.isOlderThan("21.4"), new Range<>(LocalDate.parse("1970-01-01"), LocalDate.parse("2106-02-07"))), + new DateWriteValueChecker(version -> version.isNewerOrEqualTo("21.4"), new Range<>(LocalDate.parse("1970-01-01"), LocalDate.parse("2149-06-06"))))); + public static final TrinoToClickHouseWriteChecker DATETIME = new TrinoToClickHouseWriteChecker<>( + ImmutableList.of( + new TimestampWriteValueChecker( + version -> version.isOlderThan("21.4"), + new Range<>(LocalDateTime.parse("1970-01-01T00:00:00"), LocalDateTime.parse("2106-02-06T06:28:15"))), + new TimestampWriteValueChecker( + version -> version.isNewerOrEqualTo("21.4"), + new Range<>(LocalDateTime.parse("1970-01-01T00:00:00"), LocalDateTime.parse("2106-02-07T06:28:15"))))); + + private final List> checkers; + + private TrinoToClickHouseWriteChecker(List> checkers) + { + this.checkers = ImmutableList.copyOf(requireNonNull(checkers, "checkers is null")); + } + + public void validate(ClickHouseVersion version, T value) + { + for (Checker checker : checkers) { + checker.validate(version, value); + } + } + + private interface Checker + { + void validate(ClickHouseVersion version, T value); + } + + private static class LongWriteValueChecker + implements Checker + { + private final Predicate predicate; + private final Range range; + + public LongWriteValueChecker(Predicate predicate, Range range) + { + this.predicate = requireNonNull(predicate, "predicate is null"); + this.range = requireNonNull(range, "range is null"); + } + + @Override + public void validate(ClickHouseVersion version, Long value) + { + if (!predicate.test(version)) { + return; + } + + if (value >= range.getMin() && value <= range.getMax()) { + return; + } + + throw new TrinoException(INVALID_ARGUMENTS, format("Value must be between %d and %d in ClickHouse: %d", range.getMin(), range.getMax(), value)); + } + } + + private static class BigDecimalWriteValueChecker + implements Checker + { + private final Predicate predicate; + private final Range range; + + public BigDecimalWriteValueChecker(Predicate predicate, Range range) + { + this.predicate = requireNonNull(predicate, "predicate is null"); + this.range = requireNonNull(range, "range is null"); + } + + @Override + public void validate(ClickHouseVersion version, BigDecimal value) + { + if (!predicate.test(version)) { + return; + } + + if (value.compareTo(range.getMin()) >= 0 && value.compareTo(range.getMax()) <= 0) { + return; + } + + throw new TrinoException(INVALID_ARGUMENTS, format("Value must be between %s and %s in ClickHouse: %s", range.getMin(), range.getMax(), value)); + } + } + + private static class DateWriteValueChecker + implements Checker + { + private final Predicate predicate; + private final Range range; + + public DateWriteValueChecker(Predicate predicate, Range range) + { + this.predicate = requireNonNull(predicate, "predicate is null"); + this.range = requireNonNull(range, "range is null"); + } + + @Override + public void validate(ClickHouseVersion version, LocalDate value) + { + if (!predicate.test(version)) { + return; + } + + if (value.isBefore(range.getMin()) || value.isAfter(range.getMax())) { + throw new TrinoException(INVALID_ARGUMENTS, format("Date must be between %s and %s in ClickHouse: %s", range.getMin(), range.getMax(), value)); + } + } + } + + private static class TimestampWriteValueChecker + implements Checker + { + private final Predicate predicate; + private final Range range; + + public TimestampWriteValueChecker(Predicate predicate, Range range) + { + this.predicate = requireNonNull(predicate, "predicate is null"); + this.range = requireNonNull(range, "range is null"); + } + + @Override + public void validate(ClickHouseVersion version, LocalDateTime value) + { + if (!predicate.test(version)) { + return; + } + + if (value.isBefore(range.getMin()) || value.isAfter(range.getMax())) { + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .appendPattern("uuuu-MM-dd HH:mm:ss") + .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) + .toFormatter(); + throw new TrinoException( + INVALID_ARGUMENTS, + format("Timestamp must be between %s and %s in ClickHouse: %s", formatter.format(range.getMin()), formatter.format(range.getMax()), formatter.format(value))); + } + } + } + + private static class Range + { + private final T min; + private final T max; + + public Range(T min, T max) + { + this.min = requireNonNull(min, "min is null"); + this.max = requireNonNull(max, "max is null"); + } + + public T getMin() + { + return min; + } + + public T getMax() + { + return max; + } + } +} diff --git a/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/BaseClickHouseConnectorTest.java b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/BaseClickHouseConnectorTest.java index 9399edf7583b..d9d0a658ac42 100644 --- a/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/BaseClickHouseConnectorTest.java +++ b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/BaseClickHouseConnectorTest.java @@ -608,7 +608,12 @@ public void testDateYearOfEraPredicate() assertQuery("SELECT orderdate FROM orders WHERE orderdate = DATE '1997-09-14'", "VALUES DATE '1997-09-14'"); assertQueryFails( "SELECT * FROM orders WHERE orderdate = DATE '-1996-09-14'", - "Date must be between 1970-01-01 and 2106-02-07 in ClickHouse: -1996-09-14"); + errorMessageForDateYearOfEraPredicate("-1996-09-14")); + } + + protected String errorMessageForDateYearOfEraPredicate(String date) + { + return "Date must be between 1970-01-01 and 2106-02-07 in ClickHouse: " + date; } @Override diff --git a/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/BaseClickHouseTypeMapping.java b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/BaseClickHouseTypeMapping.java new file mode 100644 index 000000000000..3e39c763769f --- /dev/null +++ b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/BaseClickHouseTypeMapping.java @@ -0,0 +1,887 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.clickhouse; + +import io.trino.Session; +import io.trino.spi.type.TimeZoneKey; +import io.trino.spi.type.UuidType; +import io.trino.testing.AbstractTestQueryFramework; +import io.trino.testing.TestingSession; +import io.trino.testing.datatype.CreateAndInsertDataSetup; +import io.trino.testing.datatype.CreateAndTrinoInsertDataSetup; +import io.trino.testing.datatype.CreateAsSelectDataSetup; +import io.trino.testing.datatype.DataSetup; +import io.trino.testing.datatype.SqlDataTypeTest; +import io.trino.testing.sql.SqlExecutor; +import io.trino.testing.sql.TestTable; +import io.trino.testing.sql.TrinoSqlExecutor; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static io.trino.plugin.clickhouse.ClickHouseQueryRunner.TPCH_SCHEMA; +import static io.trino.spi.type.BigintType.BIGINT; +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.IntegerType.INTEGER; +import static io.trino.spi.type.RealType.REAL; +import static io.trino.spi.type.SmallintType.SMALLINT; +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 io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; +import static io.trino.testing.TestingSession.testSessionBuilder; +import static io.trino.type.IpAddressType.IPADDRESS; +import static java.lang.String.format; +import static java.time.ZoneOffset.UTC; + +public abstract class BaseClickHouseTypeMapping + extends AbstractTestQueryFramework +{ + private final ZoneId jvmZone = ZoneId.systemDefault(); + + // no DST in 1970, but has DST in later years (e.g. 2018) + private final ZoneId vilnius = ZoneId.of("Europe/Vilnius"); + + // minutes offset change since 1970-01-01, no DST + private final ZoneId kathmandu = ZoneId.of("Asia/Kathmandu"); + + protected TestingClickHouseServer clickhouseServer; + + @BeforeClass + public void setUp() + { + checkState(jvmZone.getId().equals("America/Bahia_Banderas"), "This test assumes certain JVM time zone"); + LocalDate dateOfLocalTimeChangeForwardAtMidnightInJvmZone = LocalDate.of(1970, 1, 1); + checkIsGap(jvmZone, dateOfLocalTimeChangeForwardAtMidnightInJvmZone.atStartOfDay()); + + LocalDate dateOfLocalTimeChangeForwardAtMidnightInSomeZone = LocalDate.of(1983, 4, 1); + checkIsGap(vilnius, dateOfLocalTimeChangeForwardAtMidnightInSomeZone.atStartOfDay()); + LocalDate dateOfLocalTimeChangeBackwardAtMidnightInSomeZone = LocalDate.of(1983, 10, 1); + checkIsDoubled(vilnius, dateOfLocalTimeChangeBackwardAtMidnightInSomeZone.atStartOfDay().minusMinutes(1)); + + LocalDate timeGapInKathmandu = LocalDate.of(1986, 1, 1); + checkIsGap(kathmandu, timeGapInKathmandu.atStartOfDay()); + } + + private static void checkIsGap(ZoneId zone, LocalDateTime dateTime) + { + verify(isGap(zone, dateTime), "Expected %s to be a gap in %s", dateTime, zone); + } + + private static boolean isGap(ZoneId zone, LocalDateTime dateTime) + { + return zone.getRules().getValidOffsets(dateTime).isEmpty(); + } + + private static void checkIsDoubled(ZoneId zone, LocalDateTime dateTime) + { + verify(zone.getRules().getValidOffsets(dateTime).size() == 2, "Expected %s to be doubled in %s", dateTime, zone); + } + + @Test + public void testTinyint() + { + SqlDataTypeTest.create() + .addRoundTrip("tinyint", "-128", TINYINT, "TINYINT '-128'") // min value in ClickHouse and Trino + .addRoundTrip("tinyint", "5", TINYINT, "TINYINT '5'") + .addRoundTrip("tinyint", "127", TINYINT, "TINYINT '127'") // max value in ClickHouse and Trino + .execute(getQueryRunner(), trinoCreateAsSelect("test_tinyint")) + .execute(getQueryRunner(), trinoCreateAndInsert("test_tinyint")) + + .addRoundTrip("Nullable(tinyint)", "NULL", TINYINT, "CAST(NULL AS TINYINT)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_tinyint")); + + SqlDataTypeTest.create() + .addRoundTrip("tinyint", "NULL", TINYINT, "CAST(NULL AS TINYINT)") + .execute(getQueryRunner(), trinoCreateAsSelect("test_tinyint")) + .execute(getQueryRunner(), trinoCreateAndInsert("test_tinyint")); + } + + @Test + public void testUnsupportedTinyint() + { + // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. + SqlDataTypeTest.create() + .addRoundTrip("tinyint", "-129", TINYINT, "TINYINT '127'") + .addRoundTrip("tinyint", "128", TINYINT, "TINYINT '-128'") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_tinyint")); + } + + @Test + public void testSmallint() + { + SqlDataTypeTest.create() + .addRoundTrip("smallint", "-32768", SMALLINT, "SMALLINT '-32768'") // min value in ClickHouse and Trino + .addRoundTrip("smallint", "32456", SMALLINT, "SMALLINT '32456'") + .addRoundTrip("smallint", "32767", SMALLINT, "SMALLINT '32767'") // max value in ClickHouse and Trino + .execute(getQueryRunner(), trinoCreateAsSelect("test_smallint")) + .execute(getQueryRunner(), trinoCreateAndInsert("test_smallint")) + + .addRoundTrip("Nullable(smallint)", "NULL", SMALLINT, "CAST(NULL AS SMALLINT)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_smallint")); + + SqlDataTypeTest.create() + .addRoundTrip("smallint", "NULL", SMALLINT, "CAST(NULL AS SMALLINT)") + .execute(getQueryRunner(), trinoCreateAsSelect("test_smallint")) + .execute(getQueryRunner(), trinoCreateAndInsert("test_smallint")); + } + + @Test + public void testUnsupportedSmallint() + { + // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. + SqlDataTypeTest.create() + .addRoundTrip("smallint", "-32769", SMALLINT, "SMALLINT '32767'") + .addRoundTrip("smallint", "32768", SMALLINT, "SMALLINT '-32768'") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_smallint")); + } + + @Test + public void testInteger() + { + SqlDataTypeTest.create() + .addRoundTrip("integer", "-2147483648", INTEGER, "-2147483648") // min value in ClickHouse and Trino + .addRoundTrip("integer", "1234567890", INTEGER, "1234567890") + .addRoundTrip("integer", "2147483647", INTEGER, "2147483647") // max value in ClickHouse and Trino + .execute(getQueryRunner(), trinoCreateAsSelect("test_int")) + .execute(getQueryRunner(), trinoCreateAndInsert("test_int")) + + .addRoundTrip("Nullable(integer)", "NULL", INTEGER, "CAST(NULL AS INTEGER)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_int")); + + SqlDataTypeTest.create() + .addRoundTrip("integer", "NULL", INTEGER, "CAST(NULL AS INTEGER)") + .execute(getQueryRunner(), trinoCreateAsSelect("test_int")) + .execute(getQueryRunner(), trinoCreateAndInsert("test_int")); + } + + @Test + public void testUnsupportedInteger() + { + // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. + SqlDataTypeTest.create() + .addRoundTrip("integer", "-2147483649", INTEGER, "INTEGER '2147483647'") + .addRoundTrip("integer", "2147483648", INTEGER, "INTEGER '-2147483648'") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_integer")); + } + + @Test + public void testBigint() + { + SqlDataTypeTest.create() + .addRoundTrip("bigint", "-9223372036854775808", BIGINT, "-9223372036854775808") // min value in ClickHouse and Trino + .addRoundTrip("bigint", "123456789012", BIGINT, "123456789012") + .addRoundTrip("bigint", "9223372036854775807", BIGINT, "9223372036854775807") // max value in ClickHouse and Trino + .execute(getQueryRunner(), trinoCreateAsSelect("test_bigint")) + .execute(getQueryRunner(), trinoCreateAndInsert("test_bigint")) + + .addRoundTrip("Nullable(bigint)", "NULL", BIGINT, "CAST(NULL AS BIGINT)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_bigint")); + + SqlDataTypeTest.create() + .addRoundTrip("bigint", "NULL", BIGINT, "CAST(NULL AS BIGINT)") + .execute(getQueryRunner(), trinoCreateAsSelect("test_bigint")) + .execute(getQueryRunner(), trinoCreateAndInsert("test_bigint")); + } + + @Test + public void testUnsupportedBigint() + { + // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. + SqlDataTypeTest.create() + .addRoundTrip("bigint", "-9223372036854775809", BIGINT, "BIGINT '9223372036854775807'") + .addRoundTrip("bigint", "9223372036854775808", BIGINT, "BIGINT '-9223372036854775808'") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_bigint")); + } + + @Test + public void testUint8() + { + SqlDataTypeTest.create() + .addRoundTrip("UInt8", "0", SMALLINT, "SMALLINT '0'") // min value in ClickHouse + .addRoundTrip("UInt8", "255", SMALLINT, "SMALLINT '255'") // max value in ClickHouse + .addRoundTrip("Nullable(UInt8)", "NULL", SMALLINT, "CAST(null AS SMALLINT)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_uint8")); + + SqlDataTypeTest.create() + .addRoundTrip("UInt8", "0", SMALLINT, "SMALLINT '0'") // min value in ClickHouse + .addRoundTrip("UInt8", "255", SMALLINT, "SMALLINT '255'") // max value in ClickHouse + .addRoundTrip("Nullable(UInt8)", "NULL", SMALLINT, "CAST(null AS SMALLINT)") + .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_uint8")); + } + + @Test + public void testUnsupportedUint8() + { + // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. + SqlDataTypeTest.create() + .addRoundTrip("UInt8", "-1", SMALLINT, "SMALLINT '255'") + .addRoundTrip("UInt8", "256", SMALLINT, "SMALLINT '0'") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_uint8")); + + // Prevent writing incorrect results in the connector + try (TestTable table = new TestTable(onRemoteDatabase(), "tpch.test_unsupported_uint8", "(value UInt8) ENGINE=Log")) { + assertQueryFails( + format("INSERT INTO %s VALUES (-1)", table.getName()), + "Value must be between 0 and 255 in ClickHouse: -1"); + assertQueryFails( + format("INSERT INTO %s VALUES (256)", table.getName()), + "Value must be between 0 and 255 in ClickHouse: 256"); + } + } + + @Test + public void testUint16() + { + SqlDataTypeTest.create() + .addRoundTrip("UInt16", "0", INTEGER, "0") // min value in ClickHouse + .addRoundTrip("UInt16", "65535", INTEGER, "65535") // max value in ClickHouse + .addRoundTrip("Nullable(UInt16)", "NULL", INTEGER, "CAST(null AS INTEGER)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_uint16")); + + SqlDataTypeTest.create() + .addRoundTrip("UInt16", "0", INTEGER, "0") // min value in ClickHouse + .addRoundTrip("UInt16", "65535", INTEGER, "65535") // max value in ClickHouse + .addRoundTrip("Nullable(UInt16)", "NULL", INTEGER, "CAST(null AS INTEGER)") + .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_uint16")); + } + + @Test + public void testUnsupportedUint16() + { + // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. + SqlDataTypeTest.create() + .addRoundTrip("UInt16", "-1", INTEGER, "65535") + .addRoundTrip("UInt16", "65536", INTEGER, "0") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_uint16")); + + // Prevent writing incorrect results in the connector + try (TestTable table = new TestTable(onRemoteDatabase(), "tpch.test_unsupported_uint16", "(value UInt16) ENGINE=Log")) { + assertQueryFails( + format("INSERT INTO %s VALUES (-1)", table.getName()), + "Value must be between 0 and 65535 in ClickHouse: -1"); + assertQueryFails( + format("INSERT INTO %s VALUES (65536)", table.getName()), + "Value must be between 0 and 65535 in ClickHouse: 65536"); + } + } + + @Test + public void testUint32() + { + SqlDataTypeTest.create() + .addRoundTrip("UInt32", "0", BIGINT, "BIGINT '0'") // min value in ClickHouse + .addRoundTrip("UInt32", "4294967295", BIGINT, "BIGINT '4294967295'") // max value in ClickHouse + .addRoundTrip("Nullable(UInt32)", "NULL", BIGINT, "CAST(null AS BIGINT)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_uint32")); + + SqlDataTypeTest.create() + .addRoundTrip("UInt32", "BIGINT '0'", BIGINT, "BIGINT '0'") // min value in ClickHouse + .addRoundTrip("UInt32", "BIGINT '4294967295'", BIGINT, "BIGINT '4294967295'") // max value in ClickHouse + .addRoundTrip("Nullable(UInt32)", "NULL", BIGINT, "CAST(null AS BIGINT)") + .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_uint32")); + } + + @Test + public void testUnsupportedUint32() + { + // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. + SqlDataTypeTest.create() + .addRoundTrip("UInt32", "-1", BIGINT, "BIGINT '4294967295'") + .addRoundTrip("UInt32", "4294967296", BIGINT, "BIGINT '0'") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_uint32")); + + // Prevent writing incorrect results in the connector + try (TestTable table = new TestTable(onRemoteDatabase(), "tpch.test_unsupported_uint32", "(value UInt32) ENGINE=Log")) { + assertQueryFails( + format("INSERT INTO %s VALUES (CAST('-1' AS BIGINT))", table.getName()), + "Value must be between 0 and 4294967295 in ClickHouse: -1"); + assertQueryFails( + format("INSERT INTO %s VALUES (CAST('4294967296' AS BIGINT))", table.getName()), + "Value must be between 0 and 4294967295 in ClickHouse: 4294967296"); + } + } + + @Test + public void testUint64() + { + SqlDataTypeTest.create() + .addRoundTrip("UInt64", "0", createDecimalType(20), "CAST('0' AS decimal(20, 0))") // min value in ClickHouse + .addRoundTrip("UInt64", "18446744073709551615", createDecimalType(20), "CAST('18446744073709551615' AS decimal(20, 0))") // max value in ClickHouse + .addRoundTrip("Nullable(UInt64)", "NULL", createDecimalType(20), "CAST(null AS decimal(20, 0))") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_uint64")); + + SqlDataTypeTest.create() + .addRoundTrip("UInt64", "CAST('0' AS decimal(20, 0))", createDecimalType(20), "CAST('0' AS decimal(20, 0))") // min value in ClickHouse + .addRoundTrip("UInt64", "CAST('18446744073709551615' AS decimal(20, 0))", createDecimalType(20), "CAST('18446744073709551615' AS decimal(20, 0))") // max value in ClickHouse + .addRoundTrip("Nullable(UInt64)", "NULL", createDecimalType(20), "CAST(null AS decimal(20, 0))") + .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_uint64")); + } + + @Test + public void testUnsupportedUint64() + { + // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. + SqlDataTypeTest.create() + .addRoundTrip("UInt64", "-1", createDecimalType(20), "CAST('18446744073709551615' AS decimal(20, 0))") + .addRoundTrip("UInt64", "18446744073709551616", createDecimalType(20), "CAST('0' AS decimal(20, 0))") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_uint64")); + + // Prevent writing incorrect results in the connector + try (TestTable table = new TestTable(onRemoteDatabase(), "tpch.test_unsupported_uint64", "(value UInt64) ENGINE=Log")) { + assertQueryFails( + format("INSERT INTO %s VALUES (CAST('-1' AS decimal(20, 0)))", table.getName()), + "Value must be between 0 and 18446744073709551615 in ClickHouse: -1"); + assertQueryFails( + format("INSERT INTO %s VALUES (CAST('18446744073709551616' AS decimal(20, 0)))", table.getName()), + "Value must be between 0 and 18446744073709551615 in ClickHouse: 18446744073709551616"); + } + } + + @Test + public void testReal() + { + SqlDataTypeTest.create() + .addRoundTrip("real", "12.5", REAL, "REAL '12.5'") + .addRoundTrip("real", "nan()", REAL, "CAST(nan() AS REAL)") + .addRoundTrip("real", "-infinity()", REAL, "CAST(-infinity() AS REAL)") + .addRoundTrip("real", "+infinity()", REAL, "CAST(+infinity() AS REAL)") + .addRoundTrip("real", "NULL", REAL, "CAST(NULL AS REAL)") + .execute(getQueryRunner(), trinoCreateAsSelect("trino_test_real")); + + SqlDataTypeTest.create() + .addRoundTrip("real", "12.5", REAL, "REAL '12.5'") + .addRoundTrip("real", "nan", REAL, "CAST(nan() AS REAL)") + .addRoundTrip("real", "-inf", REAL, "CAST(-infinity() AS REAL)") + .addRoundTrip("real", "+inf", REAL, "CAST(+infinity() AS REAL)") + .addRoundTrip("Nullable(real)", "NULL", REAL, "CAST(NULL AS REAL)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_real")); + } + + @Test + public void testDouble() + { + SqlDataTypeTest.create() + .addRoundTrip("double", "3.1415926835", DOUBLE, "DOUBLE '3.1415926835'") + .addRoundTrip("double", "1.79769E308", DOUBLE, "DOUBLE '1.79769E308'") + .addRoundTrip("double", "2.225E-307", DOUBLE, "DOUBLE '2.225E-307'") + + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_double")) + + .addRoundTrip("double", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)") + + .execute(getQueryRunner(), trinoCreateAsSelect("trino_test_double")); + + SqlDataTypeTest.create() + .addRoundTrip("Nullable(double)", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.trino_test_nullable_double")); + } + + @Test + public void testDecimal() + { + SqlDataTypeTest.create() + .addRoundTrip("decimal(3, 0)", "CAST('193' AS decimal(3, 0))", createDecimalType(3, 0), "CAST('193' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 0)", "CAST('19' AS decimal(3, 0))", createDecimalType(3, 0), "CAST('19' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 0)", "CAST('-193' AS decimal(3, 0))", createDecimalType(3, 0), "CAST('-193' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 1)", "CAST('10.0' AS decimal(3, 1))", createDecimalType(3, 1), "CAST('10.0' AS decimal(3, 1))") + .addRoundTrip("decimal(3, 1)", "CAST('10.1' AS decimal(3, 1))", createDecimalType(3, 1), "CAST('10.1' AS decimal(3, 1))") + .addRoundTrip("decimal(3, 1)", "CAST('-10.1' AS decimal(3, 1))", createDecimalType(3, 1), "CAST('-10.1' AS decimal(3, 1))") + .addRoundTrip("decimal(4, 2)", "CAST('2' AS decimal(4, 2))", createDecimalType(4, 2), "CAST('2' AS decimal(4, 2))") + .addRoundTrip("decimal(4, 2)", "CAST('2.3' AS decimal(4, 2))", createDecimalType(4, 2), "CAST('2.3' AS decimal(4, 2))") + .addRoundTrip("decimal(24, 2)", "CAST('2' AS decimal(24, 2))", createDecimalType(24, 2), "CAST('2' AS decimal(24, 2))") + .addRoundTrip("decimal(24, 2)", "CAST('2.3' AS decimal(24, 2))", createDecimalType(24, 2), "CAST('2.3' AS decimal(24, 2))") + .addRoundTrip("decimal(24, 2)", "CAST('123456789.3' AS decimal(24, 2))", createDecimalType(24, 2), "CAST('123456789.3' AS decimal(24, 2))") + .addRoundTrip("decimal(24, 4)", "CAST('12345678901234567890.31' AS decimal(24, 4))", createDecimalType(24, 4), "CAST('12345678901234567890.31' AS decimal(24, 4))") + .addRoundTrip("decimal(30, 5)", "CAST('3141592653589793238462643.38327' AS decimal(30, 5))", createDecimalType(30, 5), "CAST('3141592653589793238462643.38327' AS decimal(30, 5))") + .addRoundTrip("decimal(30, 5)", "CAST('-3141592653589793238462643.38327' AS decimal(30, 5))", createDecimalType(30, 5), "CAST('-3141592653589793238462643.38327' AS decimal(30, 5))") + .addRoundTrip("decimal(38, 0)", "CAST('27182818284590452353602874713526624977' AS decimal(38, 0))", createDecimalType(38, 0), "CAST('27182818284590452353602874713526624977' AS decimal(38, 0))") + .addRoundTrip("decimal(38, 0)", "CAST('-27182818284590452353602874713526624977' AS decimal(38, 0))", createDecimalType(38, 0), "CAST('-27182818284590452353602874713526624977' AS decimal(38, 0))") + + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_decimal")) + + .addRoundTrip("decimal(3, 1)", "NULL", createDecimalType(3, 1), "CAST(NULL AS decimal(3,1))") + .addRoundTrip("decimal(30, 5)", "NULL", createDecimalType(30, 5), "CAST(NULL AS decimal(30,5))") + + .execute(getQueryRunner(), trinoCreateAsSelect("test_decimal")); + + SqlDataTypeTest.create() + .addRoundTrip("Nullable(decimal(3, 1))", "NULL", createDecimalType(3, 1), "CAST(NULL AS decimal(3,1))") + .addRoundTrip("Nullable(decimal(30, 5))", "NULL", createDecimalType(30, 5), "CAST(NULL AS decimal(30,5))") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_nullable_decimal")); + } + + @Test + public void testClickHouseChar() + { + // ClickHouse char is FixedString, which is arbitrary bytes + SqlDataTypeTest.create() + // plain + .addRoundTrip("char(10)", "'text_a'", VARBINARY, "to_utf8('text_a')") + .addRoundTrip("char(255)", "'text_b'", VARBINARY, "to_utf8('text_b')") + .addRoundTrip("char(5)", "'攻殻機動隊'", VARBINARY, "to_utf8('攻殻機動隊')") + .addRoundTrip("char(32)", "'攻殻機動隊'", VARBINARY, "to_utf8('攻殻機動隊')") + .addRoundTrip("char(1)", "'😂'", VARBINARY, "to_utf8('😂')") + .addRoundTrip("char(77)", "'Ну, погоди!'", VARBINARY, "to_utf8('Ну, погоди!')") + // nullable + .addRoundTrip("Nullable(char(10))", "NULL", VARBINARY, "CAST(NULL AS varbinary)") + .addRoundTrip("Nullable(char(10))", "'text_a'", VARBINARY, "to_utf8('text_a')") + .addRoundTrip("Nullable(char(1))", "'😂'", VARBINARY, "to_utf8('😂')") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_char")); + + // Set map_string_as_varchar session property as true + SqlDataTypeTest.create() + // plain + .addRoundTrip("char(10)", "'text_a'", VARCHAR, "CAST('text_a' AS varchar)") + .addRoundTrip("char(255)", "'text_b'", VARCHAR, "CAST('text_b' AS varchar)") + .addRoundTrip("char(5)", "'攻殻機動隊'", VARCHAR, "CAST('攻殻機動隊' AS varchar)") + .addRoundTrip("char(32)", "'攻殻機動隊'", VARCHAR, "CAST('攻殻機動隊' AS varchar)") + .addRoundTrip("char(1)", "'😂'", VARCHAR, "CAST('😂' AS varchar)") + .addRoundTrip("char(77)", "'Ну, погоди!'", VARCHAR, "CAST('Ну, погоди!' AS varchar)") + // nullable + .addRoundTrip("Nullable(char(10))", "NULL", VARCHAR, "CAST(NULL AS varchar)") + .addRoundTrip("Nullable(char(10))", "'text_a'", VARCHAR, "CAST('text_a' AS varchar)") + .addRoundTrip("Nullable(char(1))", "'😂'", VARCHAR, "CAST('😂' AS varchar)") + .execute(getQueryRunner(), mapStringAsVarcharSession(), clickhouseCreateAndInsert("tpch.test_char")); + } + + @Test + public void testClickHouseFixedString() + { + SqlDataTypeTest.create() + // plain + .addRoundTrip("FixedString(10)", "'c12345678b'", VARBINARY, "to_utf8('c12345678b')") + .addRoundTrip("FixedString(10)", "'c123'", VARBINARY, "to_utf8('c123\0\0\0\0\0\0')") + // nullable + .addRoundTrip("Nullable(FixedString(10))", "NULL", VARBINARY, "CAST(NULL AS varbinary)") + .addRoundTrip("Nullable(FixedString(10))", "'c12345678b'", VARBINARY, "to_utf8('c12345678b')") + .addRoundTrip("Nullable(FixedString(10))", "'c123'", VARBINARY, "to_utf8('c123\0\0\0\0\0\0')") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_fixed_string")); + + // Set map_string_as_varchar session property as true + SqlDataTypeTest.create() + // plain + .addRoundTrip("FixedString(10)", "'c12345678b'", VARCHAR, "CAST('c12345678b' AS varchar)") + .addRoundTrip("FixedString(10)", "'c123'", VARCHAR, "CAST('c123\0\0\0\0\0\0' AS varchar)") + // nullable + .addRoundTrip("Nullable(FixedString(10))", "NULL", VARCHAR, "CAST(NULL AS varchar)") + .addRoundTrip("Nullable(FixedString(10))", "'c12345678b'", VARCHAR, "CAST('c12345678b' AS varchar)") + .addRoundTrip("Nullable(FixedString(10))", "'c123'", VARCHAR, "CAST('c123\0\0\0\0\0\0' AS varchar)") + .execute(getQueryRunner(), mapStringAsVarcharSession(), clickhouseCreateAndInsert("tpch.test_fixed_string")); + } + + @Test + public void testTrinoChar() + { + SqlDataTypeTest.create() + .addRoundTrip("char(10)", "NULL", VARBINARY, "CAST(NULL AS varbinary)") + .addRoundTrip("char(10)", "'text_a'", VARBINARY, "to_utf8('text_a')") + .addRoundTrip("char(255)", "'text_b'", VARBINARY, "to_utf8('text_b')") + .addRoundTrip("char(5)", "'攻殻機動隊'", VARBINARY, "to_utf8('攻殻機動隊')") + .addRoundTrip("char(32)", "'攻殻機動隊'", VARBINARY, "to_utf8('攻殻機動隊')") + .addRoundTrip("char(1)", "'😂'", VARBINARY, "to_utf8('😂')") + .addRoundTrip("char(77)", "'Ну, погоди!'", VARBINARY, "to_utf8('Ну, погоди!')") + .execute(getQueryRunner(), trinoCreateAsSelect("test_char")) + .execute(getQueryRunner(), trinoCreateAsSelect(mapStringAsVarcharSession(), "test_char")); + + // Set map_string_as_varchar session property as true + SqlDataTypeTest.create() + .addRoundTrip("char(10)", "NULL", VARCHAR, "CAST(NULL AS varchar)") + .addRoundTrip("char(10)", "'text_a'", VARCHAR, "CAST('text_a' AS varchar)") + .addRoundTrip("char(255)", "'text_b'", VARCHAR, "CAST('text_b' AS varchar)") + .addRoundTrip("char(5)", "'攻殻機動隊'", VARCHAR, "CAST('攻殻機動隊' AS varchar)") + .addRoundTrip("char(32)", "'攻殻機動隊'", VARCHAR, "CAST('攻殻機動隊' AS varchar)") + .addRoundTrip("char(1)", "'😂'", VARCHAR, "CAST('😂' AS varchar)") + .addRoundTrip("char(77)", "'Ну, погоди!'", VARCHAR, "CAST('Ну, погоди!' AS varchar)") + .execute(getQueryRunner(), mapStringAsVarcharSession(), trinoCreateAsSelect("test_char")) + .execute(getQueryRunner(), mapStringAsVarcharSession(), trinoCreateAsSelect(mapStringAsVarcharSession(), "test_char")); + } + + @Test + public void testClickHouseVarchar() + { + // TODO add more test cases + // ClickHouse varchar is String, which is arbitrary bytes + SqlDataTypeTest.create() + // plain + .addRoundTrip("varchar(30)", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") + // nullable + .addRoundTrip("Nullable(varchar(30))", "NULL", VARBINARY, "CAST(NULL AS varbinary)") + .addRoundTrip("Nullable(varchar(30))", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_varchar")); + + // Set map_string_as_varchar session property as true + SqlDataTypeTest.create() + // plain + .addRoundTrip("varchar(30)", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") + // nullable + .addRoundTrip("Nullable(varchar(30))", "NULL", VARCHAR, "CAST(NULL AS varchar)") + .addRoundTrip("Nullable(varchar(30))", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") + .execute(getQueryRunner(), mapStringAsVarcharSession(), clickhouseCreateAndInsert("tpch.test_varchar")); + } + + @Test + public void testClickHouseString() + { + // TODO add more test cases + SqlDataTypeTest.create() + // plain + .addRoundTrip("String", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") + // nullable + .addRoundTrip("Nullable(String)", "NULL", VARBINARY, "CAST(NULL AS varbinary)") + .addRoundTrip("Nullable(String)", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_varchar")); + + // Set map_string_as_varchar session property as true + SqlDataTypeTest.create() + // plain + .addRoundTrip("String", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") + // nullable + .addRoundTrip("Nullable(String)", "NULL", VARCHAR, "CAST(NULL AS varchar)") + .addRoundTrip("Nullable(String)", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") + .execute(getQueryRunner(), mapStringAsVarcharSession(), clickhouseCreateAndInsert("tpch.test_varchar")); + } + + @Test + public void testTrinoVarchar() + { + SqlDataTypeTest.create() + .addRoundTrip("varchar(30)", "NULL", VARBINARY, "CAST(NULL AS varbinary)") + .addRoundTrip("varchar(30)", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") + .execute(getQueryRunner(), trinoCreateAsSelect("test_varchar")) + .execute(getQueryRunner(), trinoCreateAsSelect(mapStringAsVarcharSession(), "test_varchar")); + + // Set map_string_as_varchar session property as true + SqlDataTypeTest.create() + .addRoundTrip("varchar(30)", "NULL", VARCHAR, "CAST(NULL AS varchar)") + .addRoundTrip("varchar(30)", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") + .execute(getQueryRunner(), mapStringAsVarcharSession(), trinoCreateAsSelect("test_varchar")) + .execute(getQueryRunner(), mapStringAsVarcharSession(), trinoCreateAsSelect(mapStringAsVarcharSession(), "test_varchar")); + } + + @Test + public void testTrinoVarbinary() + { + SqlDataTypeTest.create() + .addRoundTrip("varbinary", "NULL", VARBINARY, "CAST(NULL AS varbinary)") + .addRoundTrip("varbinary", "X''", VARBINARY, "X''") + .addRoundTrip("varbinary", "X'68656C6C6F'", VARBINARY, "to_utf8('hello')") + .addRoundTrip("varbinary", "X'5069C4996B6E6120C582C4856B61207720E69DB1E4BAACE983BD'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") + .addRoundTrip("varbinary", "X'4261672066756C6C206F6620F09F92B0'", VARBINARY, "to_utf8('Bag full of 💰')") + .addRoundTrip("varbinary", "X'0001020304050607080DF9367AA7000000'", VARBINARY, "X'0001020304050607080DF9367AA7000000'") // non-text + .addRoundTrip("varbinary", "X'000000000000'", VARBINARY, "X'000000000000'") + .execute(getQueryRunner(), trinoCreateAsSelect("test_varbinary")); + } + + @Test(dataProvider = "sessionZonesDataProvider") + public void testDate(ZoneId sessionZone) + { + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) + .build(); + SqlDataTypeTest.create() + .addRoundTrip("date", "DATE '1970-02-03'", DATE, "DATE '1970-02-03'") + .addRoundTrip("date", "DATE '2017-07-01'", DATE, "DATE '2017-07-01'") // summer on northern hemisphere (possible DST) + .addRoundTrip("date", "DATE '2017-01-01'", DATE, "DATE '2017-01-01'") // winter on northern hemisphere (possible DST on southern hemisphere) + .addRoundTrip("date", "DATE '1970-01-01'", DATE, "DATE '1970-01-01'") + .addRoundTrip("date", "DATE '1983-04-01'", DATE, "DATE '1983-04-01'") + .addRoundTrip("date", "DATE '1983-10-01'", DATE, "DATE '1983-10-01'") + .execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_date")) + .execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_date")) + .execute(getQueryRunner(), session, trinoCreateAsSelect("test_date")) + .execute(getQueryRunner(), session, trinoCreateAndInsert(session, "test_date")) + .execute(getQueryRunner(), session, trinoCreateAndInsert("test_date")); + + // Null + SqlDataTypeTest.create() + .addRoundTrip("date", "NULL", DATE, "CAST(NULL AS DATE)") + .execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_date")) + .execute(getQueryRunner(), session, trinoCreateAsSelect("test_date")) + .execute(getQueryRunner(), session, trinoCreateAndInsert(session, "test_date")) + .execute(getQueryRunner(), session, trinoCreateAndInsert("test_date")); + SqlDataTypeTest.create() + .addRoundTrip("Nullable(date)", "NULL", DATE, "CAST(NULL AS DATE)") + .execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_date")); + } + + @Test(dataProvider = "clickHouseDateMinMaxValuesDataProvider") + public void testClickHouseDateMinMaxValues(String date) + { + SqlDataTypeTest dateTests = SqlDataTypeTest.create() + .addRoundTrip("date", format("DATE '%s'", date), DATE, format("DATE '%s'", date)); + + for (Object[] timeZoneIds : sessionZonesDataProvider()) { + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(((ZoneId) timeZoneIds[0]).getId())) + .build(); + dateTests + .execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_date")) + .execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_date")) + .execute(getQueryRunner(), session, trinoCreateAsSelect("test_date")) + .execute(getQueryRunner(), session, trinoCreateAndInsert(session, "test_date")) + .execute(getQueryRunner(), session, trinoCreateAndInsert("test_date")); + } + } + + @DataProvider + public Object[][] clickHouseDateMinMaxValuesDataProvider() + { + return new Object[][] { + {"1970-01-01"}, // min value in ClickHouse + {"2106-02-07"}, // max value in ClickHouse + }; + } + + @Test(dataProvider = "unsupportedClickHouseDateValuesDataProvider") + public void testUnsupportedDate(String unsupportedDate) + { + String minSupportedDate = (String) clickHouseDateMinMaxValuesDataProvider()[0][0]; + String maxSupportedDate = (String) clickHouseDateMinMaxValuesDataProvider()[1][0]; + + try (TestTable table = new TestTable(getQueryRunner()::execute, "test_unsupported_date", "(dt date)")) { + assertQueryFails( + format("INSERT INTO %s VALUES (DATE '%s')", table.getName(), unsupportedDate), + format("Date must be between %s and %s in ClickHouse: %s", minSupportedDate, maxSupportedDate, unsupportedDate)); + } + + try (TestTable table = new TestTable(onRemoteDatabase(), "tpch.test_unsupported_date", "(dt date) ENGINE=Log")) { + onRemoteDatabase().execute(format("INSERT INTO %s VALUES ('%s')", table.getName(), unsupportedDate)); + assertQuery(format("SELECT dt <> DATE '%s' FROM %s", unsupportedDate, table.getName()), "SELECT true"); // Inserting an unsupported date in ClickHouse will turn it into another date + } + } + + @DataProvider + public Object[][] unsupportedClickHouseDateValuesDataProvider() + { + return new Object[][] { + {"1969-12-31"}, // min - 1 day + {"2106-02-08"}, // max + 1 day + }; + } + + @Test(dataProvider = "sessionZonesDataProvider") + public void testTimestamp(ZoneId sessionZone) + { + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) + .build(); + + SqlDataTypeTest.create() + .addRoundTrip("timestamp(0)", "timestamp '1986-01-01 00:13:07'", createTimestampType(0), "TIMESTAMP '1986-01-01 00:13:07'") // time gap in Kathmandu + .addRoundTrip("timestamp(0)", "timestamp '2018-03-25 03:17:17'", createTimestampType(0), "TIMESTAMP '2018-03-25 03:17:17'") // time gap in Vilnius + .addRoundTrip("timestamp(0)", "timestamp '2018-10-28 01:33:17'", createTimestampType(0), "TIMESTAMP '2018-10-28 01:33:17'") // time doubled in JVM zone + .addRoundTrip("timestamp(0)", "timestamp '2018-10-28 03:33:33'", createTimestampType(0), "TIMESTAMP '2018-10-28 03:33:33'") // time double in Vilnius + .execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_timestamp")) + .execute(getQueryRunner(), session, trinoCreateAsSelect("test_timestamp")) + .execute(getQueryRunner(), session, trinoCreateAndInsert(session, "test_timestamp")) + .execute(getQueryRunner(), session, trinoCreateAndInsert("test_timestamp")); + + timestampTest("timestamp") + .execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_timestamp")); + timestampTest("datetime") + .execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_datetime")); + } + + private SqlDataTypeTest timestampTest(String inputType) + { + return unsupportedTimestampBecomeUnexpectedValueTest(inputType) + .addRoundTrip(inputType, "'1986-01-01 00:13:07'", createTimestampType(0), "TIMESTAMP '1986-01-01 00:13:07'") // time gap in Kathmandu + .addRoundTrip(inputType, "'2018-03-25 03:17:17'", createTimestampType(0), "TIMESTAMP '2018-03-25 03:17:17'") // time gap in Vilnius + .addRoundTrip(inputType, "'2018-10-28 01:33:17'", createTimestampType(0), "TIMESTAMP '2018-10-28 01:33:17'") // time doubled in JVM zone + .addRoundTrip(inputType, "'2018-10-28 03:33:33'", createTimestampType(0), "TIMESTAMP '2018-10-28 03:33:33'") // time double in Vilnius + .addRoundTrip(format("Nullable(%s)", inputType), "NULL", createTimestampType(0), "CAST(NULL AS TIMESTAMP(0))"); + } + + protected SqlDataTypeTest unsupportedTimestampBecomeUnexpectedValueTest(String inputType) + { + return SqlDataTypeTest.create() + .addRoundTrip(inputType, "'1969-12-31 23:59:59'", createTimestampType(0), "TIMESTAMP '1970-01-01 23:59:59'"); // unsupported timestamp become 1970-01-01 23:59:59 + } + + @Test(dataProvider = "clickHouseDateTimeMinMaxValuesDataProvider") + public void testClickHouseDateTimeMinMaxValues(String timestamp) + { + SqlDataTypeTest dateTests1 = SqlDataTypeTest.create() + .addRoundTrip("timestamp(0)", format("timestamp '%s'", timestamp), createTimestampType(0), format("TIMESTAMP '%s'", timestamp)); + SqlDataTypeTest dateTests2 = SqlDataTypeTest.create() + .addRoundTrip("timestamp", format("'%s'", timestamp), createTimestampType(0), format("TIMESTAMP '%s'", timestamp)); + SqlDataTypeTest dateTests3 = SqlDataTypeTest.create() + .addRoundTrip("datetime", format("'%s'", timestamp), createTimestampType(0), format("TIMESTAMP '%s'", timestamp)); + + for (Object[] timeZoneIds : sessionZonesDataProvider()) { + Session session = Session.builder(getSession()) + .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(((ZoneId) timeZoneIds[0]).getId())) + .build(); + dateTests1 + .execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_timestamp")) + .execute(getQueryRunner(), session, trinoCreateAsSelect("test_timestamp")) + .execute(getQueryRunner(), session, trinoCreateAndInsert(session, "test_timestamp")) + .execute(getQueryRunner(), session, trinoCreateAndInsert("test_timestamp")); + dateTests2.execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_timestamp")); + dateTests3.execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_datetime")); + } + } + + @DataProvider + public Object[][] clickHouseDateTimeMinMaxValuesDataProvider() + { + return new Object[][] { + {"1970-01-01 00:00:00"}, // min value in ClickHouse + {"2106-02-06 06:28:15"}, // max value in ClickHouse + }; + } + + @Test(dataProvider = "unsupportedTimestampDataProvider") + public void testUnsupportedTimestamp(String unsupportedTimestamp) + { + String minSupportedTimestamp = (String) clickHouseDateTimeMinMaxValuesDataProvider()[0][0]; + String maxSupportedTimestamp = (String) clickHouseDateTimeMinMaxValuesDataProvider()[1][0]; + + try (TestTable table = new TestTable(getQueryRunner()::execute, "test_unsupported_timestamp", "(dt timestamp(0))")) { + assertQueryFails( + format("INSERT INTO %s VALUES (TIMESTAMP '%s')", table.getName(), unsupportedTimestamp), + format("Timestamp must be between %s and %s in ClickHouse: %s", minSupportedTimestamp, maxSupportedTimestamp, unsupportedTimestamp)); + } + + try (TestTable table = new TestTable(onRemoteDatabase(), "tpch.test_unsupported_timestamp", "(dt datetime) ENGINE=Log")) { + onRemoteDatabase().execute(format("INSERT INTO %s VALUES ('%s')", table.getName(), unsupportedTimestamp)); + assertQuery(format("SELECT dt <> TIMESTAMP '%s' FROM %s", unsupportedTimestamp, table.getName()), "SELECT true"); // Inserting an unsupported datetime in ClickHouse will turn it into another datetime + } + } + + @DataProvider + public Object[][] unsupportedTimestampDataProvider() + { + return new Object[][] { + {"1969-12-31 23:59:59"}, // min - 1 second + {"2106-02-06 06:28:16"}, // max + 1 second + }; + } + + @DataProvider + public Object[][] sessionZonesDataProvider() + { + return new Object[][] { + {UTC}, + {jvmZone}, + // using two non-JVM zones so that we don't need to worry what ClickHouse system zone is + {vilnius}, + {kathmandu}, + {ZoneId.of(TestingSession.DEFAULT_TIME_ZONE_KEY.getId())}, + }; + } + + @Test + public void testEnum() + { + SqlDataTypeTest.create() + .addRoundTrip("Enum('hello' = 1, 'world' = 2)", "'hello'", createUnboundedVarcharType(), "VARCHAR 'hello'") + .addRoundTrip("Enum('hello' = 1, 'world' = 2)", "'world'", createUnboundedVarcharType(), "VARCHAR 'world'") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_enum")); + } + + @Test + public void testUuid() + { + SqlDataTypeTest.create() + .addRoundTrip("Nullable(UUID)", "NULL", UuidType.UUID, "CAST(NULL AS UUID)") + .addRoundTrip("Nullable(UUID)", "'114514ea-0601-1981-1142-e9b55b0abd6d'", UuidType.UUID, "CAST('114514ea-0601-1981-1142-e9b55b0abd6d' AS UUID)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("default.ck_test_uuid")); + + SqlDataTypeTest.create() + .addRoundTrip("CAST(NULL AS UUID)", "cast(NULL as UUID)") + .addRoundTrip("UUID '114514ea-0601-1981-1142-e9b55b0abd6d'", "CAST('114514ea-0601-1981-1142-e9b55b0abd6d' AS UUID)") + .execute(getQueryRunner(), trinoCreateAsSelect("default.ck_test_uuid")) + .execute(getQueryRunner(), trinoCreateAndInsert("default.ck_test_uuid")); + } + + @Test + public void testIp() + { + SqlDataTypeTest.create() + .addRoundTrip("IPv4", "'0.0.0.0'", IPADDRESS, "IPADDRESS '0.0.0.0'") + .addRoundTrip("IPv4", "'116.253.40.133'", IPADDRESS, "IPADDRESS '116.253.40.133'") + .addRoundTrip("IPv4", "'255.255.255.255'", IPADDRESS, "IPADDRESS '255.255.255.255'") + .addRoundTrip("IPv6", "'::'", IPADDRESS, "IPADDRESS '::'") + .addRoundTrip("IPv6", "'2001:44c8:129:2632:33:0:252:2'", IPADDRESS, "IPADDRESS '2001:44c8:129:2632:33:0:252:2'") + .addRoundTrip("IPv6", "'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'", IPADDRESS, "IPADDRESS 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'") + .addRoundTrip("Nullable(IPv4)", "NULL", IPADDRESS, "CAST(NULL AS IPADDRESS)") + .addRoundTrip("Nullable(IPv6)", "NULL", IPADDRESS, "CAST(NULL AS IPADDRESS)") + .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_ip")); + + SqlDataTypeTest.create() + .addRoundTrip("IPv4", "IPADDRESS '0.0.0.0'", IPADDRESS, "IPADDRESS '0.0.0.0'") + .addRoundTrip("IPv4", "IPADDRESS '116.253.40.133'", IPADDRESS, "IPADDRESS '116.253.40.133'") + .addRoundTrip("IPv4", "IPADDRESS '255.255.255.255'", IPADDRESS, "IPADDRESS '255.255.255.255'") + .addRoundTrip("IPv6", "IPADDRESS '::'", IPADDRESS, "IPADDRESS '::'") + .addRoundTrip("IPv6", "IPADDRESS '2001:44c8:129:2632:33:0:252:2'", IPADDRESS, "IPADDRESS '2001:44c8:129:2632:33:0:252:2'") + .addRoundTrip("IPv6", "IPADDRESS 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'", IPADDRESS, "IPADDRESS 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'") + .addRoundTrip("Nullable(IPv4)", "NULL", IPADDRESS, "CAST(NULL AS IPADDRESS)") + .addRoundTrip("Nullable(IPv6)", "NULL", IPADDRESS, "CAST(NULL AS IPADDRESS)") + .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_ip")); + } + + protected static Session mapStringAsVarcharSession() + { + return testSessionBuilder() + .setCatalog("clickhouse") + .setSchema(TPCH_SCHEMA) + .setCatalogSessionProperty("clickhouse", "map_string_as_varchar", "true") + .build(); + } + + protected DataSetup trinoCreateAsSelect(String tableNamePrefix) + { + return trinoCreateAsSelect(getSession(), tableNamePrefix); + } + + protected DataSetup trinoCreateAsSelect(Session session, String tableNamePrefix) + { + return new CreateAsSelectDataSetup(new TrinoSqlExecutor(getQueryRunner(), session), tableNamePrefix); + } + + protected DataSetup trinoCreateAndInsert(String tableNamePrefix) + { + return trinoCreateAndInsert(getSession(), tableNamePrefix); + } + + protected DataSetup trinoCreateAndInsert(Session session, String tableNamePrefix) + { + return new CreateAndInsertDataSetup(new TrinoSqlExecutor(getQueryRunner(), session), tableNamePrefix); + } + + protected DataSetup clickhouseCreateAndInsert(String tableNamePrefix) + { + return new CreateAndInsertDataSetup(new ClickHouseSqlExecutor(onRemoteDatabase()), tableNamePrefix); + } + + protected DataSetup clickhouseCreateAndTrinoInsert(String tableNamePrefix) + { + return new CreateAndTrinoInsertDataSetup(new ClickHouseSqlExecutor(onRemoteDatabase()), new TrinoSqlExecutor(getQueryRunner()), tableNamePrefix); + } + + protected SqlExecutor onRemoteDatabase() + { + return clickhouseServer::execute; + } +} diff --git a/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseLatestConnectorTest.java b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseLatestConnectorTest.java index 45550e4b6168..47b0e22ff467 100644 --- a/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseLatestConnectorTest.java +++ b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseLatestConnectorTest.java @@ -44,4 +44,25 @@ protected OptionalInt maxTableNameLength() // The numeric value depends on file system return OptionalInt.of(255 - ".sql.detached".length()); } + + @Override + protected String errorMessageForCreateTableAsSelectNegativeDate(String date) + { + // Override because the DateTime range was expanded in version 21.4 and later + return "Date must be between 1970-01-01 and 2149-06-06 in ClickHouse: " + date; + } + + @Override + protected String errorMessageForInsertNegativeDate(String date) + { + // Override because the DateTime range was expanded in version 21.4 and later + return "Date must be between 1970-01-01 and 2149-06-06 in ClickHouse: " + date; + } + + @Override + protected String errorMessageForDateYearOfEraPredicate(String date) + { + // Override because the DateTime range was expanded in version 21.4 and later + return "Date must be between 1970-01-01 and 2149-06-06 in ClickHouse: " + date; + } } diff --git a/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseLatestTypeMapping.java b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseLatestTypeMapping.java new file mode 100644 index 000000000000..5208050caaba --- /dev/null +++ b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseLatestTypeMapping.java @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.clickhouse; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.trino.testing.QueryRunner; +import io.trino.testing.datatype.SqlDataTypeTest; +import org.testng.annotations.DataProvider; + +import static io.trino.plugin.clickhouse.ClickHouseQueryRunner.createClickHouseQueryRunner; +import static io.trino.plugin.clickhouse.TestingClickHouseServer.CLICKHOUSE_LATEST_IMAGE; +import static io.trino.spi.type.TimestampType.createTimestampType; + +public class TestClickHouseLatestTypeMapping + extends BaseClickHouseTypeMapping +{ + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + clickhouseServer = closeAfterClass(new TestingClickHouseServer(CLICKHOUSE_LATEST_IMAGE)); + return createClickHouseQueryRunner(clickhouseServer, ImmutableMap.of(), + ImmutableMap.builder() + .put("metadata.cache-ttl", "10m") + .put("metadata.cache-missing", "true") + .buildOrThrow(), + ImmutableList.of()); + } + + @DataProvider + @Override + public Object[][] clickHouseDateMinMaxValuesDataProvider() + { + // Override because the Date range was expanded in version 21.4 and later + return new Object[][] { + {"1970-01-01"}, // min value in ClickHouse + {"2149-06-06"}, // max value in ClickHouse + }; + } + + @DataProvider + @Override + public Object[][] unsupportedClickHouseDateValuesDataProvider() + { + // Override because the Date range was expanded in version 21.4 and later + return new Object[][] { + {"1969-12-31"}, // min - 1 day + {"2149-06-07"}, // max + 1 day + }; + } + + @Override + protected SqlDataTypeTest unsupportedTimestampBecomeUnexpectedValueTest(String inputType) + { + // Override because insert DateTime '1969-12-31 23:59:59' directly in ClickHouse will + // become '1970-01-01 00:00:00' in version 21.4 and later, however in versions prior + // to 21.4 the value will become '1970-01-01 23:59:59'. + return SqlDataTypeTest.create() + .addRoundTrip(inputType, "'1969-12-31 23:59:59'", createTimestampType(0), "TIMESTAMP '1970-01-01 00:00:00'"); + } + + @DataProvider + @Override + public Object[][] clickHouseDateTimeMinMaxValuesDataProvider() + { + // Override because the DateTime range was expanded in version 21.4 and later + return new Object[][] { + {"1970-01-01 00:00:00"}, // min value in ClickHouse + {"2106-02-07 06:28:15"}, // max value in ClickHouse + }; + } + + @DataProvider + @Override + public Object[][] unsupportedTimestampDataProvider() + { + // Override because the DateTime range was expanded in version 21.4 and later + return new Object[][] { + {"1969-12-31 23:59:59"}, // min - 1 second + {"2106-02-07 06:28:16"}, // max + 1 second + }; + } +} diff --git a/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseTypeMapping.java b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseTypeMapping.java index cbbd379825fc..eaf4161bfe92 100644 --- a/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseTypeMapping.java +++ b/plugin/trino-clickhouse/src/test/java/io/trino/plugin/clickhouse/TestClickHouseTypeMapping.java @@ -15,92 +15,13 @@ 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.spi.type.UuidType; -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.CreateAndTrinoInsertDataSetup; -import io.trino.testing.datatype.CreateAsSelectDataSetup; -import io.trino.testing.datatype.DataSetup; -import io.trino.testing.datatype.SqlDataTypeTest; -import io.trino.testing.sql.TestTable; -import io.trino.testing.sql.TrinoSqlExecutor; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; - -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Verify.verify; -import static io.trino.plugin.clickhouse.ClickHouseQueryRunner.TPCH_SCHEMA; import static io.trino.plugin.clickhouse.ClickHouseQueryRunner.createClickHouseQueryRunner; -import static io.trino.spi.type.BigintType.BIGINT; -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.IntegerType.INTEGER; -import static io.trino.spi.type.RealType.REAL; -import static io.trino.spi.type.SmallintType.SMALLINT; -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 io.trino.spi.type.VarcharType.VARCHAR; -import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; -import static io.trino.testing.TestingSession.testSessionBuilder; -import static io.trino.type.IpAddressType.IPADDRESS; -import static java.lang.String.format; -import static java.time.ZoneOffset.UTC; public class TestClickHouseTypeMapping - extends AbstractTestQueryFramework + extends BaseClickHouseTypeMapping { - private final ZoneId jvmZone = ZoneId.systemDefault(); - - // no DST in 1970, but has DST in later years (e.g. 2018) - private final ZoneId vilnius = ZoneId.of("Europe/Vilnius"); - - // minutes offset change since 1970-01-01, no DST - private final ZoneId kathmandu = ZoneId.of("Asia/Kathmandu"); - - private TestingClickHouseServer clickhouseServer; - - @BeforeClass - public void setUp() - { - checkState(jvmZone.getId().equals("America/Bahia_Banderas"), "This test assumes certain JVM time zone"); - LocalDate dateOfLocalTimeChangeForwardAtMidnightInJvmZone = LocalDate.of(1970, 1, 1); - checkIsGap(jvmZone, dateOfLocalTimeChangeForwardAtMidnightInJvmZone.atStartOfDay()); - - LocalDate dateOfLocalTimeChangeForwardAtMidnightInSomeZone = LocalDate.of(1983, 4, 1); - checkIsGap(vilnius, dateOfLocalTimeChangeForwardAtMidnightInSomeZone.atStartOfDay()); - LocalDate dateOfLocalTimeChangeBackwardAtMidnightInSomeZone = LocalDate.of(1983, 10, 1); - checkIsDoubled(vilnius, dateOfLocalTimeChangeBackwardAtMidnightInSomeZone.atStartOfDay().minusMinutes(1)); - - LocalDate timeGapInKathmandu = LocalDate.of(1986, 1, 1); - checkIsGap(kathmandu, timeGapInKathmandu.atStartOfDay()); - } - - private static void checkIsGap(ZoneId zone, LocalDateTime dateTime) - { - verify(isGap(zone, dateTime), "Expected %s to be a gap in %s", dateTime, zone); - } - - private static boolean isGap(ZoneId zone, LocalDateTime dateTime) - { - return zone.getRules().getValidOffsets(dateTime).isEmpty(); - } - - private static void checkIsDoubled(ZoneId zone, LocalDateTime dateTime) - { - verify(zone.getRules().getValidOffsets(dateTime).size() == 2, "Expected %s to be doubled in %s", dateTime, zone); - } - @Override protected QueryRunner createQueryRunner() throws Exception @@ -113,704 +34,4 @@ protected QueryRunner createQueryRunner() .buildOrThrow(), ImmutableList.of()); } - - @Test - public void testTinyint() - { - SqlDataTypeTest.create() - .addRoundTrip("tinyint", "-128", TINYINT, "TINYINT '-128'") // min value in ClickHouse and Trino - .addRoundTrip("tinyint", "5", TINYINT, "TINYINT '5'") - .addRoundTrip("tinyint", "127", TINYINT, "TINYINT '127'") // max value in ClickHouse and Trino - .execute(getQueryRunner(), trinoCreateAsSelect("test_tinyint")) - .execute(getQueryRunner(), trinoCreateAndInsert("test_tinyint")) - - .addRoundTrip("Nullable(tinyint)", "NULL", TINYINT, "CAST(NULL AS TINYINT)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_tinyint")); - - SqlDataTypeTest.create() - .addRoundTrip("tinyint", "NULL", TINYINT, "CAST(NULL AS TINYINT)") - .execute(getQueryRunner(), trinoCreateAsSelect("test_tinyint")) - .execute(getQueryRunner(), trinoCreateAndInsert("test_tinyint")); - } - - @Test - public void testUnsupportedTinyint() - { - // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. - SqlDataTypeTest.create() - .addRoundTrip("tinyint", "-129", TINYINT, "TINYINT '127'") - .addRoundTrip("tinyint", "128", TINYINT, "TINYINT '-128'") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_tinyint")); - } - - @Test - public void testSmallint() - { - SqlDataTypeTest.create() - .addRoundTrip("smallint", "-32768", SMALLINT, "SMALLINT '-32768'") // min value in ClickHouse and Trino - .addRoundTrip("smallint", "32456", SMALLINT, "SMALLINT '32456'") - .addRoundTrip("smallint", "32767", SMALLINT, "SMALLINT '32767'") // max value in ClickHouse and Trino - .execute(getQueryRunner(), trinoCreateAsSelect("test_smallint")) - .execute(getQueryRunner(), trinoCreateAndInsert("test_smallint")) - - .addRoundTrip("Nullable(smallint)", "NULL", SMALLINT, "CAST(NULL AS SMALLINT)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_smallint")); - - SqlDataTypeTest.create() - .addRoundTrip("smallint", "NULL", SMALLINT, "CAST(NULL AS SMALLINT)") - .execute(getQueryRunner(), trinoCreateAsSelect("test_smallint")) - .execute(getQueryRunner(), trinoCreateAndInsert("test_smallint")); - } - - @Test - public void testUnsupportedSmallint() - { - // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. - SqlDataTypeTest.create() - .addRoundTrip("smallint", "-32769", SMALLINT, "SMALLINT '32767'") - .addRoundTrip("smallint", "32768", SMALLINT, "SMALLINT '-32768'") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_smallint")); - } - - @Test - public void testInteger() - { - SqlDataTypeTest.create() - .addRoundTrip("integer", "-2147483648", INTEGER, "-2147483648") // min value in ClickHouse and Trino - .addRoundTrip("integer", "1234567890", INTEGER, "1234567890") - .addRoundTrip("integer", "2147483647", INTEGER, "2147483647") // max value in ClickHouse and Trino - .execute(getQueryRunner(), trinoCreateAsSelect("test_int")) - .execute(getQueryRunner(), trinoCreateAndInsert("test_int")) - - .addRoundTrip("Nullable(integer)", "NULL", INTEGER, "CAST(NULL AS INTEGER)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_int")); - - SqlDataTypeTest.create() - .addRoundTrip("integer", "NULL", INTEGER, "CAST(NULL AS INTEGER)") - .execute(getQueryRunner(), trinoCreateAsSelect("test_int")) - .execute(getQueryRunner(), trinoCreateAndInsert("test_int")); - } - - @Test - public void testUnsupportedInteger() - { - // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. - SqlDataTypeTest.create() - .addRoundTrip("integer", "-2147483649", INTEGER, "INTEGER '2147483647'") - .addRoundTrip("integer", "2147483648", INTEGER, "INTEGER '-2147483648'") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_integer")); - } - - @Test - public void testBigint() - { - SqlDataTypeTest.create() - .addRoundTrip("bigint", "-9223372036854775808", BIGINT, "-9223372036854775808") // min value in ClickHouse and Trino - .addRoundTrip("bigint", "123456789012", BIGINT, "123456789012") - .addRoundTrip("bigint", "9223372036854775807", BIGINT, "9223372036854775807") // max value in ClickHouse and Trino - .execute(getQueryRunner(), trinoCreateAsSelect("test_bigint")) - .execute(getQueryRunner(), trinoCreateAndInsert("test_bigint")) - - .addRoundTrip("Nullable(bigint)", "NULL", BIGINT, "CAST(NULL AS BIGINT)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_bigint")); - - SqlDataTypeTest.create() - .addRoundTrip("bigint", "NULL", BIGINT, "CAST(NULL AS BIGINT)") - .execute(getQueryRunner(), trinoCreateAsSelect("test_bigint")) - .execute(getQueryRunner(), trinoCreateAndInsert("test_bigint")); - } - - @Test - public void testUnsupportedBigint() - { - // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. - SqlDataTypeTest.create() - .addRoundTrip("bigint", "-9223372036854775809", BIGINT, "BIGINT '9223372036854775807'") - .addRoundTrip("bigint", "9223372036854775808", BIGINT, "BIGINT '-9223372036854775808'") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_bigint")); - } - - @Test - public void testUint8() - { - SqlDataTypeTest.create() - .addRoundTrip("UInt8", "0", SMALLINT, "SMALLINT '0'") // min value in ClickHouse - .addRoundTrip("UInt8", "255", SMALLINT, "SMALLINT '255'") // max value in ClickHouse - .addRoundTrip("Nullable(UInt8)", "NULL", SMALLINT, "CAST(null AS SMALLINT)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_uint8")); - - SqlDataTypeTest.create() - .addRoundTrip("UInt8", "0", SMALLINT, "SMALLINT '0'") // min value in ClickHouse - .addRoundTrip("UInt8", "255", SMALLINT, "SMALLINT '255'") // max value in ClickHouse - .addRoundTrip("Nullable(UInt8)", "NULL", SMALLINT, "CAST(null AS SMALLINT)") - .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_uint8")); - } - - @Test - public void testUnsupportedUint8() - { - // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. - SqlDataTypeTest.create() - .addRoundTrip("UInt8", "-1", SMALLINT, "SMALLINT '255'") - .addRoundTrip("UInt8", "256", SMALLINT, "SMALLINT '0'") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_uint8")); - - // Prevent writing incorrect results in the connector - try (TestTable table = new TestTable(clickhouseServer::execute, "tpch.test_unsupported_uint8", "(value UInt8) ENGINE=Log")) { - assertQueryFails( - format("INSERT INTO %s VALUES (-1)", table.getName()), - "Value must be between 0 and 255 in ClickHouse: -1"); - assertQueryFails( - format("INSERT INTO %s VALUES (256)", table.getName()), - "Value must be between 0 and 255 in ClickHouse: 256"); - } - } - - @Test - public void testUint16() - { - SqlDataTypeTest.create() - .addRoundTrip("UInt16", "0", INTEGER, "0") // min value in ClickHouse - .addRoundTrip("UInt16", "65535", INTEGER, "65535") // max value in ClickHouse - .addRoundTrip("Nullable(UInt16)", "NULL", INTEGER, "CAST(null AS INTEGER)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_uint16")); - - SqlDataTypeTest.create() - .addRoundTrip("UInt16", "0", INTEGER, "0") // min value in ClickHouse - .addRoundTrip("UInt16", "65535", INTEGER, "65535") // max value in ClickHouse - .addRoundTrip("Nullable(UInt16)", "NULL", INTEGER, "CAST(null AS INTEGER)") - .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_uint16")); - } - - @Test - public void testUnsupportedUint16() - { - // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. - SqlDataTypeTest.create() - .addRoundTrip("UInt16", "-1", INTEGER, "65535") - .addRoundTrip("UInt16", "65536", INTEGER, "0") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_uint16")); - - // Prevent writing incorrect results in the connector - try (TestTable table = new TestTable(clickhouseServer::execute, "tpch.test_unsupported_uint16", "(value UInt16) ENGINE=Log")) { - assertQueryFails( - format("INSERT INTO %s VALUES (-1)", table.getName()), - "Value must be between 0 and 65535 in ClickHouse: -1"); - assertQueryFails( - format("INSERT INTO %s VALUES (65536)", table.getName()), - "Value must be between 0 and 65535 in ClickHouse: 65536"); - } - } - - @Test - public void testUint32() - { - SqlDataTypeTest.create() - .addRoundTrip("UInt32", "0", BIGINT, "BIGINT '0'") // min value in ClickHouse - .addRoundTrip("UInt32", "4294967295", BIGINT, "BIGINT '4294967295'") // max value in ClickHouse - .addRoundTrip("Nullable(UInt32)", "NULL", BIGINT, "CAST(null AS BIGINT)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_uint32")); - - SqlDataTypeTest.create() - .addRoundTrip("UInt32", "BIGINT '0'", BIGINT, "BIGINT '0'") // min value in ClickHouse - .addRoundTrip("UInt32", "BIGINT '4294967295'", BIGINT, "BIGINT '4294967295'") // max value in ClickHouse - .addRoundTrip("Nullable(UInt32)", "NULL", BIGINT, "CAST(null AS BIGINT)") - .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_uint32")); - } - - @Test - public void testUnsupportedUint32() - { - // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. - SqlDataTypeTest.create() - .addRoundTrip("UInt32", "-1", BIGINT, "BIGINT '4294967295'") - .addRoundTrip("UInt32", "4294967296", BIGINT, "BIGINT '0'") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_uint32")); - - // Prevent writing incorrect results in the connector - try (TestTable table = new TestTable(clickhouseServer::execute, "tpch.test_unsupported_uint32", "(value UInt32) ENGINE=Log")) { - assertQueryFails( - format("INSERT INTO %s VALUES (CAST('-1' AS BIGINT))", table.getName()), - "Value must be between 0 and 4294967295 in ClickHouse: -1"); - assertQueryFails( - format("INSERT INTO %s VALUES (CAST('4294967296' AS BIGINT))", table.getName()), - "Value must be between 0 and 4294967295 in ClickHouse: 4294967296"); - } - } - - @Test - public void testUint64() - { - SqlDataTypeTest.create() - .addRoundTrip("UInt64", "0", createDecimalType(20), "CAST('0' AS decimal(20, 0))") // min value in ClickHouse - .addRoundTrip("UInt64", "18446744073709551615", createDecimalType(20), "CAST('18446744073709551615' AS decimal(20, 0))") // max value in ClickHouse - .addRoundTrip("Nullable(UInt64)", "NULL", createDecimalType(20), "CAST(null AS decimal(20, 0))") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_uint64")); - - SqlDataTypeTest.create() - .addRoundTrip("UInt64", "CAST('0' AS decimal(20, 0))", createDecimalType(20), "CAST('0' AS decimal(20, 0))") // min value in ClickHouse - .addRoundTrip("UInt64", "CAST('18446744073709551615' AS decimal(20, 0))", createDecimalType(20), "CAST('18446744073709551615' AS decimal(20, 0))") // max value in ClickHouse - .addRoundTrip("Nullable(UInt64)", "NULL", createDecimalType(20), "CAST(null AS decimal(20, 0))") - .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_uint64")); - } - - @Test - public void testUnsupportedUint64() - { - // ClickHouse stores incorrect results when the values are out of supported range. This test should be fixed when ClickHouse changes the behavior. - SqlDataTypeTest.create() - .addRoundTrip("UInt64", "-1", createDecimalType(20), "CAST('18446744073709551615' AS decimal(20, 0))") - .addRoundTrip("UInt64", "18446744073709551616", createDecimalType(20), "CAST('0' AS decimal(20, 0))") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_unsupported_uint64")); - - // Prevent writing incorrect results in the connector - try (TestTable table = new TestTable(clickhouseServer::execute, "tpch.test_unsupported_uint64", "(value UInt64) ENGINE=Log")) { - assertQueryFails( - format("INSERT INTO %s VALUES (CAST('-1' AS decimal(20, 0)))", table.getName()), - "Value must be between 0 and 18446744073709551615 in ClickHouse: -1"); - assertQueryFails( - format("INSERT INTO %s VALUES (CAST('18446744073709551616' AS decimal(20, 0)))", table.getName()), - "Value must be between 0 and 18446744073709551615 in ClickHouse: 18446744073709551616"); - } - } - - @Test - public void testReal() - { - SqlDataTypeTest.create() - .addRoundTrip("real", "12.5", REAL, "REAL '12.5'") - .addRoundTrip("real", "nan()", REAL, "CAST(nan() AS REAL)") - .addRoundTrip("real", "-infinity()", REAL, "CAST(-infinity() AS REAL)") - .addRoundTrip("real", "+infinity()", REAL, "CAST(+infinity() AS REAL)") - .addRoundTrip("real", "NULL", REAL, "CAST(NULL AS REAL)") - .execute(getQueryRunner(), trinoCreateAsSelect("trino_test_real")); - - SqlDataTypeTest.create() - .addRoundTrip("real", "12.5", REAL, "REAL '12.5'") - .addRoundTrip("real", "nan", REAL, "CAST(nan() AS REAL)") - .addRoundTrip("real", "-inf", REAL, "CAST(-infinity() AS REAL)") - .addRoundTrip("real", "+inf", REAL, "CAST(+infinity() AS REAL)") - .addRoundTrip("Nullable(real)", "NULL", REAL, "CAST(NULL AS REAL)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_real")); - } - - @Test - public void testDouble() - { - SqlDataTypeTest.create() - .addRoundTrip("double", "3.1415926835", DOUBLE, "DOUBLE '3.1415926835'") - .addRoundTrip("double", "1.79769E308", DOUBLE, "DOUBLE '1.79769E308'") - .addRoundTrip("double", "2.225E-307", DOUBLE, "DOUBLE '2.225E-307'") - - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_double")) - - .addRoundTrip("double", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)") - - .execute(getQueryRunner(), trinoCreateAsSelect("trino_test_double")); - - SqlDataTypeTest.create() - .addRoundTrip("Nullable(double)", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.trino_test_nullable_double")); - } - - @Test - public void testDecimal() - { - SqlDataTypeTest.create() - .addRoundTrip("decimal(3, 0)", "CAST('193' AS decimal(3, 0))", createDecimalType(3, 0), "CAST('193' AS decimal(3, 0))") - .addRoundTrip("decimal(3, 0)", "CAST('19' AS decimal(3, 0))", createDecimalType(3, 0), "CAST('19' AS decimal(3, 0))") - .addRoundTrip("decimal(3, 0)", "CAST('-193' AS decimal(3, 0))", createDecimalType(3, 0), "CAST('-193' AS decimal(3, 0))") - .addRoundTrip("decimal(3, 1)", "CAST('10.0' AS decimal(3, 1))", createDecimalType(3, 1), "CAST('10.0' AS decimal(3, 1))") - .addRoundTrip("decimal(3, 1)", "CAST('10.1' AS decimal(3, 1))", createDecimalType(3, 1), "CAST('10.1' AS decimal(3, 1))") - .addRoundTrip("decimal(3, 1)", "CAST('-10.1' AS decimal(3, 1))", createDecimalType(3, 1), "CAST('-10.1' AS decimal(3, 1))") - .addRoundTrip("decimal(4, 2)", "CAST('2' AS decimal(4, 2))", createDecimalType(4, 2), "CAST('2' AS decimal(4, 2))") - .addRoundTrip("decimal(4, 2)", "CAST('2.3' AS decimal(4, 2))", createDecimalType(4, 2), "CAST('2.3' AS decimal(4, 2))") - .addRoundTrip("decimal(24, 2)", "CAST('2' AS decimal(24, 2))", createDecimalType(24, 2), "CAST('2' AS decimal(24, 2))") - .addRoundTrip("decimal(24, 2)", "CAST('2.3' AS decimal(24, 2))", createDecimalType(24, 2), "CAST('2.3' AS decimal(24, 2))") - .addRoundTrip("decimal(24, 2)", "CAST('123456789.3' AS decimal(24, 2))", createDecimalType(24, 2), "CAST('123456789.3' AS decimal(24, 2))") - .addRoundTrip("decimal(24, 4)", "CAST('12345678901234567890.31' AS decimal(24, 4))", createDecimalType(24, 4), "CAST('12345678901234567890.31' AS decimal(24, 4))") - .addRoundTrip("decimal(30, 5)", "CAST('3141592653589793238462643.38327' AS decimal(30, 5))", createDecimalType(30, 5), "CAST('3141592653589793238462643.38327' AS decimal(30, 5))") - .addRoundTrip("decimal(30, 5)", "CAST('-3141592653589793238462643.38327' AS decimal(30, 5))", createDecimalType(30, 5), "CAST('-3141592653589793238462643.38327' AS decimal(30, 5))") - .addRoundTrip("decimal(38, 0)", "CAST('27182818284590452353602874713526624977' AS decimal(38, 0))", createDecimalType(38, 0), "CAST('27182818284590452353602874713526624977' AS decimal(38, 0))") - .addRoundTrip("decimal(38, 0)", "CAST('-27182818284590452353602874713526624977' AS decimal(38, 0))", createDecimalType(38, 0), "CAST('-27182818284590452353602874713526624977' AS decimal(38, 0))") - - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_decimal")) - - .addRoundTrip("decimal(3, 1)", "NULL", createDecimalType(3, 1), "CAST(NULL AS decimal(3,1))") - .addRoundTrip("decimal(30, 5)", "NULL", createDecimalType(30, 5), "CAST(NULL AS decimal(30,5))") - - .execute(getQueryRunner(), trinoCreateAsSelect("test_decimal")); - - SqlDataTypeTest.create() - .addRoundTrip("Nullable(decimal(3, 1))", "NULL", createDecimalType(3, 1), "CAST(NULL AS decimal(3,1))") - .addRoundTrip("Nullable(decimal(30, 5))", "NULL", createDecimalType(30, 5), "CAST(NULL AS decimal(30,5))") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_nullable_decimal")); - } - - @Test - public void testClickHouseChar() - { - // ClickHouse char is FixedString, which is arbitrary bytes - SqlDataTypeTest.create() - // plain - .addRoundTrip("char(10)", "'text_a'", VARBINARY, "to_utf8('text_a')") - .addRoundTrip("char(255)", "'text_b'", VARBINARY, "to_utf8('text_b')") - .addRoundTrip("char(5)", "'攻殻機動隊'", VARBINARY, "to_utf8('攻殻機動隊')") - .addRoundTrip("char(32)", "'攻殻機動隊'", VARBINARY, "to_utf8('攻殻機動隊')") - .addRoundTrip("char(1)", "'😂'", VARBINARY, "to_utf8('😂')") - .addRoundTrip("char(77)", "'Ну, погоди!'", VARBINARY, "to_utf8('Ну, погоди!')") - // nullable - .addRoundTrip("Nullable(char(10))", "NULL", VARBINARY, "CAST(NULL AS varbinary)") - .addRoundTrip("Nullable(char(10))", "'text_a'", VARBINARY, "to_utf8('text_a')") - .addRoundTrip("Nullable(char(1))", "'😂'", VARBINARY, "to_utf8('😂')") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_char")); - - // Set map_string_as_varchar session property as true - SqlDataTypeTest.create() - // plain - .addRoundTrip("char(10)", "'text_a'", VARCHAR, "CAST('text_a' AS varchar)") - .addRoundTrip("char(255)", "'text_b'", VARCHAR, "CAST('text_b' AS varchar)") - .addRoundTrip("char(5)", "'攻殻機動隊'", VARCHAR, "CAST('攻殻機動隊' AS varchar)") - .addRoundTrip("char(32)", "'攻殻機動隊'", VARCHAR, "CAST('攻殻機動隊' AS varchar)") - .addRoundTrip("char(1)", "'😂'", VARCHAR, "CAST('😂' AS varchar)") - .addRoundTrip("char(77)", "'Ну, погоди!'", VARCHAR, "CAST('Ну, погоди!' AS varchar)") - // nullable - .addRoundTrip("Nullable(char(10))", "NULL", VARCHAR, "CAST(NULL AS varchar)") - .addRoundTrip("Nullable(char(10))", "'text_a'", VARCHAR, "CAST('text_a' AS varchar)") - .addRoundTrip("Nullable(char(1))", "'😂'", VARCHAR, "CAST('😂' AS varchar)") - .execute(getQueryRunner(), mapStringAsVarcharSession(), clickhouseCreateAndInsert("tpch.test_char")); - } - - @Test - public void testClickHouseFixedString() - { - SqlDataTypeTest.create() - // plain - .addRoundTrip("FixedString(10)", "'c12345678b'", VARBINARY, "to_utf8('c12345678b')") - .addRoundTrip("FixedString(10)", "'c123'", VARBINARY, "to_utf8('c123\0\0\0\0\0\0')") - // nullable - .addRoundTrip("Nullable(FixedString(10))", "NULL", VARBINARY, "CAST(NULL AS varbinary)") - .addRoundTrip("Nullable(FixedString(10))", "'c12345678b'", VARBINARY, "to_utf8('c12345678b')") - .addRoundTrip("Nullable(FixedString(10))", "'c123'", VARBINARY, "to_utf8('c123\0\0\0\0\0\0')") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_fixed_string")); - - // Set map_string_as_varchar session property as true - SqlDataTypeTest.create() - // plain - .addRoundTrip("FixedString(10)", "'c12345678b'", VARCHAR, "CAST('c12345678b' AS varchar)") - .addRoundTrip("FixedString(10)", "'c123'", VARCHAR, "CAST('c123\0\0\0\0\0\0' AS varchar)") - // nullable - .addRoundTrip("Nullable(FixedString(10))", "NULL", VARCHAR, "CAST(NULL AS varchar)") - .addRoundTrip("Nullable(FixedString(10))", "'c12345678b'", VARCHAR, "CAST('c12345678b' AS varchar)") - .addRoundTrip("Nullable(FixedString(10))", "'c123'", VARCHAR, "CAST('c123\0\0\0\0\0\0' AS varchar)") - .execute(getQueryRunner(), mapStringAsVarcharSession(), clickhouseCreateAndInsert("tpch.test_fixed_string")); - } - - @Test - public void testTrinoChar() - { - SqlDataTypeTest.create() - .addRoundTrip("char(10)", "NULL", VARBINARY, "CAST(NULL AS varbinary)") - .addRoundTrip("char(10)", "'text_a'", VARBINARY, "to_utf8('text_a')") - .addRoundTrip("char(255)", "'text_b'", VARBINARY, "to_utf8('text_b')") - .addRoundTrip("char(5)", "'攻殻機動隊'", VARBINARY, "to_utf8('攻殻機動隊')") - .addRoundTrip("char(32)", "'攻殻機動隊'", VARBINARY, "to_utf8('攻殻機動隊')") - .addRoundTrip("char(1)", "'😂'", VARBINARY, "to_utf8('😂')") - .addRoundTrip("char(77)", "'Ну, погоди!'", VARBINARY, "to_utf8('Ну, погоди!')") - .execute(getQueryRunner(), trinoCreateAsSelect("test_char")) - .execute(getQueryRunner(), trinoCreateAsSelect(mapStringAsVarcharSession(), "test_char")); - - // Set map_string_as_varchar session property as true - SqlDataTypeTest.create() - .addRoundTrip("char(10)", "NULL", VARCHAR, "CAST(NULL AS varchar)") - .addRoundTrip("char(10)", "'text_a'", VARCHAR, "CAST('text_a' AS varchar)") - .addRoundTrip("char(255)", "'text_b'", VARCHAR, "CAST('text_b' AS varchar)") - .addRoundTrip("char(5)", "'攻殻機動隊'", VARCHAR, "CAST('攻殻機動隊' AS varchar)") - .addRoundTrip("char(32)", "'攻殻機動隊'", VARCHAR, "CAST('攻殻機動隊' AS varchar)") - .addRoundTrip("char(1)", "'😂'", VARCHAR, "CAST('😂' AS varchar)") - .addRoundTrip("char(77)", "'Ну, погоди!'", VARCHAR, "CAST('Ну, погоди!' AS varchar)") - .execute(getQueryRunner(), mapStringAsVarcharSession(), trinoCreateAsSelect("test_char")) - .execute(getQueryRunner(), mapStringAsVarcharSession(), trinoCreateAsSelect(mapStringAsVarcharSession(), "test_char")); - } - - @Test - public void testClickHouseVarchar() - { - // TODO add more test cases - // ClickHouse varchar is String, which is arbitrary bytes - SqlDataTypeTest.create() - // plain - .addRoundTrip("varchar(30)", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") - // nullable - .addRoundTrip("Nullable(varchar(30))", "NULL", VARBINARY, "CAST(NULL AS varbinary)") - .addRoundTrip("Nullable(varchar(30))", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_varchar")); - - // Set map_string_as_varchar session property as true - SqlDataTypeTest.create() - // plain - .addRoundTrip("varchar(30)", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") - // nullable - .addRoundTrip("Nullable(varchar(30))", "NULL", VARCHAR, "CAST(NULL AS varchar)") - .addRoundTrip("Nullable(varchar(30))", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") - .execute(getQueryRunner(), mapStringAsVarcharSession(), clickhouseCreateAndInsert("tpch.test_varchar")); - } - - @Test - public void testClickHouseString() - { - // TODO add more test cases - SqlDataTypeTest.create() - // plain - .addRoundTrip("String", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") - // nullable - .addRoundTrip("Nullable(String)", "NULL", VARBINARY, "CAST(NULL AS varbinary)") - .addRoundTrip("Nullable(String)", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_varchar")); - - // Set map_string_as_varchar session property as true - SqlDataTypeTest.create() - // plain - .addRoundTrip("String", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") - // nullable - .addRoundTrip("Nullable(String)", "NULL", VARCHAR, "CAST(NULL AS varchar)") - .addRoundTrip("Nullable(String)", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") - .execute(getQueryRunner(), mapStringAsVarcharSession(), clickhouseCreateAndInsert("tpch.test_varchar")); - } - - @Test - public void testTrinoVarchar() - { - SqlDataTypeTest.create() - .addRoundTrip("varchar(30)", "NULL", VARBINARY, "CAST(NULL AS varbinary)") - .addRoundTrip("varchar(30)", "'Piękna łąka w 東京都'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") - .execute(getQueryRunner(), trinoCreateAsSelect("test_varchar")) - .execute(getQueryRunner(), trinoCreateAsSelect(mapStringAsVarcharSession(), "test_varchar")); - - // Set map_string_as_varchar session property as true - SqlDataTypeTest.create() - .addRoundTrip("varchar(30)", "NULL", VARCHAR, "CAST(NULL AS varchar)") - .addRoundTrip("varchar(30)", "'Piękna łąka w 東京都'", VARCHAR, "CAST('Piękna łąka w 東京都' AS varchar)") - .execute(getQueryRunner(), mapStringAsVarcharSession(), trinoCreateAsSelect("test_varchar")) - .execute(getQueryRunner(), mapStringAsVarcharSession(), trinoCreateAsSelect(mapStringAsVarcharSession(), "test_varchar")); - } - - @Test - public void testTrinoVarbinary() - { - SqlDataTypeTest.create() - .addRoundTrip("varbinary", "NULL", VARBINARY, "CAST(NULL AS varbinary)") - .addRoundTrip("varbinary", "X''", VARBINARY, "X''") - .addRoundTrip("varbinary", "X'68656C6C6F'", VARBINARY, "to_utf8('hello')") - .addRoundTrip("varbinary", "X'5069C4996B6E6120C582C4856B61207720E69DB1E4BAACE983BD'", VARBINARY, "to_utf8('Piękna łąka w 東京都')") - .addRoundTrip("varbinary", "X'4261672066756C6C206F6620F09F92B0'", VARBINARY, "to_utf8('Bag full of 💰')") - .addRoundTrip("varbinary", "X'0001020304050607080DF9367AA7000000'", VARBINARY, "X'0001020304050607080DF9367AA7000000'") // non-text - .addRoundTrip("varbinary", "X'000000000000'", VARBINARY, "X'000000000000'") - .execute(getQueryRunner(), trinoCreateAsSelect("test_varbinary")); - } - - @Test(dataProvider = "sessionZonesDataProvider") - public void testDate(ZoneId sessionZone) - { - Session session = Session.builder(getSession()) - .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) - .build(); - SqlDataTypeTest.create() - .addRoundTrip("date", "DATE '1970-01-01'", DATE, "DATE '1970-01-01'") // min value in ClickHouse - .addRoundTrip("date", "DATE '1970-02-03'", DATE, "DATE '1970-02-03'") - .addRoundTrip("date", "DATE '2017-07-01'", DATE, "DATE '2017-07-01'") // summer on northern hemisphere (possible DST) - .addRoundTrip("date", "DATE '2017-01-01'", DATE, "DATE '2017-01-01'") // winter on northern hemisphere (possible DST on southern hemisphere) - .addRoundTrip("date", "DATE '1970-01-01'", DATE, "DATE '1970-01-01'") - .addRoundTrip("date", "DATE '1983-04-01'", DATE, "DATE '1983-04-01'") - .addRoundTrip("date", "DATE '1983-10-01'", DATE, "DATE '1983-10-01'") - .addRoundTrip("date", "DATE '2106-02-07'", DATE, "DATE '2106-02-07'") // max value in ClickHouse - .execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_date")) - .execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_date")) - .execute(getQueryRunner(), session, trinoCreateAsSelect("test_date")) - .execute(getQueryRunner(), session, trinoCreateAndInsert(session, "test_date")) - .execute(getQueryRunner(), session, trinoCreateAndInsert("test_date")); - - // Null - SqlDataTypeTest.create() - .addRoundTrip("date", "NULL", DATE, "CAST(NULL AS DATE)") - .execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_date")) - .execute(getQueryRunner(), session, trinoCreateAsSelect("test_date")) - .execute(getQueryRunner(), session, trinoCreateAndInsert(session, "test_date")) - .execute(getQueryRunner(), session, trinoCreateAndInsert("test_date")); - SqlDataTypeTest.create() - .addRoundTrip("Nullable(date)", "NULL", DATE, "CAST(NULL AS DATE)") - .execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_date")); - } - - @Test - public void testUnsupportedDate() - { - try (TestTable table = new TestTable(getQueryRunner()::execute, "test_unsupported_date", "(dt date)")) { - assertQueryFails( - format("INSERT INTO %s VALUES (DATE '1969-12-31')", table.getName()), - "Date must be between 1970-01-01 and 2106-02-07 in ClickHouse: 1969-12-31"); - assertQueryFails( - format("INSERT INTO %s VALUES (DATE '2106-02-08')", table.getName()), - "Date must be between 1970-01-01 and 2106-02-07 in ClickHouse: 2106-02-08"); - } - } - - @Test(dataProvider = "sessionZonesDataProvider") - public void testTimestamp(ZoneId sessionZone) - { - Session session = Session.builder(getSession()) - .setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId())) - .build(); - - SqlDataTypeTest.create() - .addRoundTrip("timestamp(0)", "timestamp '1970-01-01 00:00:00'", createTimestampType(0), "TIMESTAMP '1970-01-01 00:00:00'") // min value in ClickHouse - .addRoundTrip("timestamp(0)", "timestamp '1986-01-01 00:13:07'", createTimestampType(0), "TIMESTAMP '1986-01-01 00:13:07'") // time gap in Kathmandu - .addRoundTrip("timestamp(0)", "timestamp '2018-03-25 03:17:17'", createTimestampType(0), "TIMESTAMP '2018-03-25 03:17:17'") // time gap in Vilnius - .addRoundTrip("timestamp(0)", "timestamp '2018-10-28 01:33:17'", createTimestampType(0), "TIMESTAMP '2018-10-28 01:33:17'") // time doubled in JVM zone - .addRoundTrip("timestamp(0)", "timestamp '2018-10-28 03:33:33'", createTimestampType(0), "TIMESTAMP '2018-10-28 03:33:33'") // time double in Vilnius - .addRoundTrip("timestamp(0)", "timestamp '2105-12-31 23:59:59'", createTimestampType(0), "TIMESTAMP '2105-12-31 23:59:59'") // max value in ClickHouse - .execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_timestamp")) - .execute(getQueryRunner(), session, trinoCreateAsSelect("test_timestamp")) - .execute(getQueryRunner(), session, trinoCreateAndInsert(session, "test_timestamp")) - .execute(getQueryRunner(), session, trinoCreateAndInsert("test_timestamp")); - - addTimestampRoundTrips("timestamp") - .execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_timestamp")); - addTimestampRoundTrips("datetime") - .execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_datetime")); - } - - private SqlDataTypeTest addTimestampRoundTrips(String inputType) - { - return SqlDataTypeTest.create() - .addRoundTrip(inputType, "'1969-12-31 23:59:59'", createTimestampType(0), "TIMESTAMP '1970-01-01 23:59:59'") // unsupported timestamp become 1970-01-01 23:59:59 - .addRoundTrip(inputType, "'1970-01-01 00:00:00'", createTimestampType(0), "TIMESTAMP '1970-01-01 00:00:00'") // min value in ClickHouse - .addRoundTrip(inputType, "'1986-01-01 00:13:07'", createTimestampType(0), "TIMESTAMP '1986-01-01 00:13:07'") // time gap in Kathmandu - .addRoundTrip(inputType, "'2018-03-25 03:17:17'", createTimestampType(0), "TIMESTAMP '2018-03-25 03:17:17'") // time gap in Vilnius - .addRoundTrip(inputType, "'2018-10-28 01:33:17'", createTimestampType(0), "TIMESTAMP '2018-10-28 01:33:17'") // time doubled in JVM zone - .addRoundTrip(inputType, "'2018-10-28 03:33:33'", createTimestampType(0), "TIMESTAMP '2018-10-28 03:33:33'") // time double in Vilnius - .addRoundTrip(inputType, "'2105-12-31 23:59:59'", createTimestampType(0), "TIMESTAMP '2105-12-31 23:59:59'") // max value in ClickHouse - .addRoundTrip(format("Nullable(%s)", inputType), "NULL", createTimestampType(0), "CAST(NULL AS TIMESTAMP(0))"); - } - - @Test - public void testUnsupportedTimestamp() - { - try (TestTable table = new TestTable(getQueryRunner()::execute, "test_unsupported_timestamp", "(dt timestamp(0))")) { - assertQueryFails( - format("INSERT INTO %s VALUES (TIMESTAMP '-9999-12-31 23:59:59')", table.getName()), - "Timestamp must be between 1970-01-01 00:00:00 and 2105-12-31 23:59:59 in ClickHouse: -9999-12-31 23:59:59"); - assertQueryFails( - format("INSERT INTO %s VALUES (TIMESTAMP '1969-12-31 23:59:59')", table.getName()), - "Timestamp must be between 1970-01-01 00:00:00 and 2105-12-31 23:59:59 in ClickHouse: 1969-12-31 23:59:59"); - assertQueryFails( - format("INSERT INTO %s VALUES (TIMESTAMP '2106-01-01 00:00:00')", table.getName()), - "Timestamp must be between 1970-01-01 00:00:00 and 2105-12-31 23:59:59 in ClickHouse: 2106-01-01 00:00:00"); - assertQueryFails( - format("INSERT INTO %s VALUES (TIMESTAMP '9999-12-31 23:59:59')", table.getName()), - "Timestamp must be between 1970-01-01 00:00:00 and 2105-12-31 23:59:59 in ClickHouse: 9999-12-31 23:59:59"); - } - } - - @DataProvider - public Object[][] sessionZonesDataProvider() - { - return new Object[][] { - {UTC}, - {jvmZone}, - // using two non-JVM zones so that we don't need to worry what ClickHouse system zone is - {vilnius}, - {kathmandu}, - {ZoneId.of(TestingSession.DEFAULT_TIME_ZONE_KEY.getId())}, - }; - } - - @Test - public void testEnum() - { - SqlDataTypeTest.create() - .addRoundTrip("Enum('hello' = 1, 'world' = 2)", "'hello'", createUnboundedVarcharType(), "VARCHAR 'hello'") - .addRoundTrip("Enum('hello' = 1, 'world' = 2)", "'world'", createUnboundedVarcharType(), "VARCHAR 'world'") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_enum")); - } - - @Test - public void testUuid() - { - SqlDataTypeTest.create() - .addRoundTrip("Nullable(UUID)", "NULL", UuidType.UUID, "CAST(NULL AS UUID)") - .addRoundTrip("Nullable(UUID)", "'114514ea-0601-1981-1142-e9b55b0abd6d'", UuidType.UUID, "CAST('114514ea-0601-1981-1142-e9b55b0abd6d' AS UUID)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("default.ck_test_uuid")); - - SqlDataTypeTest.create() - .addRoundTrip("CAST(NULL AS UUID)", "cast(NULL as UUID)") - .addRoundTrip("UUID '114514ea-0601-1981-1142-e9b55b0abd6d'", "CAST('114514ea-0601-1981-1142-e9b55b0abd6d' AS UUID)") - .execute(getQueryRunner(), trinoCreateAsSelect("default.ck_test_uuid")) - .execute(getQueryRunner(), trinoCreateAndInsert("default.ck_test_uuid")); - } - - @Test - public void testIp() - { - SqlDataTypeTest.create() - .addRoundTrip("IPv4", "'0.0.0.0'", IPADDRESS, "IPADDRESS '0.0.0.0'") - .addRoundTrip("IPv4", "'116.253.40.133'", IPADDRESS, "IPADDRESS '116.253.40.133'") - .addRoundTrip("IPv4", "'255.255.255.255'", IPADDRESS, "IPADDRESS '255.255.255.255'") - .addRoundTrip("IPv6", "'::'", IPADDRESS, "IPADDRESS '::'") - .addRoundTrip("IPv6", "'2001:44c8:129:2632:33:0:252:2'", IPADDRESS, "IPADDRESS '2001:44c8:129:2632:33:0:252:2'") - .addRoundTrip("IPv6", "'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'", IPADDRESS, "IPADDRESS 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'") - .addRoundTrip("Nullable(IPv4)", "NULL", IPADDRESS, "CAST(NULL AS IPADDRESS)") - .addRoundTrip("Nullable(IPv6)", "NULL", IPADDRESS, "CAST(NULL AS IPADDRESS)") - .execute(getQueryRunner(), clickhouseCreateAndInsert("tpch.test_ip")); - - SqlDataTypeTest.create() - .addRoundTrip("IPv4", "IPADDRESS '0.0.0.0'", IPADDRESS, "IPADDRESS '0.0.0.0'") - .addRoundTrip("IPv4", "IPADDRESS '116.253.40.133'", IPADDRESS, "IPADDRESS '116.253.40.133'") - .addRoundTrip("IPv4", "IPADDRESS '255.255.255.255'", IPADDRESS, "IPADDRESS '255.255.255.255'") - .addRoundTrip("IPv6", "IPADDRESS '::'", IPADDRESS, "IPADDRESS '::'") - .addRoundTrip("IPv6", "IPADDRESS '2001:44c8:129:2632:33:0:252:2'", IPADDRESS, "IPADDRESS '2001:44c8:129:2632:33:0:252:2'") - .addRoundTrip("IPv6", "IPADDRESS 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'", IPADDRESS, "IPADDRESS 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'") - .addRoundTrip("Nullable(IPv4)", "NULL", IPADDRESS, "CAST(NULL AS IPADDRESS)") - .addRoundTrip("Nullable(IPv6)", "NULL", IPADDRESS, "CAST(NULL AS IPADDRESS)") - .execute(getQueryRunner(), clickhouseCreateAndTrinoInsert("tpch.test_ip")); - } - - private static Session mapStringAsVarcharSession() - { - return testSessionBuilder() - .setCatalog("clickhouse") - .setSchema(TPCH_SCHEMA) - .setCatalogSessionProperty("clickhouse", "map_string_as_varchar", "true") - .build(); - } - - private DataSetup trinoCreateAsSelect(String tableNamePrefix) - { - return trinoCreateAsSelect(getSession(), tableNamePrefix); - } - - private DataSetup trinoCreateAsSelect(Session session, String tableNamePrefix) - { - return new CreateAsSelectDataSetup(new TrinoSqlExecutor(getQueryRunner(), session), tableNamePrefix); - } - - private DataSetup trinoCreateAndInsert(String tableNamePrefix) - { - return trinoCreateAndInsert(getSession(), tableNamePrefix); - } - - private DataSetup trinoCreateAndInsert(Session session, String tableNamePrefix) - { - return new CreateAndInsertDataSetup(new TrinoSqlExecutor(getQueryRunner(), session), tableNamePrefix); - } - - private DataSetup clickhouseCreateAndInsert(String tableNamePrefix) - { - return new CreateAndInsertDataSetup(new ClickHouseSqlExecutor(clickhouseServer::execute), tableNamePrefix); - } - - private DataSetup clickhouseCreateAndTrinoInsert(String tableNamePrefix) - { - return new CreateAndTrinoInsertDataSetup(new ClickHouseSqlExecutor(clickhouseServer::execute), new TrinoSqlExecutor(getQueryRunner()), tableNamePrefix); - } }