Description
The JDBC v2 driver fails to deserialize data from columns of type Array(Nested(...)).
Steps to reproduce
- Create a table with a
Array(Nested(`key` String, `value` String)) column
- Add some data to the column
- Try to select the data from the table, e.g. via a simple
SELECT * FROM xyz statement.
- Get the data from the specific column.
- See that deserialization fails.
Error Log or Exception StackTrace
Caused by: java.lang.IllegalStateException: There is a child array at depth 0 where it is not expected
at com.clickhouse.jdbc.internal.JdbcUtils.convertList(JdbcUtils.java:446)
at com.clickhouse.jdbc.internal.JdbcUtils.convert(JdbcUtils.java:314)
at com.clickhouse.jdbc.ResultSetImpl.getObjectImpl(ResultSetImpl.java:1555)
at com.clickhouse.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:1488)
at com.clickhouse.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:1483)
Expected Behaviour
Deserialization should just work and not fail.
Root Cause Analysis
The issue lies inside the convertList method (and also convertArray as it contains some mirrored code for arrays).
See
|
} else if (value instanceof List<?>) { |
|
List<?> srcList = (List<?>) value; |
|
int depth = cursor.depth - 1; |
|
if (depth <= 0) { |
|
throw new IllegalStateException("There is a child array at depth 0 where it is not expected"); |
|
} |
|
arrayDimensions = new int[depth]; |
|
arrayDimensions[0] = srcList.size(); |
|
T[] targetArray = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions); |
|
stack.push(new ArrayProcessingCursor(targetArray, value, srcList.size(), depth)); |
|
java.lang.reflect.Array.set(cursor.targetArray, i, targetArray); |
|
} else { |
This code expects that lists are only returned when the columns are of array types. But it misses that Nested columns also return list objects causing the code to enter this branch linked above and then failing on the depth check.
I've patched my local version with the following method to make it work:
public static <T> T[] convertList(List<?> values, Class<T> type, int dimensions) throws SQLException {
if (values == null) {
return null;
}
if (dimensions <= 0) {
throw new IllegalArgumentException("Cannot convert list to array with less then 1D");
}
int[] arrayDimensions = new int[dimensions];
arrayDimensions[0] = values.size();
T[] convertedValues;
if (type == java.sql.Array.class) {
convertedValues = (T[]) java.lang.reflect.Array.newInstance(Object.class, arrayDimensions);
} else {
convertedValues = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions);
}
Stack<ArrayProcessingCursor> stack = new Stack<>();
stack.push(new ArrayProcessingCursor(convertedValues, values, values.size(), dimensions));
while (!stack.isEmpty()) {
ArrayProcessingCursor cursor = stack.pop();
for (int i = 0; i < cursor.size; i++) {
Object value = cursor.getValue(i);
if (value == null) {
continue; // no need to set null value
} else if (value instanceof List<?>) {
List<?> srcList = (List<?>) value;
int depth = cursor.depth - 1;
if (depth <= 0) {
if (depth == 0 && type == java.sql.Array.class) {
Object[] targetArray = srcList.toArray();
java.lang.reflect.Array.set(cursor.targetArray, i, targetArray);
continue;
} else {
throw new IllegalStateException("There is a child array at depth 0 where it is not expected");
}
}
arrayDimensions = new int[depth];
arrayDimensions[0] = srcList.size();
T[] targetArray = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions);
stack.push(new ArrayProcessingCursor(targetArray, value, srcList.size(), depth));
java.lang.reflect.Array.set(cursor.targetArray, i, targetArray);
} else {
java.lang.reflect.Array.set(cursor.targetArray, i, convert(value, type));
}
}
}
return convertedValues;
}
It basically checks if the target type is an Array (which normally happens for arrays with a nested type) and in this case creating a generic object array where the arrays from the nested values are placed into. (Also patched the convertArray in an equal way.)
Obviously I don't know if that causes any side effects or other issues or whether the data format is what you would expect. At least for me it did the trick.
Configuration
Client Configuration
Nothing special besides a JDBC URL to the DB and username + password.
Environment
- Client version: 0.10.0
- Language version: Java 25
ClickHouse Server
- ClickHouse Server version: 26.3.33
- ClickHouse Server non-default settings, if any: None
CREATE TABLE statements for tables involved:
CREATE TABLE some_data (
`id` UInt64,
`data` Array(Nested(`key` String, `value` String))
);
INSERT INTO some_data (id, data) VALUES
(1,
[
[('key1', 'test'), ('key2', 'another-test')],
[('key1', 'more-data')]
]
);
Description
The JDBC v2 driver fails to deserialize data from columns of type
Array(Nested(...)).Steps to reproduce
Array(Nested(`key` String, `value` String))columnSELECT * FROM xyzstatement.Error Log or Exception StackTrace
Expected Behaviour
Deserialization should just work and not fail.
Root Cause Analysis
The issue lies inside the
convertListmethod (and alsoconvertArrayas it contains some mirrored code for arrays).See
clickhouse-java/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java
Lines 442 to 453 in 57aacb1
This code expects that lists are only returned when the columns are of array types. But it misses that
Nestedcolumns also return list objects causing the code to enter this branch linked above and then failing on thedepthcheck.I've patched my local version with the following method to make it work:
It basically checks if the target type is an Array (which normally happens for arrays with a nested type) and in this case creating a generic object array where the arrays from the nested values are placed into. (Also patched the
convertArrayin an equal way.)Obviously I don't know if that causes any side effects or other issues or whether the data format is what you would expect. At least for me it did the trick.
Configuration
Client Configuration
Nothing special besides a JDBC URL to the DB and username + password.
Environment
ClickHouse Server
CREATE TABLEstatements for tables involved: