Skip to content

Commit 9887062

Browse files
committed
draft new impl of encoding
1 parent 14c737f commit 9887062

File tree

6 files changed

+287
-54
lines changed

6 files changed

+287
-54
lines changed

jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -530,24 +530,30 @@ public Properties getClientInfo() throws SQLException {
530530
return clientInfo;
531531
}
532532

533+
/**
534+
* Creating multilevel arrays may be confusing.
535+
* Spec doesn't tell much about it so there may be different variants.
536+
* Note: createArrayOf() expect type name be for element of the array and for
537+
* Array(Array(Int8)) it should be Int8 according to spec. However element type
538+
* of 1st level array is Array(Int8)
539+
* @param typeName the SQL name of the type the elements of the array map to. The typeName is a
540+
* database-specific name which may be the name of a built-in type, a user-defined type or a standard SQL type supported by this database. This
541+
* is the value returned by {@code Array.getBaseTypeName}
542+
*
543+
* @param elements the elements that populate the returned object
544+
* @return
545+
* @throws SQLException
546+
*/
533547
@Override
534548
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
535549
ensureOpen();
536550
if (typeName == null) {
537551
throw new SQLFeatureNotSupportedException("typeName cannot be null");
538552
}
539553

540-
541-
int parentPos = typeName.indexOf('(');
542-
int endPos = parentPos == -1 ? typeName.length() : parentPos;
543-
String clickhouseDataTypeName = (typeName.substring(0, endPos)).trim();
544-
ClickHouseDataType dataType = ClickHouseDataType.valueOf(clickhouseDataTypeName);
545-
if (dataType.equals(ClickHouseDataType.Array)) {
546-
throw new SQLException("Array cannot be a base type. In case of nested array provide most deep element type name.");
547-
}
554+
ClickHouseColumn column = ClickHouseColumn.of("array", typeName);
548555
try {
549-
return new com.clickhouse.jdbc.types.Array(elements, typeName,
550-
JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.getOrDefault(dataType, JDBCType.OTHER).getVendorTypeNumber());
556+
return new com.clickhouse.jdbc.types.Array(column, elements);
551557
} catch (Exception e) {
552558
throw new SQLException("Failed to create array", ExceptionUtils.SQL_STATE_CLIENT_ERROR, e);
553559
}

jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import java.util.Collections;
6060
import java.util.List;
6161
import java.util.Map;
62+
import java.util.Stack;
6263
import java.util.UUID;
6364
import java.util.regex.Matcher;
6465
import java.util.regex.Pattern;
@@ -790,12 +791,13 @@ private String encodeObject(Object x, Long length) throws SQLException {
790791
} else if (x instanceof InetAddress) {
791792
return QUOTE + ((InetAddress) x).getHostAddress() + QUOTE;
792793
} else if (x instanceof java.sql.Array) {
793-
StringBuilder listString = new StringBuilder();
794-
listString.append(O_BRACKET);
795-
appendArrayElements((Object[]) ((Array) x).getArray(), listString);
796-
listString.append(C_BRACKET);
797-
798-
return listString.toString();
794+
com.clickhouse.jdbc.types.Array array = (com.clickhouse.jdbc.types.Array) x;
795+
int nestedLevel = Math.max(1, array.getNestedLevel());
796+
if (array.getBaseDataType() == ClickHouseDataType.Tuple) {
797+
return encodeTuple((Object[]) array.getArray());
798+
} else {
799+
return encodeArray((Object[]) array.getArray(), Math.max(1, array.getNestedLevel()), array.getBaseDataType());
800+
}
799801
} else if (x instanceof Object[]) {
800802
StringBuilder arrayString = new StringBuilder();
801803
arrayString.append(O_BRACKET);
@@ -851,9 +853,9 @@ private String encodeObject(Object x, Long length) throws SQLException {
851853
} else if (x instanceof InputStream) {
852854
return encodeCharacterStream((InputStream) x, length);
853855
} else if (x instanceof Tuple) {
854-
return arrayToTuple(((Tuple)x).getValues());
856+
return encodeTuple(((Tuple)x).getValues());
855857
} else if (x instanceof Struct) {
856-
return arrayToTuple(((Struct)x).getAttributes());
858+
return encodeTuple(((Struct)x).getAttributes());
857859
} else if (x instanceof UUID) {
858860
return QUOTE + ((UUID) x).toString() + QUOTE;
859861
}
@@ -866,20 +868,96 @@ private String encodeObject(Object x, Long length) throws SQLException {
866868
}
867869

868870
private void appendArrayElements(Object[] array, StringBuilder sb) throws SQLException {
871+
appendArrayElements(array, sb, null);
872+
}
873+
874+
private void appendArrayElements(Object[] array, StringBuilder sb, ClickHouseDataType elementType) throws SQLException {
875+
if (array == null) {
876+
return;
877+
}
869878
for (Object item : array) {
870-
sb.append(encodeObject(item)).append(',');
879+
if (elementType == ClickHouseDataType.Tuple && item != null && item.getClass().isArray()) {
880+
sb.append(encodeTuple((Object[]) item));
881+
} else {
882+
sb.append(encodeObject(item)).append(',');
883+
}
871884
}
872885
if (array.length > 0) {
873886
sb.setLength(sb.length() - 1);
874887
}
875888
}
876889

877-
private String arrayToTuple(Object[] array) throws SQLException {
878-
StringBuilder tupleString = new StringBuilder();
879-
tupleString.append('(');
880-
appendArrayElements(array, tupleString);
881-
tupleString.append(')');
882-
return tupleString.toString();
890+
public String encodeArray(Object[] elements, int levels, ClickHouseDataType elementType) throws SQLException {
891+
if (elements == null) {
892+
return "[]";
893+
}
894+
895+
StringBuilder arraySb = new StringBuilder();
896+
Stack<ArrayProcessingCursor> stack = new Stack<>();
897+
ArrayProcessingCursor cursor = new ArrayProcessingCursor(elements, 0, levels);
898+
899+
arraySb.append(O_BRACKET);
900+
while (cursor != null) {
901+
if (cursor.pos >= cursor.array.length) {
902+
if (cursor.array.length > 0) {
903+
arraySb.setLength(arraySb.length() - 1);
904+
}
905+
arraySb.append(C_BRACKET);
906+
cursor = stack.isEmpty() ? null : stack.pop();
907+
if (cursor != null) {
908+
arraySb.append(',');
909+
}
910+
continue;
911+
}
912+
913+
Object element = cursor.array[cursor.pos];
914+
if (element == null) {
915+
if (cursor.level == 1) {
916+
arraySb.append("NULL");
917+
} else {
918+
arraySb.append("[]");
919+
}
920+
arraySb.append(',');
921+
cursor.pos++;
922+
} else if (cursor.isTuples) {
923+
arraySb.append(encodeTuple((Object[]) element)).append(',');
924+
cursor.pos++;
925+
} else if (cursor.level == 1 && element.getClass().isArray() && elementType == ClickHouseDataType.Tuple) {
926+
cursor.isTuples = true;
927+
} else if (cursor.level == 1) {
928+
arraySb.append(encodeObject(element)).append(',');
929+
cursor.pos++;
930+
} else {
931+
cursor.pos++;
932+
stack.push(cursor);
933+
cursor = new ArrayProcessingCursor((Object[]) element, 0, cursor.level - 1);
934+
arraySb.append(O_BRACKET);
935+
}
936+
}
937+
938+
return arraySb.toString();
939+
}
940+
941+
private static final class ArrayProcessingCursor {
942+
Object[] array; // current array
943+
int pos; // processing position
944+
int level;
945+
boolean isTuples = false;
946+
boolean isElements = false;
947+
public ArrayProcessingCursor(Object[] array, int pos, int level) {
948+
this.array = array;
949+
this.pos = pos;
950+
this.level = level;
951+
}
952+
}
953+
954+
private String encodeTuple(Object[] array) throws SQLException {
955+
StringBuilder sb = new StringBuilder('(');
956+
if (array != null) {
957+
appendArrayElements(array, sb);
958+
}
959+
sb.append(')');
960+
return sb.toString();
883961
}
884962

885963
private static String encodeCharacterStream(InputStream stream, Long length) throws SQLException {

jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -269,19 +269,18 @@ public static Object convert(Object value, Class<?> type, ClickHouseColumn colum
269269
BinaryStreamReader.ArrayValue arrayValue = (BinaryStreamReader.ArrayValue) value;
270270
if (column != null && column.getArrayBaseColumn() != null) {
271271
ClickHouseDataType baseType = column.getArrayBaseColumn().getDataType();
272-
Object[] convertedValues = convertArray(arrayValue.getArrayOfObjects(),
273-
JdbcUtils.convertToJavaClass(baseType));
274-
return new Array(convertedValues, baseType.getName(), baseType.getVendorTypeNumber());
272+
Object[] convertedValues = convertArray(arrayValue.getArrayOfObjects(), JdbcUtils.convertToJavaClass(baseType));
273+
return new Array(column, convertedValues);
275274
}
276-
return new Array(arrayValue.getArrayOfObjects(), "Unknown", JDBCType.OTHER.getVendorTypeNumber());
275+
return new Array(column, arrayValue.getArrayOfObjects());
277276
} else if (type == java.sql.Array.class && value instanceof List<?>) {
278277
if (column != null && column.getArrayBaseColumn() != null) {
279278
ClickHouseDataType baseType = column.getArrayBaseColumn().getDataType();
280-
return new Array(convertList((List<?>) value, JdbcUtils.convertToJavaClass(baseType)),
281-
baseType.getName(), JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.getOrDefault(baseType, JDBCType.OTHER).getVendorTypeNumber());
279+
Object[] convertedValues = convertList((List<?>) value, JdbcUtils.convertToJavaClass(baseType));
280+
return new Array(column, convertedValues);
282281
}
283282
// base type is unknown. all objects should be converted
284-
return new Array(((List<?>) value).toArray(), "Unknown", JDBCType.OTHER.getVendorTypeNumber());
283+
return new Array(column, ((List<?>) value).toArray());
285284
} else if (type == Inet4Address.class && value instanceof Inet6Address) {
286285
// Convert Inet6Address to Inet4Address
287286
return InetAddressConverter.convertToIpv4((InetAddress) value);

jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
11
package com.clickhouse.jdbc.types;
22

3+
import com.clickhouse.data.ClickHouseColumn;
4+
import com.clickhouse.data.ClickHouseDataType;
35
import com.clickhouse.jdbc.internal.ExceptionUtils;
6+
import com.clickhouse.jdbc.internal.JdbcUtils;
47
import org.slf4j.Logger;
58
import org.slf4j.LoggerFactory;
69

10+
import java.sql.JDBCType;
711
import java.sql.ResultSet;
812
import java.sql.SQLException;
913
import java.sql.SQLFeatureNotSupportedException;
10-
import java.util.List;
1114
import java.util.Map;
1215

1316
public class Array implements java.sql.Array {
1417
private static final Logger log = LoggerFactory.getLogger(Array.class);
1518

19+
private final ClickHouseColumn column;
1620
private Object[] array;
1721
private final int type; //java.sql.Types
1822
private final String elementTypeName;
1923
private boolean valid;
24+
private final ClickHouseDataType baseDataType;
2025

21-
/**
22-
* @deprecated this constructor should not be used. Elements array should be constructed externally.
23-
*/
24-
public Array(List<Object> list, String elementTypeName, int itemType) throws SQLException {
25-
this(list.toArray(), elementTypeName, itemType);
26-
}
27-
28-
public Array(Object[] elements, String elementTypeName, int itemType) throws SQLException {
29-
if (elements == null) {
30-
throw ExceptionUtils.toSqlState(new IllegalArgumentException("Array cannot be null"));
31-
}
32-
if (elementTypeName == null) {
33-
throw ExceptionUtils.toSqlState(new IllegalArgumentException("Array element type name cannot be null"));
34-
}
26+
public Array(ClickHouseColumn column, Object[] elements) throws SQLException {
27+
this.column = column;
3528
this.array = elements;
36-
this.type = itemType;
37-
this.elementTypeName = elementTypeName;
29+
ClickHouseColumn baseColumn = (this.column.isArray() ? this.column.getArrayBaseColumn() : this.column);
30+
this.baseDataType = baseColumn.getDataType();
31+
this.elementTypeName = baseColumn.getOriginalTypeName();
32+
this.type = JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.getOrDefault(baseDataType, JDBCType.OTHER).getVendorTypeNumber();
3833
this.valid = true;
3934
}
4035

36+
public ClickHouseDataType getBaseDataType() {
37+
return baseDataType;
38+
}
39+
40+
public int getNestedLevel() {
41+
return column.getArrayNestedLevel();
42+
}
43+
4144
@Override
4245
public String getBaseTypeName() throws SQLException {
4346
ensureValid();
@@ -70,7 +73,7 @@ public Object getArray(long index, int count) throws SQLException {
7073
if (count < 0) {
7174
throw new SQLException("Count cannot be negative");
7275
}
73-
if (count > (array.length - index)) {
76+
if (array == null || count > (array.length - index)) {
7477
throw new SQLException("Not enough elements after index " + index);
7578
}
7679

jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.time.temporal.TemporalAccessor;
3939
import java.util.Arrays;
4040
import java.util.Base64;
41+
import java.util.Collections;
4142
import java.util.List;
4243
import java.util.Properties;
4344
import java.util.UUID;
@@ -377,9 +378,6 @@ public static Object[][] setAndGetClientInfoTestDataProvider() {
377378
public void testCreateArray() throws SQLException {
378379
try (Connection conn = getJdbcConnection()) {
379380

380-
Assert.expectThrows(SQLException.class, () -> conn.createArrayOf("Array()", new Integer[] {1}));
381-
382-
383381
final String baseType = "Tuple(String, Int8)";
384382
final String tableName = "array_create_test";
385383
final String arrayType = "Array(Array(" + baseType + "))";
@@ -458,7 +456,6 @@ public void testCreateArrayDifferentTypes() throws Exception {
458456
}
459457
};
460458

461-
462459
verification.accept("Int8", new Byte[] {1, 2, 3});
463460
verification.accept("Int16", new Short[] {Short.MIN_VALUE, -1, 0, 1, Short.MAX_VALUE});
464461
verification.accept("Int32", new Integer[] {Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE});
@@ -505,6 +502,88 @@ public void testCreateArrayDifferentTypes() throws Exception {
505502
}
506503
}
507504

505+
@Test(groups = {"integration"})
506+
public void testCreateArrayVariants() throws Exception {
507+
try (Connection conn = getJdbcConnection()) {
508+
509+
// it is valid
510+
{
511+
Array array = conn.createArrayOf("Nullable(String)", (Object[]) null);
512+
assertNull(array.getArray());
513+
assertThrows(SQLException.class, () -> array.getArray(10, 10));
514+
assertThrows(SQLFeatureNotSupportedException.class, () -> array.getArray(10, 10, Collections.emptyMap()));
515+
516+
try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Nullable(String)) as value")) {
517+
stmt.setArray(1, array);
518+
try (ResultSet rs = stmt.executeQuery()) {
519+
rs.next();
520+
assertEquals(rs.getMetaData().getColumnTypeName(1), "Array(Nullable(String))");
521+
assertEquals(rs.getArray(1).getArray(), new String[] {});
522+
// assertEquals(rs.getArray(1).getArray().getClass(), String[].class); // TODO: fix
523+
}
524+
}
525+
}
526+
527+
// array of nullables
528+
{
529+
String[] strings = new String[] {"one", null, "five"};
530+
Array array = conn.createArrayOf("Nullable(String)", strings);
531+
assertNotNull(array.getArray());
532+
533+
try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Nullable(String)) as value")) {
534+
stmt.setArray(1, array);
535+
try (ResultSet rs = stmt.executeQuery()) {
536+
rs.next();
537+
assertEquals(rs.getMetaData().getColumnTypeName(1), "Array(Nullable(String))");
538+
}
539+
}
540+
}
541+
542+
// multi-level array
543+
{
544+
Object[][] table = new Object[][] {
545+
{1, 2 ,3, 4, 5},
546+
{10, 20, 30, 40, 50, },
547+
};
548+
Array array = conn.createArrayOf("Array(Array(Int32))", table);
549+
550+
}
551+
552+
553+
// array of tuples
554+
{
555+
Object[][] tuples = new Object[][] {
556+
{"tuple1", 10},
557+
{"tuple2", 20},
558+
};
559+
Array array = conn.createArrayOf("Tuple(String, Int32)", tuples);
560+
assertNotNull(array.getArray());
561+
562+
try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Tuple(String, Int32)) as value")) {
563+
stmt.setArray(1, array);
564+
try (ResultSet rs = stmt.executeQuery()) {
565+
rs.next();
566+
assertEquals(rs.getMetaData().getColumnTypeName(1), "Array(Tuple(String, Int32))");
567+
}
568+
}
569+
}
570+
571+
{
572+
Array tuple1 = conn.createArrayOf("String", new String[] {"one", "two"});
573+
Array tuple2 = conn.createArrayOf("String", new String[] {"three", "four"});
574+
Array array = conn.createArrayOf("Tuple(String, String)", new Object[] {tuple1, tuple2});
575+
576+
try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Tuple(String, String)) as value")) {
577+
stmt.setArray(1, array);
578+
try (ResultSet rs = stmt.executeQuery()) {
579+
rs.next();
580+
assertEquals(rs.getMetaData().getColumnTypeName(1), "Array(Tuple(String, String))");
581+
}
582+
}
583+
}
584+
}
585+
}
586+
508587
@Test(groups = { "integration" })
509588
public void testCreateStruct() throws SQLException {
510589
try (Connection conn = this.getJdbcConnection()) {

0 commit comments

Comments
 (0)