Skip to content

Commit

Permalink
Deny writing unsupported dates in ClickHouse
Browse files Browse the repository at this point in the history
  • Loading branch information
ebyhr committed Jan 19, 2022
1 parent 4c99cff commit 3ff7bfb
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import io.trino.plugin.jdbc.JdbcExpression;
import io.trino.plugin.jdbc.JdbcTableHandle;
import io.trino.plugin.jdbc.JdbcTypeHandle;
import io.trino.plugin.jdbc.LongWriteFunction;
import io.trino.plugin.jdbc.RemoteTableName;
import io.trino.plugin.jdbc.SliceWriteFunction;
import io.trino.plugin.jdbc.WriteMapping;
Expand Down Expand Up @@ -65,6 +66,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -87,8 +89,7 @@
import static io.trino.plugin.jdbc.StandardColumnMappings.bigintColumnMapping;
import static io.trino.plugin.jdbc.StandardColumnMappings.bigintWriteFunction;
import static io.trino.plugin.jdbc.StandardColumnMappings.booleanWriteFunction;
import static io.trino.plugin.jdbc.StandardColumnMappings.dateColumnMappingUsingLocalDate;
import static io.trino.plugin.jdbc.StandardColumnMappings.dateWriteFunctionUsingLocalDate;
import static io.trino.plugin.jdbc.StandardColumnMappings.dateReadFunctionUsingLocalDate;
import static io.trino.plugin.jdbc.StandardColumnMappings.decimalColumnMapping;
import static io.trino.plugin.jdbc.StandardColumnMappings.doubleColumnMapping;
import static io.trino.plugin.jdbc.StandardColumnMappings.doubleWriteFunction;
Expand All @@ -108,6 +109,7 @@
import static io.trino.plugin.jdbc.StandardColumnMappings.varcharReadFunction;
import static io.trino.plugin.jdbc.StandardColumnMappings.varcharWriteFunction;
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 All @@ -134,6 +136,8 @@ public class ClickHouseClient
extends BaseJdbcClient
{
static final int CLICKHOUSE_MAX_DECIMAL_PRECISION = 76;
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 final AggregateFunctionRewriter<JdbcExpression> aggregateFunctionRewriter;
private final Type uuidType;
Expand Down Expand Up @@ -546,6 +550,30 @@ else if (prop.size() == 1) {
}
}

private static ColumnMapping dateColumnMappingUsingLocalDate()
{
return ColumnMapping.longMapping(
DATE,
dateReadFunctionUsingLocalDate(),
dateWriteFunctionUsingLocalDate());
}

private static LongWriteFunction dateWriteFunctionUsingLocalDate()
{
return (statement, index, value) -> {
verifySupportedDate(value);
statement.setObject(index, LocalDate.ofEpochDay(value));
};
}

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: %s", LocalDate.ofEpochDay(MIN_SUPPORTED_DATE_EPOCH), LocalDate.ofEpochDay(MAX_SUPPORTED_DATE_EPOCH), LocalDate.ofEpochDay(value)));
}
}

private ColumnMapping ipAddressColumnMapping(String writeBindExpression)
{
return ColumnMapping.sliceMapping(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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.AfterClass;
import org.testng.annotations.BeforeClass;
Expand Down Expand Up @@ -394,7 +395,6 @@ public void testDate(ZoneId sessionZone)
.setTimeZoneKey(TimeZoneKey.getTimeZoneKey(sessionZone.getId()))
.build();
SqlDataTypeTest.create()
.addRoundTrip("date", "DATE '1969-12-31'", DATE, "DATE '1970-01-01'") // unsupported date become 1970-01-01
.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)
Expand All @@ -403,7 +403,6 @@ public void testDate(ZoneId sessionZone)
.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
.addRoundTrip("date", "DATE '2106-02-08'", DATE, "DATE '1970-01-01'") // unsupported date become 1970-01-01
.execute(getQueryRunner(), session, clickhouseCreateAndInsert("tpch.test_date"))
.execute(getQueryRunner(), session, trinoCreateAsSelect(session, "test_date"));

Expand All @@ -416,6 +415,19 @@ public void testDate(ZoneId sessionZone)
.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: 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: 2106-02-08");
}
}

@Test(dataProvider = "sessionZonesDataProvider")
public void testTimestamp(ZoneId sessionZone)
{
Expand Down

0 comments on commit 3ff7bfb

Please sign in to comment.