Skip to content

[jdbc-v2] Deserialization fails for array columns with nested value types #3178

Description

@GFriedrich

Description

The JDBC v2 driver fails to deserialize data from columns of type Array(Nested(...)).

Steps to reproduce

  1. Create a table with a Array(Nested(`key` String, `value` String)) column
  2. Add some data to the column
  3. Try to select the data from the table, e.g. via a simple SELECT * FROM xyz statement.
  4. Get the data from the specific column.
  5. 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))
);
  • Sample data:
INSERT INTO some_data (id, data) VALUES
(1, 
 [
    [('key1', 'test'), ('key2', 'another-test')],
    [('key1', 'more-data')]
 ]
);

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions