diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f362b2ec8..dff870629 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,9 +32,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [8, 11] + java: [8, 11, 15] # most recent LTS releases as well as latest stable builds - clickhouse: ["19.14", "20.3", "20.8", "20.10", "20.12", "21.2", "latest"] + clickhouse: ["20.8", "20.10", "20.12", "21.2", "latest"] name: Build using JDK ${{ matrix.java }} against ClickHouse ${{ matrix.clickhouse }} steps: - name: Check out Git repository @@ -56,25 +56,6 @@ jobs: key: ${{ runner.os }}-build-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-build- - - name: Generate build properties - uses: actions/github-script@v3 - id: props - env: - CURRENT_VERSION: ${{ steps.version.outputs.value }} - with: - script: | - const timezones = [ - 'Asia/Chongqing', 'America/Los_Angeles', 'Etc/UTC', 'Europe/Berlin', 'Europe/Moscow' - ]; - // surprise me - return { - clickhouse: timezones[Math.floor(Math.random() * Math.floor(timezones.length))] || '', - java: timezones[Math.floor(Math.random() * Math.floor(timezones.length))] || '' - }; - name: Build with Maven run: | - find . -type f -name "pom.xml" -exec sed -i -e 's|.*argLine.*timezone=.*||g' '{}' \; - mvn --batch-mode --update-snapshots \ - -DclickhouseVersion=${{ matrix.clickhouse }} \ - -DclickhouseTimezone=${{ fromJSON(steps.props.outputs.result).clickhouse }} \ - -Duser.timezone=${{ fromJSON(steps.props.outputs.result).java }} verify + mvn --batch-mode --update-snapshots -DclickhouseVersion=${{ matrix.clickhouse }} verify diff --git a/.github/workflows/timezone.yml b/.github/workflows/timezone.yml new file mode 100644 index 000000000..c7ceb2f97 --- /dev/null +++ b/.github/workflows/timezone.yml @@ -0,0 +1,62 @@ +name: TimeZone Test + +on: + push: + branches: + - master + - develop + paths-ignore: + - "**.md" + - "docs/**" + - "**/CHANGELOG" + + pull_request: + types: + - opened + - synchronize + - reopened + paths-ignore: + - "**.md" + - "docs/**" + - "**/CHANGELOG" + + workflow_dispatch: + inputs: + pr: + description: "Pull request#" + required: false + +jobs: + timezone: + runs-on: ubuntu-latest + strategy: + matrix: + serverTz: ["Asia/Chongqing", "America/Los_Angeles", "Etc/UTC", "Europe/Berlin", "Europe/Moscow"] + clientTz: ["Asia/Chongqing", "America/Los_Angeles", "Etc/UTC", "Europe/Berlin", "Europe/Moscow"] + fail-fast: false + name: "Test TimeZones - Server: ${{ matrix.serverTz }}, Client: ${{ matrix.clientTz }}" + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + - name: Check out PR + run: | + git fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 \ + origin pull/${{ github.event.inputs.pr }}/merge:merged-pr && git checkout merged-pr + if: github.event.inputs.pr != '' + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache maven dependencies + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-build-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-build- + - name: Test using Maven + run: | + find . -type f -name "pom.xml" -exec sed -i -e 's|.*argLine.*timezone=.*||g' '{}' \; + mvn --batch-mode --update-snapshots \ + -DclickhouseTimezone=${{ matrix.serverTz }} \ + -Duser.timezone=${{ matrix.clientTz }} verify diff --git a/README.md b/README.md index 883e62af3..7bc8c2536 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ClickHouse JDBC driver =============== -[![clickhouse-jdbc](https://maven-badges.herokuapp.com/maven-central/ru.yandex.clickhouse/clickhouse-jdbc/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ru.yandex.clickhouse/clickhouse-jdbc) ![Build Status(https://github.com/ClickHouse/clickhouse-jdbc/workflows/Build/badge.svg)](https://github.com/ClickHouse/clickhouse-jdbc/workflows/Build/badge.svg) +[![clickhouse-jdbc](https://maven-badges.herokuapp.com/maven-central/ru.yandex.clickhouse/clickhouse-jdbc/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ru.yandex.clickhouse/clickhouse-jdbc) ![Build Status(https://github.com/ClickHouse/clickhouse-jdbc/workflows/Build/badge.svg)](https://github.com/ClickHouse/clickhouse-jdbc/workflows/Build/badge.svg) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ClickHouse_clickhouse-jdbc&metric=coverage)](https://sonarcloud.io/dashboard?id=ClickHouse_clickhouse-jdbc) This is a basic and restricted implementation of jdbc driver for ClickHouse. It has support of a minimal subset of features to be usable. diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseConnection.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseConnection.java index 7a3302421..7454d1a09 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseConnection.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseConnection.java @@ -10,6 +10,8 @@ public interface ClickHouseConnection extends Connection { @Deprecated ClickHouseStatement createClickHouseStatement() throws SQLException; + TimeZone getServerTimeZone(); + TimeZone getTimeZone(); @Override diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseConnectionImpl.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseConnectionImpl.java index 59dcbd54f..4c557511a 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseConnectionImpl.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseConnectionImpl.java @@ -53,8 +53,9 @@ public class ClickHouseConnectionImpl implements ClickHouseConnection { private boolean closed = false; + private TimeZone serverTimeZone; private TimeZone timezone; - private volatile String serverVersion; + private String serverVersion; public ClickHouseConnectionImpl(String url) throws SQLException { this(url, new ClickHouseProperties()); @@ -74,25 +75,35 @@ public ClickHouseConnectionImpl(String url, ClickHouseProperties properties) thr }catch (Exception e) { throw new IllegalStateException("cannot initialize http client", e); } - initTimeZone(this.properties); + initConnection(this.properties); } - private void initTimeZone(ClickHouseProperties properties) throws SQLException { + private void initConnection(ClickHouseProperties properties) throws SQLException { + // timezone if (properties.isUseServerTimeZone() && !Utils.isNullOrEmptyString(properties.getUseTimeZone())) { throw new IllegalArgumentException(String.format("only one of %s or %s must be enabled", ClickHouseConnectionSettings.USE_SERVER_TIME_ZONE.getKey(), ClickHouseConnectionSettings.USE_TIME_ZONE.getKey())); } if (!properties.isUseServerTimeZone() && Utils.isNullOrEmptyString(properties.getUseTimeZone())) { throw new IllegalArgumentException(String.format("one of %s or %s must be enabled", ClickHouseConnectionSettings.USE_SERVER_TIME_ZONE.getKey(), ClickHouseConnectionSettings.USE_TIME_ZONE.getKey())); } - if (properties.isUseServerTimeZone()) { - timezone = TimeZone.getTimeZone("UTC"); // just for next query - try (ResultSet rs = createStatement().executeQuery("select timezone()")) { - if (rs.next()) { - timezone = TimeZone.getTimeZone(rs.getString(1)); - } + + serverTimeZone = TimeZone.getTimeZone("UTC"); // just for next query + try (Statement s = createStatement(); ResultSet rs = s.executeQuery("select timezone(), version()")) { + if (rs.next()) { + serverTimeZone = TimeZone.getTimeZone(rs.getString(1)); + serverVersion = rs.getString(2); } - } else if (!Utils.isNullOrEmptyString(properties.getUseTimeZone())) { - timezone = TimeZone.getTimeZone(properties.getUseTimeZone()); + } + + timezone = serverTimeZone; + if (!properties.isUseServerTimeZone()) { + timezone = Utils.isNullOrEmptyString(properties.getUseTimeZone()) + ? TimeZone.getDefault() + : TimeZone.getTimeZone(properties.getUseTimeZone()); + } + + if (serverVersion == null) { + serverVersion = ""; } } @@ -122,6 +133,11 @@ public TimeZone getTimeZone() { return timezone; } + @Override + public TimeZone getServerTimeZone() { + return serverTimeZone; + } + private ClickHouseStatement createClickHouseStatement(CloseableHttpClient httpClient) throws SQLException { return LogProxy.wrap( ClickHouseStatement.class, @@ -169,12 +185,6 @@ public ClickHouseStatement createStatement(int resultSetType, int resultSetConcu */ @Override public String getServerVersion() throws SQLException { - if (serverVersion == null) { - ResultSet rs = createStatement().executeQuery("select version()"); - rs.next(); - serverVersion = rs.getString(1); - rs.close(); - } return serverVersion; } diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseDatabaseMetadata.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseDatabaseMetadata.java index a89569338..37e0f7795 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseDatabaseMetadata.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/ClickHouseDatabaseMetadata.java @@ -837,7 +837,8 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa //column name ClickHouseColumnInfo columnInfo = ClickHouseColumnInfo.parse( descTable.getString("type"), - descTable.getString("name")); + descTable.getString("name"), + connection.getServerTimeZone()); row.add(columnInfo.getColumnName()); //data type row.add(String.valueOf(columnInfo.getClickHouseDataType().getSqlType())); diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/domain/ClickHouseDataType.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/domain/ClickHouseDataType.java index bb9292e9f..fe0fb9b0b 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/domain/ClickHouseDataType.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/domain/ClickHouseDataType.java @@ -6,6 +6,10 @@ import java.sql.Date; import java.sql.JDBCType; import java.sql.Timestamp; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.UUID; /** @@ -19,7 +23,14 @@ * modifiers for the underlying base data types. */ public enum ClickHouseDataType { - + // aliases: + // https://clickhouse.tech/docs/en/sql-reference/data-types/multiword-types/ + // https://github.com/ClickHouse/ClickHouse/blob/master/src/DataTypes/DataTypeCustomIPv4AndIPv6.cpp + // https://github.com/ClickHouse/ClickHouse/blob/master/src/DataTypes/registerDataTypeDateTime.cpp + // https://github.com/ClickHouse/ClickHouse/blob/master/src/DataTypes/DataTypesDecimal.cpp + // https://github.com/ClickHouse/ClickHouse/blob/master/src/DataTypes/DataTypeFixedString.cpp + // https://github.com/ClickHouse/ClickHouse/blob/master/src/DataTypes/DataTypesNumber.cpp + // https://github.com/ClickHouse/ClickHouse/blob/master/src/DataTypes/DataTypeString.cpp IntervalYear (JDBCType.INTEGER, Integer.class, true, 19, 0), IntervalQuarter (JDBCType.INTEGER, Integer.class, true, 19, 0), IntervalMonth (JDBCType.INTEGER, Integer.class, true, 19, 0), @@ -28,54 +39,83 @@ public enum ClickHouseDataType { 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), + UInt256 (JDBCType.NUMERIC, BigInteger.class, true, 39, 0), + UInt128 (JDBCType.NUMERIC, BigInteger.class, true, 20, 0), + UInt64 (JDBCType.BIGINT, BigInteger.class, false, 19, 0, + "BIGINT UNSIGNED"), + UInt32 (JDBCType.BIGINT, Long.class, false, 10, 0, + "INT UNSIGNED", "INTEGER UNSIGNED", "MEDIUMINT UNSIGNED"), + UInt16 (JDBCType.SMALLINT, Integer.class, false, 5, 0, + "SMALLINT UNSIGNED"), + UInt8 (JDBCType.TINYINT, Integer.class, false, 3, 0, + "TINYINT UNSIGNED", "INT1 UNSIGNED"), + Int256 (JDBCType.NUMERIC, BigInteger.class, true, 40, 0), + Int128 (JDBCType.NUMERIC, BigInteger.class, true, 20, 0), Int64 (JDBCType.BIGINT, Long.class, true, 20, 0, - "BIGINT"), + "BIGINT", "BIGINT SIGNED"), Int32 (JDBCType.INTEGER, Integer.class, true, 11, 0, - "INTEGER", - "INT"), + "INT", "INTEGER", "MEDIUMINT", "INT SIGNED", "INTEGER SIGNED", "MEDIUMINT SIGNED"), Int16 (JDBCType.SMALLINT, Integer.class, true, 6, 0, - "SMALLINT"), + "SMALLINT", "SMALLINT SIGNED"), Int8 (JDBCType.TINYINT, Integer.class, true, 4, 0, - "TINYINT"), + "TINYINT", "BOOL", "BOOLEAN", "INT1", "BYTE", "TINYINT SIGNED", "INT1 SIGNED"), Date (JDBCType.DATE, Date.class, false, 10, 0), DateTime (JDBCType.TIMESTAMP, Timestamp.class, false, 19, 0, "TIMESTAMP"), - Enum8 (JDBCType.VARCHAR, String.class, false, 0, 0), + DateTime32 (JDBCType.TIMESTAMP, Timestamp.class, false, 19, 0), + DateTime64 (JDBCType.TIMESTAMP, Timestamp.class, false, 38, 3), // scale up to 18 + Enum8 (JDBCType.VARCHAR, String.class, false, 0, 0, + "ENUM"), Enum16 (JDBCType.VARCHAR, String.class, false, 0, 0), Float32 (JDBCType.REAL, Float.class, true, 8, 8, - "REAL"), + "SINGLE", "REAL"), Float64 (JDBCType.DOUBLE, Double.class, true, 17, 17, - "DOUBLE"), + "DOUBLE", "DOUBLE PRECISION"), Decimal32 (JDBCType.DECIMAL, BigDecimal.class, true, 9, 9), Decimal64 (JDBCType.DECIMAL, BigDecimal.class, true, 18, 18), Decimal128 (JDBCType.DECIMAL, BigDecimal.class, true, 38, 38), + Decimal256 (JDBCType.DECIMAL, BigDecimal.class, true, 76, 20), Decimal (JDBCType.DECIMAL, BigDecimal.class, true, 0, 0, - "DEC"), + "DEC", "NUMERIC", "FIXED"), UUID (JDBCType.OTHER, UUID.class, false, 36, 0), + IPv4 (JDBCType.VARCHAR, String.class, false, 10, 0), + IPv6 (JDBCType.VARCHAR, String.class, false, 0, 0), String (JDBCType.VARCHAR, String.class, false, 0, 0, - "LONGBLOB", - "MEDIUMBLOB", - "TINYBLOB", - "MEDIUMTEXT", - "CHAR", - "VARCHAR", - "TEXT", - "TINYTEXT", - "LONGTEXT", - "BLOB"), + "CHAR", "NCHAR", "CHARACTER", "VARCHAR", "NVARCHAR", "VARCHAR2", + "TEXT", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT", + "BLOB", "CLOB", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB", "BYTEA", + "CHARACTER LARGE OBJECT", "CHARACTER VARYING", "CHAR LARGE OBJECT", "CHAR VARYING", + "NATIONAL CHAR", "NATIONAL CHARACTER", "NATIONAL CHARACTER LARGE OBJECT", + "NATIONAL CHARACTER VARYING", "NATIONAL CHAR VARYING", + "NCHAR VARYING", "NCHAR LARGE OBJECT", "BINARY LARGE OBJECT", "BINARY VARYING"), FixedString (JDBCType.CHAR, String.class, false, -1, 0, "BINARY"), Nothing (JDBCType.NULL, Object.class, false, 0, 0), Nested (JDBCType.STRUCT, String.class, false, 0, 0), + // TODO use list/collection for Tuple Tuple (JDBCType.OTHER, String.class, false, 0, 0), Array (JDBCType.ARRAY, Array.class, false, 0, 0), + Map (JDBCType.OTHER, Map.class, false, 0, 0), AggregateFunction (JDBCType.OTHER, String.class, false, 0, 0), Unknown (JDBCType.OTHER, String.class, false, 0, 0); + private static final Map name2type; + + static { + Map map = new HashMap<>(); + for (ClickHouseDataType t : ClickHouseDataType.values()) { + assert map.put(t.name(), t) == null; + String nameInUpperCase = t.name().toUpperCase(); + if (!nameInUpperCase.equals(t.name())) { + assert map.put(nameInUpperCase, t) == null; + } + for (String alias: t.aliases) { + assert map.put(alias.toUpperCase(), t) == null; + } + } + name2type = Collections.unmodifiableMap(map); + } + private final JDBCType jdbcType; private final Class javaClass; private final boolean signed; @@ -85,8 +125,7 @@ public enum ClickHouseDataType { ClickHouseDataType(JDBCType jdbcType, Class javaClass, boolean signed, int defaultPrecision, int defaultScale, - String... aliases) - { + String... aliases) { this.jdbcType = jdbcType; this.javaClass = javaClass; this.signed = signed; @@ -120,27 +159,10 @@ public int getDefaultScale() { } public static ClickHouseDataType fromTypeString(String typeString) { - String s = typeString.trim(); - for (ClickHouseDataType dataType : values()) { - if (s.equalsIgnoreCase(dataType.name())) { - return dataType; - } - for (String alias : dataType.aliases) { - if (s.equalsIgnoreCase(alias)) { - return dataType; - } - } - } - return ClickHouseDataType.Unknown; + return name2type.getOrDefault(typeString.trim().toUpperCase(), ClickHouseDataType.Unknown); } public static ClickHouseDataType resolveDefaultArrayDataType(String typeName) { - for (ClickHouseDataType chDataType : values()) { - if (chDataType.name().equals(typeName)) { - return chDataType; - } - } - return ClickHouseDataType.String; + return name2type.getOrDefault(typeName, ClickHouseDataType.String); } - } diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/ClickHouseColumnInfo.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/ClickHouseColumnInfo.java index b08305c9b..95627cc97 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/ClickHouseColumnInfo.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/ClickHouseColumnInfo.java @@ -20,8 +20,15 @@ public final class ClickHouseColumnInfo { private TimeZone timeZone; private int precision; private int scale; + private ClickHouseColumnInfo keyInfo; + private ClickHouseColumnInfo valueInfo; + @Deprecated public static ClickHouseColumnInfo parse(String typeInfo, String columnName) { + return parse(typeInfo, columnName, null); + } + + public static ClickHouseColumnInfo parse(String typeInfo, String columnName, TimeZone serverTimeZone) { ClickHouseColumnInfo column = new ClickHouseColumnInfo(typeInfo, columnName); int currIdx = 0; while (typeInfo.startsWith(KEYWORD_ARRAY, currIdx)) { @@ -52,6 +59,7 @@ public static ClickHouseColumnInfo parse(String typeInfo, String columnName) { } column.precision = dataType.getDefaultPrecision(); column.scale = dataType.getDefaultScale(); + column.timeZone = serverTimeZone; currIdx = endIdx; if (endIdx == typeInfo.length() || !typeInfo.startsWith("(", currIdx)) @@ -61,14 +69,33 @@ public static ClickHouseColumnInfo parse(String typeInfo, String columnName) { switch (dataType) { case DateTime : - String[] argsTZ = splitArgs(typeInfo, currIdx); - if (argsTZ.length == 1) { + String[] argsDT = splitArgs(typeInfo, currIdx); + if (argsDT.length == 2) { // same as DateTime64 + column.scale = Integer.parseInt(argsDT[0]); + column.timeZone = TimeZone.getTimeZone(argsDT[1].replace("'", "")); + } else if (argsDT.length == 1) { // same as DateTime32 + // unfortunately this will fall back to GMT if the time zone + // cannot be resolved + TimeZone tz = TimeZone.getTimeZone(argsDT[0].replace("'", "")); + column.timeZone = tz; + } + break; + case DateTime32: + String[] argsD32 = splitArgs(typeInfo, currIdx); + if (argsD32.length == 1) { // unfortunately this will fall back to GMT if the time zone // cannot be resolved - TimeZone tz = TimeZone.getTimeZone(argsTZ[0].replace("'", "")); + TimeZone tz = TimeZone.getTimeZone(argsD32[0].replace("'", "")); column.timeZone = tz; } break; + case DateTime64: + String[] argsD64 = splitArgs(typeInfo, currIdx); + if (argsD64.length == 2) { + column.scale = Integer.parseInt(argsD64[0]); + column.timeZone = TimeZone.getTimeZone(argsD64[1].replace("'", "")); + } + break; case Decimal : String[] argsDecimal = splitArgs(typeInfo, currIdx); if (argsDecimal.length == 2) { @@ -79,6 +106,7 @@ public static ClickHouseColumnInfo parse(String typeInfo, String columnName) { case Decimal32 : case Decimal64 : case Decimal128 : + case Decimal256 : String[] argsScale = splitArgs(typeInfo, currIdx); column.scale = Integer.parseInt(argsScale[0]); break; @@ -86,6 +114,13 @@ public static ClickHouseColumnInfo parse(String typeInfo, String columnName) { String[] argsPrecision = splitArgs(typeInfo, currIdx); column.precision = Integer.parseInt(argsPrecision[0]); break; + case Map: + String[] argsMap = splitArgs(typeInfo, currIdx); + if (argsMap.length == 2) { + column.keyInfo = ClickHouseColumnInfo.parse(argsMap[0], columnName + "Key", serverTimeZone); + column.valueInfo = ClickHouseColumnInfo.parse(argsMap[1], columnName + "Value", serverTimeZone); + } + break; default : break; } @@ -184,4 +219,11 @@ public int getScale() { return scale; } + public ClickHouseColumnInfo getKeyInfo() { + return this.keyInfo; + } + + public ClickHouseColumnInfo getValueInfo() { + return this.valueInfo; + } } diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java index a9e57855c..e04d45c65 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java @@ -20,9 +20,9 @@ import java.util.Collections; import java.util.List; import java.util.TimeZone; -import java.util.UUID; import ru.yandex.clickhouse.ClickHouseArray; +import ru.yandex.clickhouse.ClickHouseConnection; import ru.yandex.clickhouse.ClickHouseStatement; import ru.yandex.clickhouse.domain.ClickHouseDataType; import ru.yandex.clickhouse.except.ClickHouseExceptionSpecifier; @@ -30,6 +30,8 @@ import ru.yandex.clickhouse.response.parser.ClickHouseValueParser; import ru.yandex.clickhouse.settings.ClickHouseProperties; import ru.yandex.clickhouse.util.ClickHouseArrayUtil; +import ru.yandex.clickhouse.util.ClickHouseValueFormatter; +import ru.yandex.clickhouse.util.Utils; public class ClickHouseResultSet extends AbstractResultSet { @@ -88,7 +90,7 @@ public ClickHouseResultSet(InputStream is, int bufferSize, String db, String tab this.dateTimeTimeZone = timeZone; this.dateTimeZone = properties.isUseServerTimeZoneForDates() ? timeZone - : TimeZone.getDefault(); + : TimeZone.getDefault(); // FIXME should be the timezone defined in useTimeZone? bis = new StreamSplitter(is, (byte) 0x0A, bufferSize); /// \n ByteFragment headerFragment = bis.next(); if (headerFragment == null) { @@ -106,8 +108,21 @@ public ClickHouseResultSet(InputStream is, int bufferSize, String db, String tab } String[] types = toStringArray(typesFragment); columns = new ArrayList<>(cols.length); + TimeZone tz = null; + try { + if (statement != null && statement.getConnection() instanceof ClickHouseConnection) { + tz = ((ClickHouseConnection)statement.getConnection()).getServerTimeZone(); + } + } catch (SQLException e) { + // ignore the error + } + + if (tz == null) { + tz = timeZone; + } + for (int i = 0; i < cols.length; i++) { - columns.add(ClickHouseColumnInfo.parse(types[i], cols[i])); + columns.add(ClickHouseColumnInfo.parse(types[i], cols[i], tz)); } } @@ -288,6 +303,27 @@ public Timestamp getTimestamp(String column) throws SQLException { return getTimestamp(findColumn(column)); } + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + ClickHouseColumnInfo columnInfo = getColumnInfo(columnIndex); + TimeZone tz = getEffectiveTimeZone(columnInfo); + + return ClickHouseValueParser.getParser(Timestamp.class).parse( + getValue(columnIndex), columnInfo, tz); + } + + private TimeZone getEffectiveTimeZone(ClickHouseColumnInfo columnInfo) { + TimeZone tz = null; + + if (columnInfo.getClickHouseDataType() == ClickHouseDataType.Date) { + tz = dateTimeZone; + } else { + tz = properties.isUseServerTimeZone() ? null : dateTimeTimeZone; + } + + return tz; + } + @Override public Timestamp getTimestamp(String column, Calendar cal) throws SQLException { return getTimestamp(findColumn(column), cal); @@ -377,8 +413,34 @@ public Object getObject(String columnLabel) throws SQLException { @Override public String getString(int colNum) throws SQLException { - return ClickHouseValueParser.getParser(String.class) - .parse(getValue(colNum), getColumnInfo(colNum), null); + // FIXME this won't help when datetime string is in a nested structure + ClickHouseColumnInfo columnInfo = getColumnInfo(colNum); + ByteFragment value = getValue(colNum); + ClickHouseDataType dataType = columnInfo.getClickHouseDataType(); + + // Date is time-zone netural so let's skip that. + // DateTime string returned from Server however is always formatted using server/column + // timezone. The behaviour may change when + // https://github.com/ClickHouse/ClickHouse/issues/4548 is addressed + if (!properties.isUseServerTimeZone() && ( + dataType == ClickHouseDataType.DateTime + || dataType == ClickHouseDataType.DateTime32 + || dataType == ClickHouseDataType.DateTime64)) { + TimeZone serverTimeZone = columnInfo.getTimeZone(); + if (serverTimeZone == null) { + serverTimeZone = ((ClickHouseConnection)getStatement().getConnection()).getServerTimeZone(); + } + TimeZone clientTimeZone = Utils.isNullOrEmptyString(properties.getUseTimeZone()) + ? TimeZone.getDefault() + : TimeZone.getTimeZone(properties.getUseTimeZone()); + + if (!clientTimeZone.equals(serverTimeZone)) { + Timestamp newTs = ClickHouseValueParser.getParser(Timestamp.class).parse(value, columnInfo, serverTimeZone); + value = ByteFragment.fromString(ClickHouseValueFormatter.formatTimestamp(newTs, clientTimeZone)); + } + } + + return ClickHouseValueParser.getParser(String.class).parse(value, columnInfo, null); } @Override @@ -417,11 +479,9 @@ public byte[] getBytes(int colNum) { */ @Deprecated public Long getTimestampAsLong(int colNum) { - ClickHouseColumnInfo info = getColumnInfo(colNum); - TimeZone timeZone = info.getTimeZone() != null - ? info.getTimeZone() - : dateTimeTimeZone; - return getTimestampAsLong(colNum, timeZone); + ClickHouseColumnInfo columnInfo = getColumnInfo(colNum); + TimeZone tz = getEffectiveTimeZone(columnInfo); + return getTimestampAsLong(colNum, tz); } /** @@ -450,16 +510,6 @@ public Long getTimestampAsLong(int colNum, TimeZone timeZone) { } } - @Override - public Timestamp getTimestamp(int columnIndex) throws SQLException { - 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) throws SQLException { return ClickHouseValueParser.parseShort( @@ -506,10 +556,10 @@ public Statement getStatement() { @Override public Date getDate(int columnIndex) throws SQLException { + ClickHouseColumnInfo columnInfo = getColumnInfo(columnIndex); + TimeZone tz = getEffectiveTimeZone(columnInfo); return ClickHouseValueParser.getParser(Date.class).parse( - getValue(columnIndex), - getColumnInfo(columnIndex), - dateTimeZone); + getValue(columnIndex), columnInfo, tz); } @Override @@ -519,10 +569,10 @@ public Date getDate(int columnIndex, Calendar calendar) throws SQLException { @Override public Time getTime(int columnIndex) throws SQLException { + ClickHouseColumnInfo columnInfo = getColumnInfo(columnIndex); + TimeZone tz = getEffectiveTimeZone(columnInfo); return ClickHouseValueParser.getParser(Time.class).parse( - getValue(columnIndex), - getColumnInfo(columnIndex), - dateTimeTimeZone); + getValue(columnIndex), columnInfo, tz); } @Override @@ -537,8 +587,7 @@ public Object getObject(int columnIndex) throws SQLException { return null; } ClickHouseDataType chType = getColumnInfo(columnIndex).getClickHouseDataType(); - int type = chType.getSqlType(); - switch (type) { + switch (chType.getSqlType()) { case Types.BIGINT: if (chType == ClickHouseDataType.UInt64) { return getObject(columnIndex, BigInteger.class); @@ -561,12 +610,16 @@ public Object getObject(int columnIndex) throws SQLException { case Types.BLOB: return getString(columnIndex); case Types.ARRAY: return getArray(columnIndex); case Types.DECIMAL: return getBigDecimal(columnIndex); + case Types.NUMERIC: return getBigInteger(columnIndex); default: // do not return } switch (chType) { + // case Array: + // case Tuple: + case Map: case UUID : - return getObject(columnIndex, UUID.class); + return getObject(columnIndex, chType.getJavaClass()); default : return getString(columnIndex); } @@ -667,14 +720,15 @@ private ClickHouseColumnInfo getColumnInfo(int colNum) { @SuppressWarnings("unchecked") @Override public T getObject(int columnIndex, Class type) throws SQLException { - TimeZone tz = Date.class.equals(type) - ? dateTimeZone - : dateTimeTimeZone; + if (String.class.equals(type)) { + return (T) getString(columnIndex); + } + ClickHouseColumnInfo columnInfo = getColumnInfo(columnIndex); + TimeZone tz = getEffectiveTimeZone(columnInfo); return columnInfo.isArray() ? (T) getArray(columnIndex) - : ClickHouseValueParser.getParser(type) - .parse(getValue(columnIndex), getColumnInfo(columnIndex), tz); + : ClickHouseValueParser.getParser(type).parse(getValue(columnIndex), columnInfo, tz); } @Override @@ -718,6 +772,15 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException : null; } + public BigInteger getBigInteger(String columnLabel) throws SQLException { + return getBigInteger(findColumn(columnLabel)); + } + + public BigInteger getBigInteger(int columnIndex) throws SQLException { + BigDecimal dec = getBigDecimal(columnIndex); + return dec == null ? null : dec.toBigInteger(); + } + public String[] getColumnNames() { String[] columnNames = new String[columns.size()]; for (int i = 0; i < columns.size(); ++i) { diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseArrayParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseArrayParser.java new file mode 100644 index 000000000..20b3afed4 --- /dev/null +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseArrayParser.java @@ -0,0 +1,59 @@ +package ru.yandex.clickhouse.response.parser; + +import java.sql.Array; +import java.sql.SQLException; +import java.util.TimeZone; + +import ru.yandex.clickhouse.ClickHouseArray; +import ru.yandex.clickhouse.domain.ClickHouseDataType; +import ru.yandex.clickhouse.response.ByteFragment; +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; +import ru.yandex.clickhouse.util.ClickHouseArrayUtil; + +final class ClickHouseArrayParser extends ClickHouseValueParser { + + private static ClickHouseArrayParser instance; + + static ClickHouseArrayParser getInstance() { + if (instance == null) { + instance = new ClickHouseArrayParser(); + } + return instance; + } + + private ClickHouseArrayParser() { + // prevent instantiation + } + + @Override + public Array parse(ByteFragment value, ClickHouseColumnInfo columnInfo, TimeZone resultTimeZone) + throws SQLException { + if (columnInfo.getClickHouseDataType() != ClickHouseDataType.Array) { + throw new SQLException("Column not an array"); + } + + if (value.isNull()) { + return null; + } + + final Object array; + switch (columnInfo.getArrayBaseType()) { + case Date: + // FIXME: properties.isUseObjectsInArrays() + array = ClickHouseArrayUtil.parseArray(value, false, resultTimeZone, columnInfo); + break; + default: + // properties.isUseObjectsInArrays() + TimeZone timeZone = columnInfo.getTimeZone() != null ? columnInfo.getTimeZone() : resultTimeZone; + array = ClickHouseArrayUtil.parseArray(value, false, timeZone, columnInfo); + break; + } + + return new ClickHouseArray(columnInfo.getArrayBaseType(), array); + } + + @Override + protected Array getDefaultValue() { + return null; + } +} diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDateValueParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDateValueParser.java index 49a741636..527c8fe21 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDateValueParser.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseDateValueParser.java @@ -7,6 +7,7 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; @@ -20,6 +21,7 @@ import ru.yandex.clickhouse.response.ClickHouseColumnInfo; abstract class ClickHouseDateValueParser extends ClickHouseValueParser { + private static final ZoneId UTC_ZONE = ZoneId.of("UTC"); private static final Pattern PATTERN_EMPTY_DATE = Pattern.compile("^(0000-00-00|0000-00-00 00:00:00|0)$"); @@ -27,7 +29,7 @@ abstract class ClickHouseDateValueParser extends ClickHouseValueParser { 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]"); + DateTimeFormatter.ofPattern("yyyy-MM-dd['T'][ ]HH:mm:ss"); private static final DateTimeFormatter TIME_FORMATTER_NUMBERS = DateTimeFormatter.ofPattern("HH[mm][ss]") .withResolverStyle(ResolverStyle.STRICT); @@ -38,6 +40,33 @@ protected ClickHouseDateValueParser(Class clazz) { this.clazz = Objects.requireNonNull(clazz); } + protected LocalDateTime dateToLocalDate(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { + return parseAsLocalDate(value).atStartOfDay(); + } + + protected LocalDateTime dateTimeToLocalDateTime(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { + TimeZone serverTimeZone = columnInfo.getTimeZone(); + LocalDateTime localDateTime = parseAsLocalDateTime(value); + if (serverTimeZone != null && (serverTimeZone.useDaylightTime() || serverTimeZone.getRawOffset() > 0)) { // non-UTC + localDateTime = localDateTime.atZone(columnInfo.getTimeZone().toZoneId()) + .withZoneSameInstant(java.time.ZoneId.of("UTC")).toLocalDateTime(); + } + + return localDateTime; + } + + protected ZonedDateTime dateToZonedDateTime(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { + LocalDate localDate = parseAsLocalDate(value); + return localDate.atStartOfDay(timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault()); + } + + protected ZonedDateTime dateTimeToZonedDateTime(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { + LocalDateTime localDateTime = parseAsLocalDateTime(value); + return timeZone != null && !timeZone.equals(columnInfo.getTimeZone()) + ? localDateTime.atZone(columnInfo.getTimeZone().toZoneId()).withZoneSameInstant(timeZone.toZoneId()) + : localDateTime.atZone(columnInfo.getTimeZone().toZoneId()); + } + @Override public T parse(ByteFragment value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) throws ClickHouseException @@ -69,6 +98,8 @@ public T parse(ByteFragment value, ClickHouseColumnInfo columnInfo, e); } case DateTime: + case DateTime32: + case DateTime64: try { return parseDateTime(s, columnInfo, timeZone); } catch (Exception e) { @@ -146,10 +177,10 @@ abstract T parseOther(String value, ClickHouseColumnInfo columnInfo, protected final ZoneId effectiveTimeZone(ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return columnInfo.getTimeZone() != null - ? columnInfo.getTimeZone().toZoneId() - : timeZone != null - ? timeZone.toZoneId() + return timeZone != null + ? timeZone.toZoneId() + : columnInfo.getTimeZone() != null + ? columnInfo.getTimeZone().toZoneId() : ZoneId.systemDefault(); } @@ -158,6 +189,28 @@ protected final LocalDate parseAsLocalDate(String value) { } protected final LocalDateTime parseAsLocalDateTime(String value) { + int index = value == null ? -1 : value.indexOf('.'); + if (index > 0) { + int endIndex = -1; + for (int i = index + 1, len = value.length(); i < len; i++) { + char ch = value.charAt(i); + if (!Character.isDigit(ch)) { + endIndex = i; + break; + } + } + String part1 = value.substring(0, index); + if (endIndex > index) { + part1 += value.substring(endIndex); + } + String part2 = endIndex > index ? value.substring(index, endIndex) : value.substring(index); + + LocalDateTime ts = LocalDateTime.parse(part1, DATE_TIME_FORMATTER); + int nanoSeconds = (int) Math.round(Double.parseDouble(part2) * 1000000000); + return LocalDateTime.of(ts.getYear(), ts.getMonth(), ts.getDayOfMonth(), + ts.getHour(), ts.getMinute(), ts.getSecond(), nanoSeconds); + } + return LocalDateTime.parse(value, DATE_TIME_FORMATTER); } diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseInstantParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseInstantParser.java index af539b1ff..a770a0c5e 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseInstantParser.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseInstantParser.java @@ -1,6 +1,7 @@ package ru.yandex.clickhouse.response.parser; import java.time.Instant; +import java.time.ZoneOffset; import java.time.format.DateTimeParseException; import java.util.TimeZone; @@ -25,18 +26,14 @@ private ClickHouseInstantParser() { Instant parseDate(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return parseAsLocalDate(value) - .atStartOfDay(effectiveTimeZone(columnInfo, timeZone)) - .toInstant(); + return parseAsLocalDate(value).atStartOfDay().toInstant(ZoneOffset.UTC); } @Override Instant parseDateTime(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return parseAsLocalDateTime(value) - .atZone(effectiveTimeZone(columnInfo, timeZone)) - .toInstant(); + return dateTimeToZonedDateTime(value, columnInfo, timeZone).toInstant(); } @Override diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateParser.java index 7c13ec9e6..34d3ad3b3 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateParser.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateParser.java @@ -26,14 +26,14 @@ private ClickHouseLocalDateParser() { LocalDate parseDate(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return parseAsLocalDate(value); + return dateToLocalDate(value, columnInfo, timeZone).toLocalDate(); } @Override LocalDate parseDateTime(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return parseAsLocalDateTime(value).toLocalDate(); + return dateTimeToLocalDateTime(value, columnInfo, timeZone).toLocalDate(); } @Override diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateTimeParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateTimeParser.java index 61bc7d636..34080f73f 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateTimeParser.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalDateTimeParser.java @@ -33,7 +33,7 @@ LocalDateTime parseDate(String value, ClickHouseColumnInfo columnInfo, LocalDateTime parseDateTime(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return parseAsLocalDateTime(value); + return dateTimeToLocalDateTime(value, columnInfo, timeZone); } @Override diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalTimeParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalTimeParser.java index bd2c243d8..3e9381919 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalTimeParser.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseLocalTimeParser.java @@ -33,7 +33,7 @@ LocalTime parseDate(String value, ClickHouseColumnInfo columnInfo, LocalTime parseDateTime(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return parseAsLocalDateTime(value).toLocalTime(); + return dateTimeToLocalDateTime(value, columnInfo, timeZone).toLocalTime(); } @Override diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseMapParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseMapParser.java new file mode 100644 index 000000000..c0b7d44e3 --- /dev/null +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseMapParser.java @@ -0,0 +1,183 @@ +package ru.yandex.clickhouse.response.parser; + +import java.sql.Array; +import java.sql.SQLException; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.TimeZone; + +import ru.yandex.clickhouse.domain.ClickHouseDataType; +import ru.yandex.clickhouse.response.ByteFragment; +import ru.yandex.clickhouse.response.ClickHouseColumnInfo; + +@SuppressWarnings("rawtypes") +final class ClickHouseMapParser extends ClickHouseValueParser { + + private static ClickHouseMapParser instance; + + static ClickHouseMapParser getInstance() { + if (instance == null) { + instance = new ClickHouseMapParser(); + } + return instance; + } + + private ClickHouseMapParser() { + // prevent instantiation + } + + int readPart(ClickHouseDataType type, String str, int startPosition, int len, StringBuilder sb, char stopChar) { + Deque stack = new ArrayDeque<>(); + stack.push('\0'); + char lastChar = '\0'; + for (int i = startPosition; i < len; i++) { + char ch = str.charAt(startPosition = i); + + if (lastChar == '\0') { + if (Character.isWhitespace(ch)) { + continue; + } + + if (ch == stopChar) { + break; + } + + switch (ch) { + case '\'': + if (lastChar != '\0') { + stack.push(lastChar); + } + lastChar = ch; + if (type != ClickHouseDataType.String) { + sb.append(ch); + } + break; + case '{': + if (lastChar != '\0') { + stack.push(lastChar); + } + lastChar = '}'; + sb.append(ch); + break; + case '(': + if (lastChar != '\0') { + stack.push(lastChar); + } + lastChar = ')'; + sb.append(ch); + break; + case '[': + if (lastChar != '\0') { + stack.push(lastChar); + } + lastChar = ']'; + sb.append(ch); + break; + case '}': + return i + 1; + default: + sb.append(ch); + break; + } + } else if (lastChar == '\'') { // quoted + if (ch != '\'' || type != ClickHouseDataType.String) { + sb.append(ch); + } + if (i + 1 < len) { + char nextChar = str.charAt(i + 1); + if (ch == '\\') { + sb.append(nextChar); + i++; + } else if (ch == '\'' && nextChar == ch) { + sb.append(ch).append(nextChar); + i++; + } else if (ch == '\'') { + lastChar = stack.pop(); + } + } + } else if (lastChar == '}' || lastChar == ')' || lastChar == ']') { + if (ch == lastChar) { + lastChar = stack.pop(); + } + sb.append(ch); + } + } + + return startPosition; + } + + @Override + public Map parse(ByteFragment value, ClickHouseColumnInfo columnInfo, TimeZone resultTimeZone) throws SQLException { + if (value.isNull()) { + return null; + } + + ClickHouseColumnInfo keyInfo = Objects.requireNonNull(columnInfo.getKeyInfo()); + ClickHouseColumnInfo valueInfo = Objects.requireNonNull(columnInfo.getValueInfo()); + + ClickHouseValueParser keyParser = ClickHouseValueParser + .getParser(keyInfo.getClickHouseDataType().getJavaClass()); + ClickHouseValueParser valueParser = ClickHouseValueParser + .getParser(valueInfo.getClickHouseDataType().getJavaClass()); + + String str = value.asString(); + int len = str == null ? 0 : str.length(); + if (len < 2) { + return Collections.emptyMap(); + } + + Map map = new LinkedHashMap<>(); + + int part = -1; // -1 - uncertain, 0 - key, 1 - value + StringBuilder sb = new StringBuilder(); + Object k = null; + Object v = null; + for (int i = 0; i < len; i++) { + char ch = str.charAt(i); + + if (Character.isWhitespace(ch)) { // skip whitespaces + continue; + } + + if (part == -1) { + if (ch == '{') { + part = 0; + continue; + } else { + throw new IllegalArgumentException("Invalid map. Expect '{' but we got '" + ch + "' at " + i); + } + } + + if (ch == '}') { + // TODO check if there's any pending characters + break; + } + + if (part == 0) { // reading key(String or Integer) + i = readPart(keyInfo.getClickHouseDataType(), str, i, len, sb, ':'); + k = keyParser.parse(ByteFragment.fromString(sb.toString()), keyInfo, resultTimeZone); + + part = 1; + sb.setLength(0); + } else { // reading value(String, Integer or Array) + i = readPart(valueInfo.getClickHouseDataType(), str, i, len, sb, ','); + v = valueParser.parse(ByteFragment.fromString(sb.toString()), valueInfo, resultTimeZone); + map.put(k, valueInfo.isArray() && v != null ? ((Array) v).getArray() : v); + + part = 0; + sb.setLength(0); + } + } + + return Collections.unmodifiableMap(map); + } + + @Override + protected Map getDefaultValue() { + return Collections.emptyMap(); + } +} diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetDateTimeParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetDateTimeParser.java index d6e58621c..a74b45866 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetDateTimeParser.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetDateTimeParser.java @@ -26,18 +26,14 @@ private ClickHouseOffsetDateTimeParser() { OffsetDateTime parseDate(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return parseAsLocalDate(value) - .atStartOfDay(effectiveTimeZone(columnInfo, timeZone)) - .toOffsetDateTime(); + return dateToZonedDateTime(value, columnInfo, timeZone).toOffsetDateTime(); } @Override OffsetDateTime parseDateTime(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return parseAsLocalDateTime(value) - .atZone(effectiveTimeZone(columnInfo, timeZone)) - .toOffsetDateTime(); + return dateTimeToZonedDateTime(value, columnInfo, timeZone).toOffsetDateTime(); } @Override diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetTimeParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetTimeParser.java index 374e08768..5feb348c1 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetTimeParser.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseOffsetTimeParser.java @@ -29,20 +29,14 @@ private ClickHouseOffsetTimeParser() { OffsetTime parseDate(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return OffsetTime.of( - LocalTime.MIDNIGHT, - effectiveTimeZone(columnInfo, timeZone).getRules().getOffset( - parseAsLocalDate(value).atStartOfDay())); + return dateToZonedDateTime(value, columnInfo, timeZone).toOffsetDateTime().toOffsetTime(); } @Override OffsetTime parseDateTime(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - LocalDateTime ldt = parseAsLocalDateTime(value); - return OffsetTime.of( - ldt.toLocalTime(), - effectiveTimeZone(columnInfo, timeZone).getRules().getOffset(ldt)); + return dateTimeToZonedDateTime(value, columnInfo, timeZone).toOffsetDateTime().toOffsetTime(); } @Override diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLDateParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLDateParser.java index 314d4019a..77c9732a3 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLDateParser.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLDateParser.java @@ -29,22 +29,17 @@ private ClickHouseSQLDateParser() { Date parseDate(String value, ClickHouseColumnInfo columnInfo, TimeZone timeZone) { - return new Date(parseAsLocalDate(value) - .atStartOfDay(getParsingTimeZone(columnInfo, timeZone)) - .toInstant() - .toEpochMilli()); + return new Date(dateToZonedDateTime(value, columnInfo, timeZone).truncatedTo(ChronoUnit.DAYS).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()); + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + return new Date(dateTimeToZonedDateTime(value, columnInfo, timeZone).truncatedTo(ChronoUnit.DAYS).toInstant().toEpochMilli()); } @Override diff --git a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLTimeParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLTimeParser.java index b1b42f4a1..357729fec 100644 --- a/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLTimeParser.java +++ b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLTimeParser.java @@ -11,8 +11,6 @@ import ru.yandex.clickhouse.response.ClickHouseColumnInfo; final class ClickHouseSQLTimeParser extends ClickHouseDateValueParser