From 6f6985372b051e531d424c64447e039792260508 Mon Sep 17 00:00:00 2001 From: Zhichun Wu Date: Fri, 5 Mar 2021 20:21:26 +0800 Subject: [PATCH 1/8] Support more data types --- README.md | 2 +- .../clickhouse/domain/ClickHouseDataType.java | 112 +++++--- .../response/ClickHouseColumnInfo.java | 42 ++- .../response/ClickHouseResultSet.java | 18 +- .../parser/ClickHouseArrayParser.java | 59 ++++ .../parser/ClickHouseDateValueParser.java | 26 +- .../response/parser/ClickHouseMapParser.java | 183 ++++++++++++ .../parser/ClickHouseSQLTimeParser.java | 21 +- .../parser/ClickHouseValueParser.java | 3 + .../util/ClickHouseRowBinaryInputStream.java | 21 +- .../util/ClickHouseRowBinaryStream.java | 76 +++-- .../util/ClickHouseValueFormatter.java | 64 +++-- .../java/ru/yandex/clickhouse/util/Utils.java | 35 +++ .../integration/ClickHouseDataTypeTest.java | 271 ++++++++++++++++++ .../response/ClickHouseResultSetTest.java | 24 ++ .../parser/ClickHouseMapParserTest.java | 126 ++++++++ .../parser/ClickHouseSQLTimeParserTest.java | 2 +- .../ru/yandex/clickhouse/util/UtilsTest.java | 2 +- 18 files changed, 974 insertions(+), 113 deletions(-) create mode 100644 clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseArrayParser.java create mode 100644 clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseMapParser.java create mode 100644 clickhouse-jdbc/src/test/java/ru/yandex/clickhouse/integration/ClickHouseDataTypeTest.java create mode 100644 clickhouse-jdbc/src/test/java/ru/yandex/clickhouse/response/parser/ClickHouseMapParserTest.java 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/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..0387489b9 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,6 +20,8 @@ public final class ClickHouseColumnInfo { private TimeZone timeZone; private int precision; private int scale; + private ClickHouseColumnInfo keyInfo; + private ClickHouseColumnInfo valueInfo; public static ClickHouseColumnInfo parse(String typeInfo, String columnName) { ClickHouseColumnInfo column = new ClickHouseColumnInfo(typeInfo, columnName); @@ -61,14 +63,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(argsTZ[0].replace("'", "")); + 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(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 +100,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 +108,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"); + column.valueInfo = ClickHouseColumnInfo.parse(argsMap[1], columnName + "Value"); + } + break; default : break; } @@ -184,4 +213,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..f78526e4a 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 @@ -537,8 +537,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 +560,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); } @@ -718,6 +721,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..57be2a73e 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 @@ -27,7 +27,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); @@ -69,6 +69,8 @@ public T parse(ByteFragment value, ClickHouseColumnInfo columnInfo, e); } case DateTime: + case DateTime32: + case DateTime64: try { return parseDateTime(s, columnInfo, timeZone); } catch (Exception e) { @@ -158,6 +160,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/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/ClickHouseSQLTimeParser.java b/clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/response/parser/ClickHouseSQLTimeParser.java index b1b42f4a1..2d18a0c03 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 @@ -8,6 +8,7 @@ import java.time.format.DateTimeParseException; import java.util.TimeZone; +import ru.yandex.clickhouse.domain.ClickHouseDataType; import ru.yandex.clickhouse.response.ClickHouseColumnInfo; final class ClickHouseSQLTimeParser extends ClickHouseDateValueParser