diff --git a/docs/datetime.md b/docs/datetime.md new file mode 100644 index 000000000..cbb058c4e --- /dev/null +++ b/docs/datetime.md @@ -0,0 +1,69 @@ +# ClickHouse Server + +ClickHouse has dedicated data types for [Date](https://clickhouse.tech/docs/en/data_types/date) and [DateTime](https://clickhouse.tech/docs/en/data_types/datetime). A DateTime value is usually to be interpreted in the server time zone, but a DateTime value may also be formatted for a different time zone. Note that the explicit time zone per column only affects inserting and displaying values, _not_ the predicates (see one example [here](https://github.com/ClickHouse/ClickHouse/issues/5206)). + + +# Setting Values + +When setting values via the PreparedStatement setter methods, the JDBC driver does not have any knowledge about the target columns. Its job is to serialize any date time values into a textual representation. One of the aims of this driver is to use a serialization format that makes it easy to use ClickHouse Date or DateTime fields from a Java application. + +In some cases the driver cannot perform the serialization without referring to a relevant time zone. This is how it works: + +If the client supplies a valid `Calendar` object as optional argument, the driver will use the time zone contained therein (`tz_calendar`). + +Two regular time zones are initialized like this: + +* `tz_datetime`: value from `ru.yandex.clickhouse.settings.ClickHouseConnectionSettings.USE_TIME_ZONE`. If null, either ClickHouse server time zone (`ru.yandex.clickhouse.settings.ClickHouseConnectionSettings.USE_SERVER_TIME_ZONE` is `true`) or JVM time zone (else) + +* `tz_date`: same as `tz_datetime` if `ru.yandex.clickhouse.settings.ClickHouseConnectionSettings.USE_SERVER_TIME_ZONE_FOR_DATES` is `true`, JVM time zone else + +The JDBC driver supports all explicit methods, e.g. setDate, setTimestamp etc. with their optional Calendar argument. Providing hints via target SQL type does not have any effect. + +The following table illustrates the serialization format for some popular date time data types, which we consider the most convenient. Clients are of course free to take care of serialization themselves by supplying a String or an Integer parameter, optionally using one of the server's utility methods (e.g. [parseDateTimeBestEffort](https://clickhouse.tech/docs/en/query_language/functions/type_conversion_functions/#type_conversion_functions-parsedatetimebesteffort)). + + Method | Format | Relevant time zone | + ------ | ------ | ------------------- +[setDate(int, Date)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setDate-int-java.sql.Date-) | yyyy-MM-dd | tz_date +[setDate(int, Date, Calendar)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setDate-int-java.sql.Date-java.util.Calendar-) | yyyy-MM-dd | tz_calendar +[setObject(int, Date)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | yyyy-MM-dd | tz_date +[setTime(int, Time)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setTime-int-java.sql.Time-) | HH:mm:ss | tz_datetime +[setTime(int, Time, Calendar)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setTime-int-java.sql.Time-java.util.Calendar-) | HH:mm:ss | tz_calendar +[setObject(int, Time)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | HH:mm:ss | tz_datetime +[setTimestamp(int, Timestamp)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setTimestamp-int-java.sql.Timestamp-) | yyyy-MM-dd HH:mm:ss | tz_datetime +[setTimestamp(int, Timestamp, Calendar)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setTimestamp-int-java.sql.Timestamp-java.util.Calendar-) | yyyy-MM-dd HH:mm:ss | tz_calendar +[setObject(int, Timestamp)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | yyyy-MM-dd HH:mm:ss | tz_datetime +[setObject(int, LocalTime)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | HH:mm:ss | _none_ +[setObject(int, OffsetTime)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | HH:mm:ssZZZZ | _none_ +[setObject(int, LocalDate)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | yyyy-MM-dd | _none_ +[setObject(int, LocalDateTime)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | yyyy-MM-dd HH:mm:ss | _none_ +[setObject(int, OffsetDateTime)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | yyyy-MM-dd HH:mm:ss | tz_datetime +[setObject(int, ZonedDateTime)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | yyyy-MM-dd HH:mm:ss | tz_datetime +[setObject(int, Instant)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setObject-int-java.lang.Object-) | yyyy-MM-dd HH:mm:ss | tz_datetime + +# Retrieving Values + +When retrieving values via the ResultSet's getter methods, the JDBC driver will try to accomodate for some obvious options. If the underlying data field is of type Date or DateTime, the driver knows the implied time zone. This helps during the interpretation of the values retrieved from the server. Users may configure the driver to use a different time zone when reporting results back to the client (via `tz_date` or`tz_datetime`, see above). + +The methods which take a [Calendar]((https://docs.oracle.com/javase/8/docs/api/java.base/java/util/Calendar.html) argument behave the same as the corresponding methods without such an argument. The API documentation says something like + +> This method uses the given calendar to construct an appropriate millisecond value for the _x_ if the underlying database does not store timezone information. + +For Date and DateTime fields, the JDBC driver has enough time zone related information available, so these methods would only be relevant for String or other typed fields. There might be valid use cases, but for now we think that adding such an option would make things even more complicated. + +Requested Type | Number | Date | DateTime | Other +---------------| ---------------------------------- +[Date](https://docs.oracle.com/javase/8/docs/api/java/sql/Date.html) | Seconds or milliseconds past epoch truncated to day in relevant time zone | Date in relevant time zone, midnight | Date time in relevant time zone, rewind to midnight | Try number, date time (with or without offset) truncated to day, date +[Time](https://docs.oracle.com/javase/8/docs/api/java/sql/Time.html) | Local time at 1970-01-01 (e.g. “1337” is “13:37:00” at TZ) | Midnight on 1970-01-01 in relevant time zone | Local time in relevant time zone | Local time in relevant time zone via ISO format or via number, at 1970-01-01 +[Timestamp](https://docs.oracle.com/javase/8/docs/api/java/sql/Timestamp.html) | Seconds or milliseconds past epoch | Local date at midnight in relevant time zone | Local date and time in relevant time zone | Number, date time with or without offset +[LocalTime](https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html) | Local time (e.g. "430" is "04:30:00") | Midnight | Local time | ISO format with or without offset, number +[OffsetTime](https://docs.oracle.com/javase/8/docs/api/java/time/OffsetTime.html) | Local time with current (!) offset of relevant time zone | Midnight with offset of relevant time zone on that date | Local time with offset of relevant time zone at value's date | ISO format, number +[LocalDate](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html) | Seconds or milliseconds past epoch as local date in relevant time zone | Local date | Local date (no conversion) | Local date, local time, number +[LocalDateTime](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html) | Seconds or milliseconds past epoch as local date time in relevant time zone | Local date midnight | Local date time | Local date time, number +[OffsetDateTime](https://docs.oracle.com/javase/8/docs/api/java/time/OffsetDateTime.html) | Seconds or milliseconds past epoch, offset from relevant time zone | Date midnight in relevant time zone | Date time in relevant time zone | Local date time in relevant time zone, ISO formats, number +[ZonedDdateTime](https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html) | Seconds or milliseconds past epoch, offset from relevant time zone | Date midnight in relevant time zone | Date time in relevant time zone | Local date, local date time in relevant time zone, ISO formats, number + +# Summary + +Life as a developer would be boring without time zones: [xkcd Super Villain Plan](https://xkcd.com/1883) Have fun! + +If you think the ClickHouse JDBC driver behaves wrong, please file an issue. Make sure to include some time zone information of your ClickHouse server, the JVM, and the relevant driver settings. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 99d86ee8f..e85ec3844 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ 2.9.10.8 29.0-jre 2.3.1 - 1.7 + 1.8 1.15.1 6.14.3 1.10.19 diff --git a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java index a0e64175e..d41a8c89e 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java @@ -38,6 +38,7 @@ import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlStatement; import ru.yandex.clickhouse.jdbc.parser.StatementType; + import ru.yandex.clickhouse.response.ClickHouseResponse; import ru.yandex.clickhouse.settings.ClickHouseProperties; import ru.yandex.clickhouse.settings.ClickHouseQueryParam; @@ -452,17 +453,38 @@ public ResultSetMetaData getMetaData() throws SQLException { @Override public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException(); + if (x != null && cal != null && cal.getTimeZone() != null) { + setBind( + parameterIndex, + ClickHouseValueFormatter.formatDate(x, cal.getTimeZone()), + true); + } else { + setDate(parameterIndex, x); + } } @Override public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException(); + if (x != null && cal != null && cal.getTimeZone() != null) { + setBind( + parameterIndex, + ClickHouseValueFormatter.formatTime(x, cal.getTimeZone()), + true); + } else { + setTime(parameterIndex, x); + } } @Override public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException(); + if (x != null && cal != null && cal.getTimeZone() != null) { + setBind( + parameterIndex, + ClickHouseValueFormatter.formatTimestamp(x, cal.getTimeZone()), + true); + } else { + setTimestamp(parameterIndex, x); + } } @Override diff --git a/src/main/java/ru/yandex/clickhouse/ClickHouseStatement.java b/src/main/java/ru/yandex/clickhouse/ClickHouseStatement.java index 1773698ae..80dc0971c 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHouseStatement.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHouseStatement.java @@ -42,68 +42,42 @@ ResultSet executeQuery(String sql, List externalData, Map additionalRequestParams) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendStream(InputStream content, String table, Map additionalDBParams) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendStream(InputStream content, String table) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendRowBinaryStream(String sql, Map additionalDBParams, ClickHouseStreamCallback callback) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendRowBinaryStream(String sql, ClickHouseStreamCallback callback) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendNativeStream(String sql, Map additionalDBParams, ClickHouseStreamCallback callback) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendNativeStream(String sql, ClickHouseStreamCallback callback) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendCSVStream(InputStream content, String table, Map additionalDBParams) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendCSVStream(InputStream content, String table) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendStreamSQL(InputStream content, String sql, Map additionalDBParams) throws SQLException; - /** - * @see #write() - */ @Deprecated void sendStreamSQL(InputStream content, String sql) throws SQLException; /** - * Returns extended write-API + * Returns extended write-API, which simplifies uploading larger files or + * data streams + * + * @return a new {@link Writer} builder object which can be used to + * construct a request to the server */ Writer write(); diff --git a/src/main/java/ru/yandex/clickhouse/Writer.java b/src/main/java/ru/yandex/clickhouse/Writer.java index a93ea8fa6..3ceabae9c 100644 --- a/src/main/java/ru/yandex/clickhouse/Writer.java +++ b/src/main/java/ru/yandex/clickhouse/Writer.java @@ -13,9 +13,11 @@ import java.io.InputStream; import java.sql.SQLException; -import static ru.yandex.clickhouse.domain.ClickHouseFormat.*; +import static ru.yandex.clickhouse.domain.ClickHouseFormat.Native; +import static ru.yandex.clickhouse.domain.ClickHouseFormat.RowBinary; +import static ru.yandex.clickhouse.domain.ClickHouseFormat.TabSeparated; -public class Writer extends ConfigurableApi { +public final class Writer extends ConfigurableApi { private ClickHouseFormat format = TabSeparated; private ClickHouseCompression compression = null; @@ -29,6 +31,10 @@ public class Writer extends ConfigurableApi { /** * Specifies format for further insert of data via send() + * + * @param format + * the format of the data to upload + * @return this writer instance */ public Writer format(ClickHouseFormat format) { if (null == format) { @@ -41,8 +47,9 @@ public Writer format(ClickHouseFormat format) { /** * Set table name for data insertion * - * @param table table name - * @return this + * @param table + * name of the table to upload the data to + * @return this writer instance */ public Writer table(String table) { this.sql = null; @@ -53,8 +60,9 @@ public Writer table(String table) { /** * Set SQL for data insertion * - * @param sql in a form "INSERT INTO table_name [(X,Y,Z)] VALUES " - * @return this + * @param sql + * in a form "INSERT INTO table_name [(X,Y,Z)] VALUES " + * @return this writer instance */ public Writer sql(String sql) { this.sql = sql; @@ -64,18 +72,35 @@ public Writer sql(String sql) { /** * Specifies data input stream + * + * @param stream + * a stream providing the data to upload + * @return this writer instance */ public Writer data(InputStream stream) { streamProvider = new HoldingInputProvider(stream); return this; } + /** + * Specifies data input stream, and the format to use + * + * @param stream + * a stream providing the data to upload + * @param format + * the format of the data to upload + * @return this writer instance + */ public Writer data(InputStream stream, ClickHouseFormat format) { return format(format).data(stream); } /** * Shortcut method for specifying a file as an input + * + * @param input + * the file to upload + * @return this writer instance */ public Writer data(File input) { streamProvider = new FileInputProvider(input); @@ -126,10 +151,14 @@ private void send(HttpEntity entity) throws SQLException { /** * Allows to send stream of data to ClickHouse * - * @param sql in a form of "INSERT INTO table_name (X,Y,Z) VALUES " - * @param data where to read data from - * @param format format of data in InputStream + * @param sql + * in a form of "INSERT INTO table_name (X,Y,Z) VALUES " + * @param data + * where to read data from + * @param format + * format of data in InputStream * @throws SQLException + * if the upload fails */ public void send(String sql, InputStream data, ClickHouseFormat format) throws SQLException { sql(sql).data(data).format(format).send(); @@ -138,17 +167,32 @@ public void send(String sql, InputStream data, ClickHouseFormat format) throws S /** * Convenient method for importing the data into table * - * @param table table name - * @param data source data - * @param format format of data in InputStream + * @param table + * table name + * @param data + * source data + * @param format + * format of data in InputStream * @throws SQLException + * if the upload fails */ public void sendToTable(String table, InputStream data, ClickHouseFormat format) throws SQLException { table(table).data(data).format(format).send(); } /** - * Sends the data in RowBinary or in Native formats + * Sends the data in {@link ClickHouseFormat#RowBinary RowBinary} or in + * {@link ClickHouseFormat#Native Native} format + * + * @param sql + * the SQL statement to execute + * @param callback + * data source for the upload + * @param format + * the format to use, either {@link ClickHouseFormat#RowBinary + * RowBinary} or {@link ClickHouseFormat#Native Native} + * @throws SQLException + * if the upload fails */ public void send(String sql, ClickHouseStreamCallback callback, ClickHouseFormat format) throws SQLException { if (!(RowBinary.equals(format) || Native.equals(format))) { diff --git a/src/main/java/ru/yandex/clickhouse/domain/ClickHouseDataType.java b/src/main/java/ru/yandex/clickhouse/domain/ClickHouseDataType.java index 31f0f2f47..bb9292e9f 100644 --- a/src/main/java/ru/yandex/clickhouse/domain/ClickHouseDataType.java +++ b/src/main/java/ru/yandex/clickhouse/domain/ClickHouseDataType.java @@ -4,8 +4,8 @@ import java.math.BigInteger; import java.sql.Array; import java.sql.Date; +import java.sql.JDBCType; import java.sql.Timestamp; -import java.sql.Types; import java.util.UUID; /** @@ -20,43 +20,43 @@ */ public enum ClickHouseDataType { - IntervalYear (Types.INTEGER, Integer.class, true, 19, 0), - IntervalQuarter (Types.INTEGER, Integer.class, true, 19, 0), - IntervalMonth (Types.INTEGER, Integer.class, true, 19, 0), - IntervalWeek (Types.INTEGER, Integer.class, true, 19, 0), - IntervalDay (Types.INTEGER, Integer.class, true, 19, 0), - IntervalHour (Types.INTEGER, Integer.class, true, 19, 0), - IntervalMinute (Types.INTEGER, Integer.class, true, 19, 0), - IntervalSecond (Types.INTEGER, Integer.class, true, 19, 0), - UInt64 (Types.BIGINT, BigInteger.class, false, 19, 0), - UInt32 (Types.INTEGER, Long.class, false, 10, 0), - UInt16 (Types.SMALLINT, Integer.class, false, 5, 0), - UInt8 (Types.TINYINT, Integer.class, false, 3, 0), - Int64 (Types.BIGINT, Long.class, true, 20, 0, + IntervalYear (JDBCType.INTEGER, Integer.class, true, 19, 0), + IntervalQuarter (JDBCType.INTEGER, Integer.class, true, 19, 0), + IntervalMonth (JDBCType.INTEGER, Integer.class, true, 19, 0), + IntervalWeek (JDBCType.INTEGER, Integer.class, true, 19, 0), + IntervalDay (JDBCType.INTEGER, Integer.class, true, 19, 0), + IntervalHour (JDBCType.INTEGER, Integer.class, true, 19, 0), + IntervalMinute (JDBCType.INTEGER, Integer.class, true, 19, 0), + IntervalSecond (JDBCType.INTEGER, Integer.class, true, 19, 0), + UInt64 (JDBCType.BIGINT, BigInteger.class, false, 19, 0), + UInt32 (JDBCType.BIGINT, Long.class, false, 10, 0), + UInt16 (JDBCType.SMALLINT, Integer.class, false, 5, 0), + UInt8 (JDBCType.TINYINT, Integer.class, false, 3, 0), + Int64 (JDBCType.BIGINT, Long.class, true, 20, 0, "BIGINT"), - Int32 (Types.INTEGER, Integer.class, true, 11, 0, + Int32 (JDBCType.INTEGER, Integer.class, true, 11, 0, "INTEGER", "INT"), - Int16 (Types.SMALLINT, Integer.class, true, 6, 0, + Int16 (JDBCType.SMALLINT, Integer.class, true, 6, 0, "SMALLINT"), - Int8 (Types.TINYINT, Integer.class, true, 4, 0, + Int8 (JDBCType.TINYINT, Integer.class, true, 4, 0, "TINYINT"), - Date (Types.DATE, Date.class, false, 10, 0), - DateTime (Types.TIMESTAMP, Timestamp.class, false, 19, 0, + Date (JDBCType.DATE, Date.class, false, 10, 0), + DateTime (JDBCType.TIMESTAMP, Timestamp.class, false, 19, 0, "TIMESTAMP"), - Enum8 (Types.VARCHAR, String.class, false, 0, 0), - Enum16 (Types.VARCHAR, String.class, false, 0, 0), - Float32 (Types.FLOAT, Float.class, true, 8, 8, - "FLOAT"), - Float64 (Types.DOUBLE, Double.class, true, 17, 17, + Enum8 (JDBCType.VARCHAR, String.class, false, 0, 0), + Enum16 (JDBCType.VARCHAR, String.class, false, 0, 0), + Float32 (JDBCType.REAL, Float.class, true, 8, 8, + "REAL"), + Float64 (JDBCType.DOUBLE, Double.class, true, 17, 17, "DOUBLE"), - Decimal32 (Types.DECIMAL, BigDecimal.class, true, 9, 9), - Decimal64 (Types.DECIMAL, BigDecimal.class, true, 18, 18), - Decimal128 (Types.DECIMAL, BigDecimal.class, true, 38, 38), - Decimal (Types.DECIMAL, BigDecimal.class, true, 0, 0, + Decimal32 (JDBCType.DECIMAL, BigDecimal.class, true, 9, 9), + Decimal64 (JDBCType.DECIMAL, BigDecimal.class, true, 18, 18), + Decimal128 (JDBCType.DECIMAL, BigDecimal.class, true, 38, 38), + Decimal (JDBCType.DECIMAL, BigDecimal.class, true, 0, 0, "DEC"), - UUID (Types.OTHER, UUID.class, false, 36, 0), - String (Types.VARCHAR, String.class, false, 0, 0, + UUID (JDBCType.OTHER, UUID.class, false, 36, 0), + String (JDBCType.VARCHAR, String.class, false, 0, 0, "LONGBLOB", "MEDIUMBLOB", "TINYBLOB", @@ -67,27 +67,27 @@ public enum ClickHouseDataType { "TINYTEXT", "LONGTEXT", "BLOB"), - FixedString (Types.CHAR, String.class, false, -1, 0, + FixedString (JDBCType.CHAR, String.class, false, -1, 0, "BINARY"), - Nothing (Types.NULL, Object.class, false, 0, 0), - Nested (Types.STRUCT, String.class, false, 0, 0), - Tuple (Types.OTHER, String.class, false, 0, 0), - Array (Types.ARRAY, Array.class, false, 0, 0), - AggregateFunction (Types.OTHER, String.class, false, 0, 0), - Unknown (Types.OTHER, String.class, false, 0, 0); + Nothing (JDBCType.NULL, Object.class, false, 0, 0), + Nested (JDBCType.STRUCT, String.class, false, 0, 0), + Tuple (JDBCType.OTHER, String.class, false, 0, 0), + Array (JDBCType.ARRAY, Array.class, false, 0, 0), + AggregateFunction (JDBCType.OTHER, String.class, false, 0, 0), + Unknown (JDBCType.OTHER, String.class, false, 0, 0); - private final int sqlType; + private final JDBCType jdbcType; private final Class javaClass; private final boolean signed; private final int defaultPrecision; private final int defaultScale; private final String[] aliases; - ClickHouseDataType(int sqlType, Class javaClass, + ClickHouseDataType(JDBCType jdbcType, Class javaClass, boolean signed, int defaultPrecision, int defaultScale, String... aliases) { - this.sqlType = sqlType; + this.jdbcType = jdbcType; this.javaClass = javaClass; this.signed = signed; this.defaultPrecision = defaultPrecision; @@ -96,7 +96,11 @@ public enum ClickHouseDataType { } public int getSqlType() { - return sqlType; + return jdbcType.getVendorTypeNumber().intValue(); + } + + public JDBCType getJdbcType() { + return jdbcType; } public Class getJavaClass() { diff --git a/src/main/java/ru/yandex/clickhouse/except/ClickHouseException.java b/src/main/java/ru/yandex/clickhouse/except/ClickHouseException.java index 37d020855..570da79cb 100644 --- a/src/main/java/ru/yandex/clickhouse/except/ClickHouseException.java +++ b/src/main/java/ru/yandex/clickhouse/except/ClickHouseException.java @@ -13,4 +13,10 @@ public ClickHouseException(int code, String message, Throwable cause, String hos super("ClickHouse exception, message: " + message + ", host: " + host + ", port: " + port + "; " + (cause == null ? "" : cause.getMessage()), null, code, cause); } + + public ClickHouseException(int code, String message, Throwable cause) { + super("ClickHouse exception, message: " + message + "; " + + (cause == null ? "" : cause.getMessage()), null, code, cause); + } + } diff --git a/src/main/java/ru/yandex/clickhouse/except/ClickHouseUnknownException.java b/src/main/java/ru/yandex/clickhouse/except/ClickHouseUnknownException.java index d2c295a19..e1bd79708 100644 --- a/src/main/java/ru/yandex/clickhouse/except/ClickHouseUnknownException.java +++ b/src/main/java/ru/yandex/clickhouse/except/ClickHouseUnknownException.java @@ -11,6 +11,10 @@ public ClickHouseUnknownException(String message, Throwable cause, String host, super(ClickHouseErrorCode.UNKNOWN_EXCEPTION.code, message, cause, host, port); } + public ClickHouseUnknownException(String message, Throwable cause) { + super(ClickHouseErrorCode.UNKNOWN_EXCEPTION.code, message, cause); + } + public ClickHouseUnknownException(Integer code, Throwable cause, String host, int port) { super(code, cause, host, port); } diff --git a/src/main/java/ru/yandex/clickhouse/response/AbstractResultSet.java b/src/main/java/ru/yandex/clickhouse/response/AbstractResultSet.java index 14642c959..9c44e6f6a 100644 --- a/src/main/java/ru/yandex/clickhouse/response/AbstractResultSet.java +++ b/src/main/java/ru/yandex/clickhouse/response/AbstractResultSet.java @@ -4,12 +4,27 @@ import java.io.Reader; import java.math.BigDecimal; import java.net.URL; -import java.sql.*; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; import java.util.Calendar; import java.util.Map; public abstract class AbstractResultSet implements ResultSet { + @Override public boolean next() throws SQLException { throw new UnsupportedOperationException(); @@ -952,12 +967,20 @@ public boolean isWrapperFor(Class iface) throws SQLException { throw new UnsupportedOperationException(); } - public long[] getLongArray(String column) throws SQLException { - Array array = getArray(column); - return (long[])array.getArray(); // optimistic - } - - - + /** + * Parse the value in current row at column with label {@code column} as an array + * of long + * + * @param column + * the label, name, alias of the column + * @return an array of longs + * @throws SQLException + * if the value cannot be interpreted as {@code long[]} + * @deprecated prefer to use regular JDBC API, e.g. via + * {@link #getArray(int)} or simply + * {@link #getObject(int, Class)} + */ + @Deprecated + public abstract long[] getLongArray(String column) throws SQLException; } diff --git a/src/main/java/ru/yandex/clickhouse/response/ArrayByteFragment.java b/src/main/java/ru/yandex/clickhouse/response/ArrayByteFragment.java index bf7390b00..602ec42f5 100644 --- a/src/main/java/ru/yandex/clickhouse/response/ArrayByteFragment.java +++ b/src/main/java/ru/yandex/clickhouse/response/ArrayByteFragment.java @@ -1,12 +1,12 @@ package ru.yandex.clickhouse.response; -class ArrayByteFragment extends ByteFragment { +public final class ArrayByteFragment extends ByteFragment { private ArrayByteFragment(byte[] buf, int start, int len) { super(buf, start, len); } - static ArrayByteFragment wrap(ByteFragment fragment) { + public static ArrayByteFragment wrap(ByteFragment fragment) { return new ArrayByteFragment(fragment.buf, fragment.start, fragment.len); } @@ -16,8 +16,5 @@ public boolean isNull() { return len == 4 && buf[start] == 'N' && buf[start + 1] == 'U' && buf[start + 2] == 'L' && buf[start + 3] == 'L'; } - public boolean isNaN() { - // nan - return len == 3 && buf[start] == 'n' && buf[start + 1] == 'a' && buf[start + 2] == 'n'; - } + } diff --git a/src/main/java/ru/yandex/clickhouse/response/ByteFragment.java b/src/main/java/ru/yandex/clickhouse/response/ByteFragment.java index f9e3037f9..f7c9d379c 100644 --- a/src/main/java/ru/yandex/clickhouse/response/ByteFragment.java +++ b/src/main/java/ru/yandex/clickhouse/response/ByteFragment.java @@ -1,11 +1,11 @@ package ru.yandex.clickhouse.response; -import ru.yandex.clickhouse.util.guava.StreamUtils; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.OutputStream; +import ru.yandex.clickhouse.util.guava.StreamUtils; + public class ByteFragment { protected final byte[] buf; @@ -30,7 +30,9 @@ public String asString() { public String asString(boolean unescape) { if(unescape) { - if (isNull()) return null; + if (isNull()) { + return null; + } return new String(unescape(), StreamUtils.UTF_8); } else { return asString(); @@ -42,6 +44,15 @@ public boolean isNull() { return len == 2 && buf[start] == '\\' && buf[start + 1] == 'N'; } + public boolean isEmpty() { + return len == 0; + } + + public boolean isNaN() { + // nan + return len == 3 && buf[start] == 'n' && buf[start + 1] == 'a' && buf[start + 2] == 'n'; + } + @Override public String toString() { StringBuilder b = new StringBuilder(); @@ -72,7 +83,9 @@ public ByteFragment[] split(byte sep) { } } catch (IOException ignore) { } - if(res[c-1] == null) res[c-1] = ByteFragment.EMPTY; + if(res[c-1] == null) { + res[c-1] = ByteFragment.EMPTY; + } return res; } // [45, 49, 57, 52, 49, 51, 56, 48, 57, 49, 52, 9, 9, 50, 48, 49, 50, 45, 48, 55, 45, 49, 55, 32, 49, 51, 58, 49, 50, 58, 50, 49, 9, 49, 50, 49, 50, 55, 53, 53, 9, 50, 57, 57, 57, 55, 55, 57, 57, 55, 56, 9, 48, 9, 52, 48, 57, 49, 57, 55, 52, 49, 49, 51, 50, 56, 53, 53, 50, 54, 57, 51, 9, 51, 9, 54, 9, 50, 48, 9, 48, 92, 48, 9, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, 97, 118, 105, 116, 111, 46, 114, 117, 47, 99, 97, 116, 97, 108, 111, 103, 47, 103, 97, 114, 97, 122, 104, 105, 95, 105, 95, 109, 97, 115, 104, 105, 110, 111, 109, 101, 115, 116, 97, 45, 56, 53, 47, 116, 97, 116, 97, 114, 115, 116, 97, 110, 45, 54, 53, 48, 49, 51, 48, 47, 112, 97, 103, 101, 56, 9, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, 97, 118, 105, 116, 111, 46, 114, 117, 47, 99, 97, 116, 97, 108, 111, 103, 47, 103, 97, 114, 97, 122, 104, 105, 95, 105, 95, 109, 97, 115, 104, 105, 110, 111, 109, 101, 115, 116, 97, 45, 56, 53, 47, 116, 97, 116, 97, 114, 115, 116, 97, 110, 45, 54, 53, 48, 49, 51, 48, 47, 112, 97, 103, 101, 55, 9, 48, 9, 48, 9, 50, 56, 53, 55, 48, 56, 48, 9, 45, 49, 9, 48, 9, 9, 48, 9, 48, 9, 48, 9, 45, 49, 9, 48, 48, 48, 48, 45, 48, 48, 45, 48, 48, 32, 48, 48, 58, 48, 48, 58, 48, 48, 9, 9, 48, 9, 48, 9, 103, 9, 45, 49, 9, 45, 49, 9, 45, 49, 9] @@ -174,7 +187,9 @@ public byte[] unescape() { for (int i = 0; i < convert.length; i++) { reverse[i] = -1; byte c = convert[i]; - if (c != -1) reverse[c] = (byte) i; + if (c != -1) { + reverse[c] = (byte) i; + } } } diff --git a/src/main/java/ru/yandex/clickhouse/response/ByteFragmentUtils.java b/src/main/java/ru/yandex/clickhouse/response/ByteFragmentUtils.java deleted file mode 100644 index d26df93e5..000000000 --- a/src/main/java/ru/yandex/clickhouse/response/ByteFragmentUtils.java +++ /dev/null @@ -1,339 +0,0 @@ -package ru.yandex.clickhouse.response; - - -import com.google.common.primitives.Primitives; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.sql.Date; -import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; - -final class ByteFragmentUtils { - - private static final char ARRAY_ELEMENTS_SEPARATOR = ','; - private static final char STRING_QUOTATION = '\''; - private static final int MAX_ARRAY_DEPTH = 32; - - private ByteFragmentUtils() { - } - - static int parseInt(ByteFragment s) throws NumberFormatException { - if (s == null) { - throw new NumberFormatException("null"); - } - - if (s.isNull()) { - return 0; //jdbc spec - } - - int result = 0; - boolean negative = false; - int i = 0, max = s.length(); - int limit; - int multmin; - int digit; - - if (max > 0) { - if (s.charAt(0) == '-') { - negative = true; - limit = Integer.MIN_VALUE; - i++; - } else { - limit = -Integer.MAX_VALUE; - } - multmin = limit / 10; - if (i < max) { - digit = s.charAt(i++) - 0x30; //Character.digit(s.charAt(i++), 10); - if (digit < 0) { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } else { - result = -digit; - } - } - while (i < max) { - // Accumulating negatively avoids surprises near MAX_VALUE - digit = s.charAt(i++) - 0x30; // Character.digit(s.charAt(i++), 10); - if (digit < 0 || digit > 9) { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - if (result < multmin) { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - result *= 10; - if (result < limit + digit) { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - result -= digit; - } - } else { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - if (negative) { - if (i > 1) { - return result; - } else { /* Only got "-" */ - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - } else { - return -result; - } - } - - - static long parseLong(ByteFragment s) throws NumberFormatException { - if (s == null) { - throw new NumberFormatException("null"); - } - - if (s.isNull()) { - return 0; //jdbc spec - } - - long result = 0; - boolean negative = false; - int i = 0, max = s.length(); - long limit; - long multmin; - int digit; - - if (max > 0) { - if (s.charAt(0) == '-') { - negative = true; - limit = Long.MIN_VALUE; - i++; - } else { - limit = -Long.MAX_VALUE; - } - multmin = limit / 10; - if (i < max) { - digit = s.charAt(i++) - 0x30; // Character.digit(s.charAt(i++), 10); - if (digit < 0 || digit > 9) { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } else { - result = -digit; - } - } - while (i < max) { - // Accumulating negatively avoids surprises near MAX_VALUE - digit = s.charAt(i++) - 0x30; // Character.digit(s.charAt(i++), 10); - if (digit < 0 || digit > 9) { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - if (result < multmin) { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - result *= 10; - if (result < limit + digit) { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - result -= digit; - } - } else { - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - if (negative) { - if (i > 1) { - return result; - } else { /* Only got "-" */ - throw new NumberFormatException("For input string: \"" + s.asString() + '"'); - } - } else { - return -result; - } - } - - static Object parseArray(ByteFragment value, Class elementClass, int arrayLevel) { - return parseArray(value, elementClass, false, null, arrayLevel); - } - - static Object parseArray(ByteFragment value, Class elementClass, SimpleDateFormat dateFormat, int arrayLevel) { - return parseArray(value, elementClass, false, dateFormat, arrayLevel); - } - - static Object parseArray(ByteFragment value, Class elementClass, boolean useObjects, int arrayLevel) { - return parseArray(value, elementClass, useObjects, null, arrayLevel); - } - - static Object parseArray(ByteFragment value, Class elementClass, boolean useObjects, SimpleDateFormat dateFormat, int arrayLevel) { - if (arrayLevel > MAX_ARRAY_DEPTH) { - throw new IllegalArgumentException("Maximum parse depth exceeded"); - } - - if (value.isNull()) { - return null; - } - - if (value.charAt(0) != '[' || value.charAt(value.length() - 1) != ']') { - throw new IllegalArgumentException("not an array: " + value); - } - - if ((elementClass == Date.class || elementClass == Timestamp.class) && dateFormat == null) { - throw new IllegalArgumentException("DateFormat must be provided for date/dateTime array"); - } - - ByteFragment trim = value.subseq(1, value.length() - 2); - - int index = 0; - Object array; - if (arrayLevel > 1) { - int[] dimensions = new int[arrayLevel]; - dimensions[0] = getArrayLength(trim); - array = java.lang.reflect.Array.newInstance( - useObjects ? elementClass : Primitives.unwrap(elementClass), - dimensions - ); - } else { - array = java.lang.reflect.Array.newInstance( - useObjects ? elementClass : Primitives.unwrap(elementClass), - getArrayLength(trim) - ); - } - int fieldStart = 0; - int currentLevel = 0; - boolean inQuotation = false; - for (int chIdx = 0; chIdx < trim.length(); chIdx++) { - int ch = trim.charAt(chIdx); - - if (ch == '\\') { - chIdx++; - } - inQuotation = ch == STRING_QUOTATION ^ inQuotation; - if (!inQuotation) { - if (ch == '[') { - currentLevel++; - } else if (ch == ']') { - currentLevel--; - } - } - - if (!inQuotation && ch == ARRAY_ELEMENTS_SEPARATOR && currentLevel == 0 || chIdx == trim.length() - 1) { - int fieldEnd = chIdx == trim.length() - 1 ? chIdx + 1 : chIdx; - if (trim.charAt(fieldStart) == '\'') { - fieldStart++; - fieldEnd--; - } - ArrayByteFragment fragment = ArrayByteFragment.wrap(trim.subseq(fieldStart, fieldEnd - fieldStart)); - if (arrayLevel > 1) { - Object arrayValue = parseArray(fragment, elementClass, useObjects, dateFormat, arrayLevel - 1); - java.lang.reflect.Array.set(array, index++, arrayValue); - } else if (elementClass == String.class) { - String stringValue = fragment.asString(true); - java.lang.reflect.Array.set(array, index++, stringValue); - } else if (elementClass == Long.class) { - Long longValue; - if (fragment.isNull()) { - longValue = useObjects ? null : 0L; - } else { - longValue = parseLong(fragment); - } - java.lang.reflect.Array.set(array, index++, longValue); - } else if (elementClass == Integer.class) { - Integer intValue; - if (fragment.isNull()) { - intValue = useObjects ? null : 0; - } else { - intValue = parseInt(fragment); - } - java.lang.reflect.Array.set(array, index++, intValue); - } else if (elementClass == BigInteger.class) { - BigInteger bigIntegerValue; - if (fragment.isNull()) { - bigIntegerValue = null; - } else { - bigIntegerValue = new BigInteger(fragment.asString(true)); - } - java.lang.reflect.Array.set(array, index++, bigIntegerValue); - } else if (elementClass == BigDecimal.class) { - BigDecimal bigDecimalValue; - if (fragment.isNull()) { - bigDecimalValue = null; - } else { - bigDecimalValue = new BigDecimal(fragment.asString(true)); - } - java.lang.reflect.Array.set(array, index++, bigDecimalValue); - } else if (elementClass == Float.class) { - Float floatValue; - if (fragment.isNull()) { - floatValue = useObjects ? null : 0.0F; - } else if (fragment.isNaN()) { - floatValue = Float.NaN; - } else { - floatValue = Float.parseFloat(fragment.asString()); - } - java.lang.reflect.Array.set(array, index++, floatValue); - } else if (elementClass == Double.class) { - Double doubleValue; - if (fragment.isNull()) { - doubleValue = useObjects ? null : 0.0; - } else if (fragment.isNaN()) { - doubleValue = Double.NaN; - } else { - doubleValue = Double.parseDouble(fragment.asString()); - } - java.lang.reflect.Array.set(array, index++, doubleValue); - } else if (elementClass == Date.class) { - Date dateValue; - if (fragment.isNull()) { - dateValue = null; - } else { - try { - dateValue = new Date(dateFormat.parse(fragment.asString()).getTime()); - } catch (ParseException e) { - throw new IllegalArgumentException(e); - } - } - java.lang.reflect.Array.set(array, index++, dateValue); - } else if (elementClass == Timestamp.class) { - Timestamp dateTimeValue; - if (fragment.isNull()) { - dateTimeValue = null; - } else { - try { - dateTimeValue = new Timestamp(dateFormat.parse(fragment.asString()).getTime()); - } catch (ParseException e) { - throw new IllegalArgumentException(e); - } - } - java.lang.reflect.Array.set(array, index++, dateTimeValue); - } else { - throw new IllegalStateException(); - } - - fieldStart = chIdx + 1; - } - } - - return array; - } - - private static int getArrayLength(ByteFragment value) { - if (value.length() == 0) { - return 0; - } - - int length = 1; - boolean inQuotation = false; - int arrayLevel = 0; - for (int i = 0; i < value.length(); i++) { - int ch = value.charAt(i); - - if (ch == '\\') { - i++; - } - - inQuotation = ch == STRING_QUOTATION ^ inQuotation; - if (!inQuotation) { - if (ch == '[') { - ++arrayLevel; - } else if (ch == ']') { - --arrayLevel; - } else if (ch == ARRAY_ELEMENTS_SEPARATOR && arrayLevel == 0) { - ++length; - } - } - } - return length; - } -} diff --git a/src/main/java/ru/yandex/clickhouse/response/ClickHouseColumnInfo.java b/src/main/java/ru/yandex/clickhouse/response/ClickHouseColumnInfo.java index 5783300c7..b08305c9b 100644 --- a/src/main/java/ru/yandex/clickhouse/response/ClickHouseColumnInfo.java +++ b/src/main/java/ru/yandex/clickhouse/response/ClickHouseColumnInfo.java @@ -156,15 +156,23 @@ boolean isLowCardinality() { return lowCardinality; } - int getArrayLevel() { + public int getArrayLevel() { return arrayLevel; } + public boolean isArray() { + return arrayLevel > 0; + } + public ClickHouseDataType getArrayBaseType() { return arrayBaseType; } - TimeZone getTimeZone() { + public ClickHouseDataType getEffectiveClickHouseDataType() { + return arrayLevel > 0 ? arrayBaseType : clickHouseDataType; + } + + public TimeZone getTimeZone() { return timeZone; } diff --git a/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java b/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java index 11e190797..a9e57855c 100644 --- a/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java +++ b/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java @@ -13,8 +13,7 @@ import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -27,21 +26,18 @@ import ru.yandex.clickhouse.ClickHouseStatement; import ru.yandex.clickhouse.domain.ClickHouseDataType; import ru.yandex.clickhouse.except.ClickHouseExceptionSpecifier; +import ru.yandex.clickhouse.except.ClickHouseUnknownException; +import ru.yandex.clickhouse.response.parser.ClickHouseValueParser; import ru.yandex.clickhouse.settings.ClickHouseProperties; - -import static ru.yandex.clickhouse.response.ByteFragmentUtils.parseArray; +import ru.yandex.clickhouse.util.ClickHouseArrayUtil; public class ClickHouseResultSet extends AbstractResultSet { private final static long[] EMPTY_LONG_ARRAY = new long[0]; - private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; - private static final String DATE_PATTERN = "yyyy-MM-dd"; private final TimeZone dateTimeTimeZone; private final TimeZone dateTimeZone; - private final SimpleDateFormat dateTimeFormat = new SimpleDateFormat(DATE_TIME_PATTERN); - private final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN); private final StreamSplitter bis; @@ -77,7 +73,7 @@ public class ClickHouseResultSet extends AbstractResultSet { // it does not do prefetch. It is effectively a witness // to the fact that rs.next() returned false. private boolean lastReached = false; - + private boolean isAfterLastReached = false; public ClickHouseResultSet(InputStream is, int bufferSize, String db, String table, @@ -93,8 +89,6 @@ public ClickHouseResultSet(InputStream is, int bufferSize, String db, String tab this.dateTimeZone = properties.isUseServerTimeZoneForDates() ? timeZone : TimeZone.getDefault(); - dateTimeFormat.setTimeZone(dateTimeTimeZone); - dateFormat.setTimeZone(dateTimeZone); bis = new StreamSplitter(is, (byte) 0x0A, bufferSize); /// \n ByteFragment headerFragment = bis.next(); if (headerFragment == null) { @@ -111,7 +105,7 @@ public ClickHouseResultSet(InputStream is, int bufferSize, String db, String tab throw new IllegalArgumentException("ClickHouse response without column types"); } String[] types = toStringArray(typesFragment); - columns = new ArrayList(cols.length); + columns = new ArrayList<>(cols.length); for (int i = 0; i < cols.length; i++) { columns.add(ClickHouseColumnInfo.parse(types[i], cols[i])); } @@ -127,6 +121,17 @@ private static String[] toStringArray(ByteFragment headerFragment) { return c; } + /** + * Check if there is another row + * + * @return {@code true} if this result set has another row after the current + * cursor position, {@code false} else + * @throws SQLException + * if something goes wrong + * @deprecated prefer to use JDBC API methods, for example {@link #isLast()} + * or simply looping using {@code while (rs.next())} + */ + @Deprecated public boolean hasNext() throws SQLException { if (nextLine == null && !lastReached) { try { @@ -149,12 +154,12 @@ public boolean hasNext() throws SQLException { return nextLine != null; } - @Override + @Override public boolean isBeforeFirst() throws SQLException { return rowNumber == 0 && hasNext(); } - @Override + @Override public boolean isAfterLast() throws SQLException { return isAfterLastReached; } @@ -166,8 +171,8 @@ public boolean isFirst() throws SQLException { @Override public boolean isLast() throws SQLException { - return !hasNext(); - // && !isAfterLastReached should be probably added, + return !hasNext(); + // && !isAfterLastReached should be probably added, // but it may brake compatibility with the previous implementation } @@ -199,7 +204,7 @@ private boolean onTheSeparatorRow() throws IOException { return onSeparatorRow; } - private void checkValues(List columns, ByteFragment[] values, + private static void checkValues(List columns, ByteFragment[] values, ByteFragment fragment) throws SQLException { if (columns.size() != values.length) { @@ -235,6 +240,7 @@ public void getTotals() throws SQLException { this.next(); } + // this method is mocked in a test, do not make it final :-) List getColumns() { return Collections.unmodifiableList(columns); } @@ -244,10 +250,6 @@ public ResultSetMetaData getMetaData() throws SQLException { return new ClickHouseResultSetMetaData(this); } - - ///////////////////////////////////////////////////////// - - @Override public boolean wasNull() throws SQLException { if (lastReadColumn == 0) { @@ -257,70 +259,63 @@ public boolean wasNull() throws SQLException { } @Override - public int getInt(String column) { - return getInt(asColNum(column)); + public int getInt(String column) throws SQLException { + return getInt(findColumn(column)); } @Override - public boolean getBoolean(String column) { - return getBoolean(asColNum(column)); + public boolean getBoolean(String column) throws SQLException { + return getBoolean(findColumn(column)); } @Override - public long getLong(String column) { - return getLong(asColNum(column)); + public long getLong(String column) throws SQLException { + return getLong(findColumn(column)); } @Override - public String getString(String column) { - return getString(asColNum(column)); + public String getString(String column) throws SQLException { + return getString(findColumn(column)); } @Override - public byte[] getBytes(String column) { - return getBytes(asColNum(column)); - } - - public Long getTimestampAsLong(String column) { - return getTimestampAsLong(asColNum(column)); + public byte[] getBytes(String column) throws SQLException { + return getBytes(findColumn(column)); } @Override public Timestamp getTimestamp(String column) throws SQLException { - Long value = getTimestampAsLong(column); - return value == null ? null : new Timestamp(value); + return getTimestamp(findColumn(column)); } @Override public Timestamp getTimestamp(String column, Calendar cal) throws SQLException { - Long value = getTimestampAsLong(asColNum(column), cal.getTimeZone()); - return value == null ? null : new Timestamp(value); + return getTimestamp(findColumn(column), cal); } @Override public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { - Long value = getTimestampAsLong(columnIndex, cal.getTimeZone()); - return value == null ? null : new Timestamp(value); + return getTimestamp(columnIndex); } @Override - public short getShort(String column) { - return getShort(asColNum(column)); + public short getShort(String column) throws SQLException { + return getShort(findColumn(column)); } @Override - public byte getByte(String column) { - return getByte(asColNum(column)); + public byte getByte(String column) throws SQLException { + return getByte(findColumn(column)); } @Override - public long[] getLongArray(String column) { - return getLongArray(asColNum(column)); + public long[] getLongArray(String column) throws SQLException { + return getLongArray(findColumn(column)); } @Override public Array getArray(int columnIndex) throws SQLException { - ClickHouseColumnInfo colInfo = columns.get(columnIndex - 1); + ClickHouseColumnInfo colInfo = getColumnInfo(columnIndex); if (colInfo.getClickHouseDataType() != ClickHouseDataType.Array) { throw new SQLException("Column not an array"); } @@ -328,88 +323,80 @@ public Array getArray(int columnIndex) throws SQLException { final Object array; switch (colInfo.getArrayBaseType()) { case Date : - array = parseArray( + array = ClickHouseArrayUtil.parseArray( getValue(columnIndex), - colInfo.getArrayBaseType().getJavaClass(), properties.isUseObjectsInArrays(), - dateFormat, - colInfo.getArrayLevel() + dateTimeZone, + colInfo ); break; - case DateTime : + default : TimeZone timeZone = colInfo.getTimeZone() != null ? colInfo.getTimeZone() : dateTimeTimeZone; - dateTimeFormat.setTimeZone(timeZone); - array = parseArray( - getValue(columnIndex), - colInfo.getArrayBaseType().getJavaClass(), - properties.isUseObjectsInArrays(), - dateTimeFormat, - colInfo.getArrayLevel() - ); - break; - default : - array = parseArray( + array = ClickHouseArrayUtil.parseArray( getValue(columnIndex), - colInfo.getArrayBaseType().getJavaClass(), properties.isUseObjectsInArrays(), - colInfo.getArrayLevel() + timeZone, + colInfo ); break; } - return new ClickHouseArray(colInfo.getArrayBaseType(), array); } @Override public Array getArray(String column) throws SQLException { - return getArray(asColNum(column)); + return getArray(findColumn(column)); } @Override public double getDouble(String columnLabel) throws SQLException { - return getDouble(asColNum(columnLabel)); + return getDouble(findColumn(columnLabel)); } @Override public float getFloat(String columnLabel) throws SQLException { - return getFloat(asColNum(columnLabel)); + return getFloat(findColumn(columnLabel)); } @Override public Date getDate(String columnLabel) throws SQLException { - return getDate(asColNum(columnLabel)); + return getDate(findColumn(columnLabel)); } @Override public Time getTime(String columnLabel) throws SQLException { - return getTime(asColNum(columnLabel)); + return getTime(findColumn(columnLabel)); } @Override public Object getObject(String columnLabel) throws SQLException { - return getObject(asColNum(columnLabel)); + return getObject(findColumn(columnLabel)); } @Override - public String getString(int colNum) { - return toString(getValue(colNum)); + public String getString(int colNum) throws SQLException { + return ClickHouseValueParser.getParser(String.class) + .parse(getValue(colNum), getColumnInfo(colNum), null); } @Override - public int getInt(int colNum) { - return ByteFragmentUtils.parseInt(getValue(colNum)); + public int getInt(int colNum) throws SQLException { + return ClickHouseValueParser.parseInt( + getValue(colNum), getColumnInfo(colNum)); } @Override - public boolean getBoolean(int colNum) { - return toBoolean(getValue(colNum)); + public boolean getBoolean(int colNum) throws SQLException { + return ClickHouseValueParser.parseBoolean( + getValue(colNum), getColumnInfo(colNum)); } @Override - public long getLong(int colNum) { - return ByteFragmentUtils.parseLong(getValue(colNum)); + public long getLong(int colNum) throws SQLException { + return ClickHouseValueParser.parseLong( + getValue(colNum), getColumnInfo(colNum)); } @Override @@ -417,27 +404,66 @@ public byte[] getBytes(int colNum) { return toBytes(getValue(colNum)); } + /** + * Tries to parse the value as a timestamp using the connection time zone if + * applicable and return its representation as milliseconds since epoch + * + * @param colNum + * column number + * @return timestamp value as milliseconds since epoch + * @deprecated prefer to use regular JDBC API methods, e.g. + * {@link #getTimestamp(int)} or {@link #getObject(int, Class)} + * using {@link Instant} + */ + @Deprecated public Long getTimestampAsLong(int colNum) { - ClickHouseColumnInfo info = columns.get(colNum - 1); + ClickHouseColumnInfo info = getColumnInfo(colNum); TimeZone timeZone = info.getTimeZone() != null ? info.getTimeZone() : dateTimeTimeZone; - return toTimestamp(getValue(colNum), timeZone); - } - - public Long getTimestampAsLong(int colNum, TimeZone tz) { - return toTimestamp(getValue(colNum), tz); + return getTimestampAsLong(colNum, timeZone); + } + + /** + * Tries to parse the value as a timestamp and return its representation as + * milliseconds since epoch + * + * @param colNum + * the column number + * @param timeZone + * time zone to use when parsing date / date time values + * @return value interpreted as timestamp as milliseconds since epoch + * @deprecated prefer to use regular JDBC API method + */ + @Deprecated + public Long getTimestampAsLong(int colNum, TimeZone timeZone) { + ByteFragment value = getValue(colNum); + if (value.isNull() || value.asString().equals("0000-00-00 00:00:00")) { + return null; + } + try { + Instant instant = ClickHouseValueParser.getParser(Instant.class) + .parse(value, getColumnInfo(colNum), timeZone); + return Long.valueOf(instant.toEpochMilli()); + } catch (SQLException sqle) { + throw new RuntimeException(sqle); + } } @Override public Timestamp getTimestamp(int columnIndex) throws SQLException { - Long value = getTimestampAsLong(columnIndex); - return value == null ? null : new Timestamp(value.longValue()); + ByteFragment value = getValue(columnIndex); + if (value.isNull()) { + return null; + } + return ClickHouseValueParser.getParser(Timestamp.class).parse( + value, getColumnInfo(columnIndex), dateTimeTimeZone); } @Override - public short getShort(int colNum) { - return toShort(getValue(colNum)); + public short getShort(int colNum) throws SQLException { + return ClickHouseValueParser.parseShort( + getValue(colNum), getColumnInfo(colNum)); } @Override @@ -445,29 +471,32 @@ public byte getByte(int colNum) { return toByte(getValue(colNum)); } - public long[] getLongArray(int colNum) { - return toLongArray(getValue(colNum)); + /** + * Parse the value in current row at column index {@code colNum} as an array + * of long + * + * @param colNum + * column number + * @return an array of longs + * @throws SQLException + * if the value cannot be interpreted as {@code long[]} + * @deprecated prefer to use regular JDBC API + */ + @Deprecated + public long[] getLongArray(int colNum) throws SQLException { + return toLongArray(getValue(colNum), getColumnInfo(colNum)); } @Override public float getFloat(int columnIndex) throws SQLException { - return (float) getDouble(columnIndex); + return ClickHouseValueParser.parseFloat( + getValue(columnIndex), getColumnInfo(columnIndex)); } @Override public double getDouble(int columnIndex) throws SQLException { - String string = getString(columnIndex); - if (string == null){ - return 0; - } else if (string.equals("nan")) { - return Double.NaN; - } else if (string.equals("+inf") || string.equals("inf")) { - return Double.POSITIVE_INFINITY; - } else if (string.equals("-inf")) { - return Double.NEGATIVE_INFINITY; - } else { - return Double.parseDouble(string); - } + return ClickHouseValueParser.parseDouble( + getValue(columnIndex), getColumnInfo(columnIndex)); } @Override @@ -477,26 +506,28 @@ public Statement getStatement() { @Override public Date getDate(int columnIndex) throws SQLException { - // date is passed as a string from clickhouse - ByteFragment value = getValue(columnIndex); - if (value.isNull() || value.asString().equals("0000-00-00")) { - return null; - } - try { - return new Date(dateFormat.parse(value.asString()).getTime()); - } catch (ParseException e) { - return null; - } + return ClickHouseValueParser.getParser(Date.class).parse( + getValue(columnIndex), + getColumnInfo(columnIndex), + dateTimeZone); + } + + @Override + public Date getDate(int columnIndex, Calendar calendar) throws SQLException { + return getDate(columnIndex); } @Override public Time getTime(int columnIndex) throws SQLException { - Timestamp ts = getTimestamp(columnIndex); - if (ts == null) { - return null; - } + return ClickHouseValueParser.getParser(Time.class).parse( + getValue(columnIndex), + getColumnInfo(columnIndex), + dateTimeTimeZone); + } - return new Time(ts.getTime()); + @Override + public Time getTime(int columnIndex, Calendar calendar) throws SQLException { + return getTime(columnIndex); } @Override @@ -505,31 +536,33 @@ public Object getObject(int columnIndex) throws SQLException { if (getValue(columnIndex).isNull()) { return null; } - ClickHouseDataType chType = columns.get(columnIndex - 1).getClickHouseDataType(); + ClickHouseDataType chType = getColumnInfo(columnIndex).getClickHouseDataType(); int type = chType.getSqlType(); switch (type) { case Types.BIGINT: - if (!chType.isSigned()){ - String stringVal = getString(columnIndex); - return new BigInteger(stringVal); + if (chType == ClickHouseDataType.UInt64) { + return getObject(columnIndex, BigInteger.class); } - return getLong(columnIndex); + return getObject(columnIndex, Long.class); case Types.INTEGER: if (!chType.isSigned()){ - return getLong(columnIndex); + return getObject(columnIndex, Long.class); } - return getInt(columnIndex); + return getObject(columnIndex, Integer.class); case Types.TINYINT: case Types.SMALLINT: - return getInt(columnIndex); + return getObject(columnIndex, Integer.class); case Types.VARCHAR: return getString(columnIndex); - case Types.FLOAT: return getFloat(columnIndex); - case Types.DOUBLE: return getDouble(columnIndex); + case Types.REAL: return getObject(columnIndex, Float.class); + case Types.FLOAT: + case Types.DOUBLE: return getObject(columnIndex, Double.class); case Types.DATE: return getDate(columnIndex); case Types.TIMESTAMP: return getTimestamp(columnIndex); case Types.BLOB: return getString(columnIndex); case Types.ARRAY: return getArray(columnIndex); case Types.DECIMAL: return getBigDecimal(columnIndex); + default: + // do not return } switch (chType) { case UUID : @@ -538,7 +571,9 @@ public Object getObject(int columnIndex) throws SQLException { return getString(columnIndex); } } catch (Exception e) { - throw new RuntimeException("Parse exception: " + values[columnIndex - 1].toString(), e); + throw new ClickHouseUnknownException( + "Parse exception: " + values[columnIndex - 1].toString(), + e); } } @@ -551,20 +586,6 @@ private static byte toByte(ByteFragment value) { return Byte.parseByte(value.asString()); } - private static short toShort(ByteFragment value) { - if (value.isNull()) { - return 0; - } - return Short.parseShort(value.asString()); - } - - private static boolean toBoolean(ByteFragment value) { - if (value.isNull()) { - return false; - } - return "1".equals(value.asString()); // 1 or 0 there - } - private static byte[] toBytes(ByteFragment value) { if (value.isNull()) { return null; @@ -572,11 +593,9 @@ private static byte[] toBytes(ByteFragment value) { return value.unescape(); } - private static String toString(ByteFragment value) { - return value.asString(true); - } - - static long[] toLongArray(ByteFragment value) { + static long[] toLongArray(ByteFragment value, ClickHouseColumnInfo columnInfo) + throws SQLException + { if (value.isNull()) { return null; } @@ -590,28 +609,11 @@ static long[] toLongArray(ByteFragment value) { ByteFragment[] values = trim.split((byte) ','); long[] result = new long[values.length]; for (int i = 0; i < values.length; i++) { - result[i] = ByteFragmentUtils.parseLong(values[i]); + result[i] = ClickHouseValueParser.parseLong(values[i], columnInfo); } return result; } - private Long toTimestamp(ByteFragment value, TimeZone timeZone) { - if (value.isNull() || value.asString().equals("0000-00-00 00:00:00")) { - return null; - } - try { - dateTimeFormat.setTimeZone(timeZone); - return dateTimeFormat.parse(value.asString()).getTime(); - } catch (ParseException e) { - throw new RuntimeException(e); - } - } - - @Override - public int findColumn(String columnLabel) throws SQLException { - return asColNum(columnLabel); - } - ////// @Override @@ -639,14 +641,18 @@ public void setMaxRows(int maxRows) { ///// // 1-based index in column list - private int asColNum(String column) { + @Override + public int findColumn(String column) throws SQLException { + if (column == null || column.isEmpty()) { + throw new ClickHouseUnknownException( + "column name required", null); + } for (int i = 0; i < columns.size(); i++) { - if (column.equals(columns.get(i).getColumnName())) { + if (column.equalsIgnoreCase(columns.get(i).getColumnName())) { return i+1; } } - // TODO Java8 - throw new RuntimeException("no column " + column + " in columns list " + getColumnNamesString()); + throw new SQLException("no column " + column + " in columns list " + getColumnNamesString()); } private ByteFragment getValue(int colNum) { @@ -654,50 +660,62 @@ private ByteFragment getValue(int colNum) { return values[colNum - 1]; } + private ClickHouseColumnInfo getColumnInfo(int colNum) { + return columns.get(colNum - 1); + } + + @SuppressWarnings("unchecked") + @Override public T getObject(int columnIndex, Class type) throws SQLException { - if(type.equals(UUID.class)) { - return (T) UUID.fromString(getString(columnIndex)); - } else { - throw new SQLException("Not implemented for type=" + type.toString()); - } + TimeZone tz = Date.class.equals(type) + ? dateTimeZone + : dateTimeTimeZone; + ClickHouseColumnInfo columnInfo = getColumnInfo(columnIndex); + return columnInfo.isArray() + ? (T) getArray(columnIndex) + : ClickHouseValueParser.getParser(type) + .parse(getValue(columnIndex), getColumnInfo(columnIndex), tz); } + @Override public T getObject(String columnLabel, Class type) throws SQLException { - return getObject(asColNum(columnLabel), type); + return getObject(findColumn(columnLabel), type); } + /** + * Retrieve the results in "raw" form. + * + * @return the results as an array of {@link ByteFragment}s + * @deprecated prefer to use regular JDBC API to retrieve the results + */ + @Deprecated public ByteFragment[] getValues() { return values; } @Override - public BigDecimal getBigDecimal(String columnLabel) { - return getBigDecimal(asColNum(columnLabel)); + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return getBigDecimal(findColumn(columnLabel)); } @Override - public BigDecimal getBigDecimal(int columnIndex) { - String string = getString(columnIndex); - if (string == null) { - return null; - } - return new BigDecimal(string); + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + return ClickHouseValueParser.getParser(BigDecimal.class) + .parse(getValue(columnIndex), getColumnInfo(columnIndex), null); } - @Override - public BigDecimal getBigDecimal(String columnLabel, int scale) { - return getBigDecimal(asColNum(columnLabel), scale); + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return getBigDecimal(findColumn(columnLabel), scale); } @Override - public BigDecimal getBigDecimal(int columnIndex, int scale) { - String string = getString(columnIndex); - if (string == null) { - return null; - } - BigDecimal result = new BigDecimal(string); - return result.setScale(scale, RoundingMode.HALF_UP); + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + BigDecimal result = ClickHouseValueParser.getParser(BigDecimal.class) + .parse(getValue(columnIndex), getColumnInfo(columnIndex), null); + return result != null + ? result.setScale(scale, RoundingMode.HALF_UP) + : null; } public String[] getColumnNames() { @@ -722,8 +740,8 @@ public void setFetchSize(int rows) throws SQLException { @Override public String toString() { return "ClickHouseResultSet{" + - "sdf=" + dateTimeFormat + - ", dateFormat=" + dateFormat + + "dateTimeTimeZone=" + dateTimeTimeZone.toString() + + ", dateTimeZone=" + dateTimeZone.toString() + ", bis=" + bis + ", db='" + db + '\'' + ", table='" + table + '\'' + diff --git a/src/main/java/ru/yandex/clickhouse/response/FastByteArrayOutputStream.java b/src/main/java/ru/yandex/clickhouse/response/FastByteArrayOutputStream.java index c890334f3..b8e7321f8 100644 --- a/src/main/java/ru/yandex/clickhouse/response/FastByteArrayOutputStream.java +++ b/src/main/java/ru/yandex/clickhouse/response/FastByteArrayOutputStream.java @@ -117,9 +117,9 @@ public int size() { /** - * Closing a ByteArrayOutputStream has no effect. The methods in + * Closing a {@code ByteArrayOutputStream} has no effect. The methods in * this class can be called after the stream has been closed without - * generating an IOException. + * generating an {@code IOException}. */ @Override public void close() throws IOException { @@ -156,7 +156,7 @@ public void copyTo(DataOutput dest) throws IOException { * @return a input stream contained all bytes recorded in a current stream */ public FastByteArrayInputStream convertToInputStream() { - return new FastByteArrayInputStream(buf, count); + return new FastByteArrayInputStream(buf, count); } public ByteBuffer toByteBuffer() { diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDateValueParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDateValueParser.java new file mode 100644 index 000000000..49a741636 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDateValueParser.java @@ -0,0 +1,193 @@ +package ru.yandex.clickhouse.response.parser; + +import java.math.BigInteger; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.Objects; +import java.util.TimeZone; +import java.util.regex.Pattern; + +import ru.yandex.clickhouse.except.ClickHouseException; +import ru.yandex.clickhouse.except.ClickHouseUnknownException; +import ru.yandex.clickhouse.response.ByteFragment; +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +abstract class ClickHouseDateValueParser extends ClickHouseValueParser { + + private static final Pattern PATTERN_EMPTY_DATE = + Pattern.compile("^(0000-00-00|0000-00-00 00:00:00|0)$"); + + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy[-]MM[-]dd"); + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd['T'][ ]HH:mm:ss[.SSS]"); + private static final DateTimeFormatter TIME_FORMATTER_NUMBERS = + DateTimeFormatter.ofPattern("HH[mm][ss]") + .withResolverStyle(ResolverStyle.STRICT); + + private final Class clazz; + + protected ClickHouseDateValueParser(Class clazz) { + this.clazz = Objects.requireNonNull(clazz); + } + + @Override + public T parse(ByteFragment value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) throws ClickHouseException + { + + if (value.isNull()) { + return null; + } + + String s = value.asString(); + + /* + * filter default values for relevant data types, + * even if the column has nullable flag set. + */ + if (PATTERN_EMPTY_DATE.matcher(s).matches()) { + return null; + } + + switch (columnInfo.getEffectiveClickHouseDataType()) { + case Date: + try { + return parseDate(s, columnInfo, timeZone); + } catch (Exception e) { + throw new ClickHouseUnknownException( + "Error parsing '" + s + "' of data type '" + + columnInfo.getOriginalTypeName() + + "' as " + clazz.getName(), + e); + } + case DateTime: + try { + return parseDateTime(s, columnInfo, timeZone); + } catch (Exception e) { + throw new ClickHouseUnknownException( + "Error parsing '" + s + "' of data type '" + + columnInfo.getOriginalTypeName() + + "' as " + clazz.getName(), + e); + } + case Int8: + case Int16: + case Int32: + case Int64: + case UInt8: + case UInt16: + case UInt32: + try { + long l = Long.parseLong(s); + return parseNumber(l, columnInfo, timeZone); + } catch (Exception e) { + throw new ClickHouseUnknownException( + "Error parsing '" + s + "' of data type '" + + columnInfo.getOriginalTypeName() + + "' as " + clazz.getName(), + e); + } + case UInt64: + // If we have a large nanos value, we trim to millis + try { + BigInteger bi = new BigInteger(s); + if (bi.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) { + return parseNumber( + bi.divide(BigInteger.valueOf(1000_000L)).longValue(), + columnInfo, + timeZone); + } + return parseNumber(bi.longValue(), columnInfo, timeZone); + } catch (Exception e) { + throw new ClickHouseUnknownException( + "Error parsing '" + s + "' of data type '" + + columnInfo.getOriginalTypeName() + + "' as " + clazz.getName(), + e); + } + case String: + case Unknown: + try { + return parseOther(s, columnInfo, timeZone); + } catch (Exception e) { + throw new ClickHouseUnknownException( + "Error parsing '" + s + "' as " + clazz.getName(), e); + } + default: + throw new ClickHouseUnknownException( + "Error parsing '" + s + "' of data type '" + + columnInfo.getOriginalTypeName() + + "' as " + clazz.getName(), + null); + } + + } + + abstract T parseDate(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone); + + abstract T parseDateTime(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone); + + abstract T parseNumber(long value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone); + + abstract T parseOther(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone); + + protected final ZoneId effectiveTimeZone(ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return columnInfo.getTimeZone() != null + ? columnInfo.getTimeZone().toZoneId() + : timeZone != null + ? timeZone.toZoneId() + : ZoneId.systemDefault(); + } + + protected final LocalDate parseAsLocalDate(String value) { + return LocalDate.parse(value, DATE_FORMATTER); + } + + protected final LocalDateTime parseAsLocalDateTime(String value) { + return LocalDateTime.parse(value, DATE_TIME_FORMATTER); + } + + protected final OffsetDateTime parseAsOffsetDateTime(String value) { + return OffsetDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME); + } + + protected final Instant parseAsInstant(String value) { + try { + long l = Long.parseLong(value); + return parseAsInstant(l); + } catch (NumberFormatException nfe) { + throw new DateTimeParseException("unparsable as long", value, -1, nfe); + } + } + + protected final Instant parseAsInstant(long value) { + return value > Integer.MAX_VALUE + ? Instant.ofEpochMilli(value) + : Instant.ofEpochSecond(value); + } + + protected final LocalTime parseAsLocalTime(String value) { + return LocalTime.parse( + value.length() % 2 == 0 ? value : "0" + value, + TIME_FORMATTER_NUMBERS); + } + + protected final LocalTime parseAsLocalTime(long value) { + return parseAsLocalTime(String.valueOf(value)); + } + +} diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDoubleParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDoubleParser.java new file mode 100644 index 000000000..86c691b5c --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDoubleParser.java @@ -0,0 +1,58 @@ +package ru.yandex.clickhouse.response.parser; + +import java.sql.SQLException; +import java.util.TimeZone; + +import ru.yandex.clickhouse.except.ClickHouseUnknownException; +import ru.yandex.clickhouse.response.ByteFragment; +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +final class ClickHouseDoubleParser extends ClickHouseValueParser { + + private static ClickHouseDoubleParser instance; + + static ClickHouseDoubleParser getInstance() { + if (instance == null) { + instance = new ClickHouseDoubleParser(); + } + return instance; + } + + private ClickHouseDoubleParser() { + // prevent instantiation + } + + @Override + public Double parse(ByteFragment value, ClickHouseColumnInfo columnInfo, + TimeZone resultTimeZone) throws SQLException + { + if (value.isNull()) { + return null; + } + if (value.isNaN()) { + return Double.valueOf(Double.NaN); + } + String s = value.asString(); + switch (s) { + case "+inf": + case "inf": + return Double.valueOf(Double.POSITIVE_INFINITY); + case "-inf": + return Double.valueOf(Double.NEGATIVE_INFINITY); + default: + try { + return Double.valueOf(s); + } catch (NumberFormatException nfe) { + throw new ClickHouseUnknownException( + "Error parsing '" + s + "' as Double", + nfe); + } + } + } + + @Override + protected Double getDefaultValue() { + return Double.valueOf(0); + } + +} diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseInstantParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseInstantParser.java new file mode 100644 index 000000000..af539b1ff --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseInstantParser.java @@ -0,0 +1,77 @@ +package ru.yandex.clickhouse.response.parser; + +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.TimeZone; + +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +final class ClickHouseInstantParser extends ClickHouseDateValueParser { + + private static ClickHouseInstantParser instance; + + static ClickHouseInstantParser getInstance() { + if (instance == null) { + instance = new ClickHouseInstantParser(); + } + return instance; + } + + private ClickHouseInstantParser() { + super(Instant.class); + } + + @Override + Instant parseDate(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalDate(value) + .atStartOfDay(effectiveTimeZone(columnInfo, timeZone)) + .toInstant(); + } + + @Override + Instant parseDateTime(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalDateTime(value) + .atZone(effectiveTimeZone(columnInfo, timeZone)) + .toInstant(); + } + + @Override + Instant parseNumber(long value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return value > Integer.MAX_VALUE + ? Instant.ofEpochMilli(value) + : Instant.ofEpochSecond(value); + } + + @Override + Instant parseOther(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + try { + return parseAsLocalDate(value) + .atStartOfDay(effectiveTimeZone(columnInfo, timeZone)) + .toInstant(); + } catch (DateTimeParseException dtpe) { + // better luck next time + } + try { + return parseAsLocalDateTime(value) + .atZone(effectiveTimeZone(columnInfo, timeZone)) + .toInstant(); + } catch (DateTimeParseException dtpe) { + // better luck next time + } + try { + return parseAsOffsetDateTime(value) + .toInstant(); + } catch (DateTimeParseException dtpe) { + // better luck next time + } + return parseAsInstant(value); + } +} diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateParser.java new file mode 100644 index 000000000..7c13ec9e6 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateParser.java @@ -0,0 +1,64 @@ +package ru.yandex.clickhouse.response.parser; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.TimeZone; + +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +final class ClickHouseLocalDateParser extends ClickHouseDateValueParser { + + private static ClickHouseLocalDateParser instance; + + static ClickHouseLocalDateParser getInstance() { + if (instance == null) { + instance = new ClickHouseLocalDateParser(); + } + return instance; + } + + private ClickHouseLocalDateParser() { + super(LocalDate.class); + } + + @Override + LocalDate parseDate(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalDate(value); + } + + @Override + LocalDate parseDateTime(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalDateTime(value).toLocalDate(); + } + + @Override + LocalDate parseNumber(long value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsInstant(value).atZone(timeZone.toZoneId()).toLocalDate(); + } + + @Override + LocalDate parseOther(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + try { + return parseAsLocalDate(value); + } catch (DateTimeParseException dtpe) { + // not parseable as date + } + try { + return parseAsLocalDateTime(value).toLocalDate(); + } catch (DateTimeParseException dtpe) { + // not parseable as datetime + } + Instant i = parseAsInstant(value); + return i.atZone(timeZone.toZoneId()).toLocalDate(); + } + +} diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateTimeParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateTimeParser.java new file mode 100644 index 000000000..61bc7d636 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateTimeParser.java @@ -0,0 +1,66 @@ +package ru.yandex.clickhouse.response.parser; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.TimeZone; + +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +final class ClickHouseLocalDateTimeParser extends ClickHouseDateValueParser { + + private static ClickHouseLocalDateTimeParser instance; + + static ClickHouseLocalDateTimeParser getInstance() { + if (instance == null) { + instance = new ClickHouseLocalDateTimeParser(); + } + return instance; + } + + private ClickHouseLocalDateTimeParser() { + super(LocalDateTime.class); + } + + @Override + LocalDateTime parseDate(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalDate(value).atStartOfDay(); + } + + @Override + LocalDateTime parseDateTime(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalDateTime(value); + } + + @Override + LocalDateTime parseNumber(long value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsInstant(value) + .atZone(timeZone.toZoneId()) + .toLocalDateTime(); + } + + @Override + LocalDateTime parseOther(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + try { + return parseAsLocalDate(value).atStartOfDay(); + } catch (DateTimeParseException dtpe) { + // not parseable as date + } + try { + return parseAsLocalDateTime(value); + } catch (DateTimeParseException dtpe) { + // not parseable as datetime + } + Instant i = parseAsInstant(value); + return i.atZone(timeZone.toZoneId()).toLocalDateTime(); + } + +} diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalTimeParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalTimeParser.java new file mode 100644 index 000000000..bd2c243d8 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalTimeParser.java @@ -0,0 +1,63 @@ +package ru.yandex.clickhouse.response.parser; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.TimeZone; + +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +final class ClickHouseLocalTimeParser extends ClickHouseDateValueParser { + + private static ClickHouseLocalTimeParser instance; + + static ClickHouseLocalTimeParser getInstance() { + if (instance == null) { + instance = new ClickHouseLocalTimeParser(); + } + return instance; + } + + private ClickHouseLocalTimeParser() { + super(LocalTime.class); + } + + @Override + LocalTime parseDate(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return LocalTime.MIDNIGHT; + } + + @Override + LocalTime parseDateTime(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalDateTime(value).toLocalTime(); + } + + @Override + LocalTime parseNumber(long value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalTime(value); + } + + @Override + LocalTime parseOther(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + try { + return LocalTime.parse(value, DateTimeFormatter.ISO_LOCAL_TIME); + } catch (DateTimeParseException dtpe) { + // try different pattern + } + try { + return LocalTime.parse(value, DateTimeFormatter.ISO_OFFSET_TIME); + } catch (DateTimeParseException dtpe) { + // try different pattern + } + return parseAsLocalTime(value); + } + +} diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetDateTimeParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetDateTimeParser.java new file mode 100644 index 000000000..d6e58621c --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetDateTimeParser.java @@ -0,0 +1,73 @@ +package ru.yandex.clickhouse.response.parser; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.TimeZone; + +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +final class ClickHouseOffsetDateTimeParser extends ClickHouseDateValueParser { + + private static ClickHouseOffsetDateTimeParser instance; + + static ClickHouseOffsetDateTimeParser getInstance() { + if (instance == null) { + instance = new ClickHouseOffsetDateTimeParser(); + } + return instance; + } + + private ClickHouseOffsetDateTimeParser() { + super(OffsetDateTime.class); + } + + @Override + OffsetDateTime parseDate(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalDate(value) + .atStartOfDay(effectiveTimeZone(columnInfo, timeZone)) + .toOffsetDateTime(); + } + + @Override + OffsetDateTime parseDateTime(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsLocalDateTime(value) + .atZone(effectiveTimeZone(columnInfo, timeZone)) + .toOffsetDateTime(); + } + + @Override + OffsetDateTime parseNumber(long value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return parseAsInstant(value) + .atZone(effectiveTimeZone(columnInfo, timeZone)) + .toOffsetDateTime(); + } + + @Override + OffsetDateTime parseOther(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + try { + return parseAsLocalDateTime(value) + .atZone(effectiveTimeZone(columnInfo, timeZone)) + .toOffsetDateTime(); + } catch (DateTimeParseException dtpe) { + // try another way + } + try { + return OffsetDateTime.parse(value, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } catch (DateTimeParseException dtpe) { + // try another way + } + return parseAsInstant(value) + .atZone(effectiveTimeZone(columnInfo, timeZone)) + .toOffsetDateTime(); + } + +} diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetTimeParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetTimeParser.java new file mode 100644 index 000000000..374e08768 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetTimeParser.java @@ -0,0 +1,78 @@ +package ru.yandex.clickhouse.response.parser; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.TimeZone; + +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +final class ClickHouseOffsetTimeParser extends ClickHouseDateValueParser { + + private static ClickHouseOffsetTimeParser instance; + + static ClickHouseOffsetTimeParser getInstance() { + if (instance == null) { + instance = new ClickHouseOffsetTimeParser(); + } + return instance; + } + + private ClickHouseOffsetTimeParser() { + super(OffsetTime.class); + } + + @Override + OffsetTime parseDate(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return OffsetTime.of( + LocalTime.MIDNIGHT, + effectiveTimeZone(columnInfo, timeZone).getRules().getOffset( + parseAsLocalDate(value).atStartOfDay())); + } + + @Override + OffsetTime parseDateTime(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + LocalDateTime ldt = parseAsLocalDateTime(value); + return OffsetTime.of( + ldt.toLocalTime(), + effectiveTimeZone(columnInfo, timeZone).getRules().getOffset(ldt)); + } + + @Override + OffsetTime parseNumber(long value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return OffsetTime.of( + parseAsLocalTime(value), + effectiveTimeZone(columnInfo, timeZone).getRules().getOffset(Instant.now())); + } + + @Override + OffsetTime parseOther(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + try { + return OffsetTime.parse(value, DateTimeFormatter.ISO_OFFSET_TIME); + } catch (DateTimeParseException dtpe) { + // try next pattern + } + try { + return OffsetTime.of( + LocalTime.parse(value, DateTimeFormatter.ISO_LOCAL_TIME), + effectiveTimeZone(columnInfo, timeZone).getRules().getOffset(Instant.now())); + } catch (DateTimeParseException dtpe) { + // try next pattern + } + return OffsetTime.of( + parseAsLocalTime(value), + effectiveTimeZone(columnInfo, timeZone).getRules().getOffset(Instant.now())); + } + +} diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLDateParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLDateParser.java new file mode 100644 index 000000000..314d4019a --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLDateParser.java @@ -0,0 +1,122 @@ +package ru.yandex.clickhouse.response.parser; + +import java.sql.Date; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.TimeZone; + +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +final class ClickHouseSQLDateParser extends ClickHouseDateValueParser { + + private static ClickHouseSQLDateParser instance; + + static ClickHouseSQLDateParser getInstance() { + if (instance == null) { + instance = new ClickHouseSQLDateParser(); + } + return instance; + } + + private ClickHouseSQLDateParser() { + super(Date.class); + } + + @Override + Date parseDate(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return new Date(parseAsLocalDate(value) + .atStartOfDay(getParsingTimeZone(columnInfo, timeZone)) + .toInstant() + .toEpochMilli()); + } + + @Override + Date parseDateTime(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return new Date(parseAsLocalDateTime(value) + .atZone(getParsingTimeZone(columnInfo, timeZone)) + .withZoneSameInstant(getResultTimeZone(timeZone)) + .truncatedTo(ChronoUnit.DAYS) + .toInstant() + .toEpochMilli()); + } + + @Override + Date parseNumber(long value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return new Date(parseAsInstant(value) + .atZone(getResultTimeZone(timeZone)) + .truncatedTo(ChronoUnit.DAYS) + .toInstant() + .toEpochMilli()); + } + + @Override + Date parseOther(String value, ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + try { + return new Date(parseAsInstant(value) + .atZone(getResultTimeZone(timeZone)) + .truncatedTo(ChronoUnit.DAYS) + .toInstant() + .toEpochMilli()); + } catch (DateTimeParseException dtpe) { + // try next candidate + } + + try { + return new Date(parseAsOffsetDateTime(value) + .toInstant() + .atZone(getResultTimeZone(timeZone)) + .truncatedTo(ChronoUnit.DAYS) + .toInstant() + .toEpochMilli()); + } catch (DateTimeParseException dtpe) { + // try next candidate + } + + try { + return new Date(parseAsLocalDateTime(value) + .atZone(getParsingTimeZone(columnInfo, timeZone)) + .withZoneSameInstant(getResultTimeZone(timeZone)) + .truncatedTo(ChronoUnit.DAYS) + .toInstant() + .toEpochMilli()); + } catch (DateTimeParseException dtpe) { + // try next candidate + } + + return new Date(LocalDateTime + .of( + parseAsLocalDate(value), + LocalTime.MIDNIGHT) + .atZone(getResultTimeZone(timeZone)) + .toInstant() + .toEpochMilli()); + } + + private static ZoneId getParsingTimeZone(ClickHouseColumnInfo columnInfo, + TimeZone timeZone) + { + return columnInfo.getTimeZone() != null + ? columnInfo.getTimeZone().toZoneId() + : timeZone != null + ? timeZone.toZoneId() + : ZoneId.systemDefault(); + } + + private static ZoneId getResultTimeZone(TimeZone timeZone) { + return timeZone != null + ? timeZone.toZoneId() + : ZoneId.systemDefault(); + } + +} diff --git a/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLTimeParser.java b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLTimeParser.java new file mode 100644 index 000000000..88e4b87bc --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLTimeParser.java @@ -0,0 +1,93 @@ +package ru.yandex.clickhouse.response.parser; + +import java.sql.Time; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.TimeZone; + +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +final class ClickHouseSQLTimeParser extends ClickHouseDateValueParser