Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = "";
Expand All @@ -197,6 +183,7 @@ public class ClickHouseClient
private final AggregateFunctionRewriter<JdbcExpression, String> aggregateFunctionRewriter;
private final Type uuidType;
private final Type ipAddressType;
private final AtomicReference<ClickHouseVersion> clickHouseVersion = new AtomicReference<>();

@Inject
public ClickHouseClient(
Expand Down Expand Up @@ -511,16 +498,16 @@ public Optional<ColumnMapping> 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:
Expand Down Expand Up @@ -595,15 +582,15 @@ public Optional<ColumnMapping> 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) {
verify(typeHandle.getRequiredDecimalDigits() == 0, "Expected 0 as timestamp precision, but got %s", typeHandle.getRequiredDecimalDigits());
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));
Expand Down Expand Up @@ -658,17 +645,38 @@ 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());
}
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
*
Expand All @@ -688,97 +696,77 @@ private Optional<String> formatProperty(List<String> 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,
(statement, index, value) -> {
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(
Expand Down
Loading