From cfe631502cc689bed814aa6de8fd1252e6e56376 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 25 Nov 2025 13:09:52 -0800 Subject: [PATCH 1/2] fixed issue with nested arrays --- .../clickhouse/jdbc/internal/JdbcUtils.java | 68 +++++++++++++++++-- .../jdbc/internal/JdbcUtilsTest.java | 4 +- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java index 4b8b00a39..b80838ef0 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java @@ -6,6 +6,7 @@ import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.data.Tuple; import com.clickhouse.data.format.BinaryStreamUtils; +import com.clickhouse.jdbc.PreparedStatementImpl; import com.clickhouse.jdbc.types.Array; import com.google.common.collect.ImmutableMap; import org.slf4j.Logger; @@ -31,7 +32,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Stack; import java.util.TreeMap; +import java.util.function.Function; import java.util.stream.Collectors; public class JdbcUtils { @@ -85,6 +88,8 @@ private static Map generateTypeMap() { map.put(ClickHouseDataType.LineString, JDBCType.ARRAY); map.put(ClickHouseDataType.MultiPolygon, JDBCType.ARRAY); map.put(ClickHouseDataType.MultiLineString, JDBCType.ARRAY); + map.put(ClickHouseDataType.Tuple, JDBCType.OTHER); + map.put(ClickHouseDataType.Nothing, JDBCType.OTHER); return ImmutableMap.copyOf(map); } @@ -169,6 +174,8 @@ private static Map> getDataTypeClassMap() { default: map.put(e.getKey(), Object.class); } + } else if (e.getValue().equals(JDBCType.STRUCT)) { + map.put(e.getKey(), Object.class); } else { map.put(e.getKey(), SQL_TYPE_TO_CLASS_MAP.get(e.getValue())); } @@ -255,12 +262,12 @@ public static Object convert(Object value, Class type, ClickHouseColumn colum if (value instanceof List) { List listValue = (List) value; if (type != java.sql.Array.class) { - return convertList(listValue, type); + return convertList(listValue, type, column.getArrayNestedLevel()); } if (column != null && column.getArrayBaseColumn() != null) { ClickHouseDataType baseType = column.getArrayBaseColumn().getDataType(); - Object[] convertedValues = convertList(listValue, convertToJavaClass(baseType)); + Object[] convertedValues = convertList(listValue, convertToJavaClass(baseType), column.getArrayNestedLevel()); return new Array(column, convertedValues); } @@ -348,17 +355,41 @@ public static Object convertObject(Object value, Class type) throws SQLExcept throw new SQLException("Unsupported conversion from " + value.getClass().getName() + " to " + type.getName(), ExceptionUtils.SQL_STATE_DATA_EXCEPTION); } - public static T[] convertList(List values, Class type) throws SQLException { + public static T[] convertList(List values, Class type, int dimensions) throws SQLException { if (values == null) { return null; } if (values.isEmpty()) { return (T[]) java.lang.reflect.Array.newInstance(type, 0); } - T[] convertedValues = (T[]) java.lang.reflect.Array.newInstance(type, values.size()); - for (int i = 0; i < values.size(); i++) { - convertedValues[i] = (T) convert(values.get(i), type); + + + int[] arrayDimensions = new int[dimensions]; + arrayDimensions[0] = values.size(); + T[] convertedValues = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions); + Stack stack = new Stack<>(); + stack.push(new ArrayProcessingCursor(convertedValues, values, 0, values.size())); + + 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; + arrayDimensions = new int[Math.max(dimensions - stack.size() - 1, 1)]; + arrayDimensions[0] = srcList.size(); + T[] targetArray = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions); + stack.push(new ArrayProcessingCursor(targetArray, value, 0, srcList.size())); + java.lang.reflect.Array.set(cursor.targetArray, i, targetArray); + } else { + java.lang.reflect.Array.set(cursor.targetArray, i, convert(value, type)); + } + } } + return convertedValues; } @@ -373,6 +404,31 @@ public static T[] convertArray(Object[] values, Class type) throws SQLExc return convertedValues; } + private static final class ArrayProcessingCursor { + private final Object targetArray; + private final Object srcArray; + private final int pos; + private final int size; + private final Function valueGetter; + + public ArrayProcessingCursor(Object targetArray, Object srcArray, int pos, int size) { + this.targetArray = targetArray; + this.srcArray = srcArray; + this.pos = pos; + this.size = size; + if (srcArray instanceof List) { + List list = (List) srcArray; + this.valueGetter = list::get; + } else { + this.valueGetter = (i) -> java.lang.reflect.Array.get(srcArray, i); + } + } + + public Object getValue(int i) { + return valueGetter.apply(i); + } + } + private static Object[] arrayToObjectArray(Object array) { if (array == null) { return null; diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java index d23605610..a6be0b82f 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java @@ -67,13 +67,13 @@ public void testConvertArray() throws Exception { public void testConvertList() throws Exception { ClickHouseColumn column = ClickHouseColumn.of("arr", "Array(Int32)"); List src = Arrays.asList(1, 2, 3); - Integer[] dst = JdbcUtils.convertList(src, Integer.class); + Integer[] dst = JdbcUtils.convertList(src, Integer.class, 1); assertEquals(dst.length, src.size()); assertEquals(dst[0], src.get(0)); assertEquals(dst[1], src.get(1)); assertEquals(dst[2], src.get(2)); - assertNull(JdbcUtils.convertList(null, Integer.class)); + assertNull(JdbcUtils.convertList(null, Integer.class, 1)); } From d7856fd6703dfb34f23b6dec0297f2fe83013636 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 25 Nov 2025 13:47:50 -0800 Subject: [PATCH 2/2] Fixed array convert with multidimension --- .../clickhouse/jdbc/internal/JdbcUtils.java | 59 +++++++++++++------ .../jdbc/internal/JdbcUtilsTest.java | 23 ++++---- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java index b80838ef0..81ceb5fa9 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java @@ -267,7 +267,8 @@ public static Object convert(Object value, Class type, ClickHouseColumn colum if (column != null && column.getArrayBaseColumn() != null) { ClickHouseDataType baseType = column.getArrayBaseColumn().getDataType(); - Object[] convertedValues = convertList(listValue, convertToJavaClass(baseType), column.getArrayNestedLevel()); + Object[] convertedValues = convertList(listValue, convertToJavaClass(baseType), + column.getArrayNestedLevel()); return new Array(column, convertedValues); } @@ -288,7 +289,8 @@ public static Object convert(Object value, Class type, ClickHouseColumn colum if (column != null && column.getArrayBaseColumn() != null) { ClickHouseDataType baseType = column.getArrayBaseColumn().getDataType(); - Object[] convertedValues = convertArray(arrayValue.getArrayOfObjects(), convertToJavaClass(baseType)); + Object[] convertedValues = convertArray(arrayValue.getArray(), convertToJavaClass(baseType), + column.getArrayNestedLevel()); return new Array(column, convertedValues); } @@ -359,16 +361,12 @@ public static T[] convertList(List values, Class type, int dimensions) if (values == null) { return null; } - if (values.isEmpty()) { - return (T[]) java.lang.reflect.Array.newInstance(type, 0); - } - int[] arrayDimensions = new int[dimensions]; arrayDimensions[0] = values.size(); T[] convertedValues = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions); Stack stack = new Stack<>(); - stack.push(new ArrayProcessingCursor(convertedValues, values, 0, values.size())); + stack.push(new ArrayProcessingCursor(convertedValues, values, values.size())); while (!stack.isEmpty()) { ArrayProcessingCursor cursor = stack.pop(); @@ -382,7 +380,7 @@ public static T[] convertList(List values, Class type, int dimensions) arrayDimensions = new int[Math.max(dimensions - stack.size() - 1, 1)]; arrayDimensions[0] = srcList.size(); T[] targetArray = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions); - stack.push(new ArrayProcessingCursor(targetArray, value, 0, srcList.size())); + stack.push(new ArrayProcessingCursor(targetArray, value, srcList.size())); java.lang.reflect.Array.set(cursor.targetArray, i, targetArray); } else { java.lang.reflect.Array.set(cursor.targetArray, i, convert(value, type)); @@ -393,28 +391,55 @@ public static T[] convertList(List values, Class type, int dimensions) return convertedValues; } - public static T[] convertArray(Object[] values, Class type) throws SQLException { + /** + * Convert array to java array and all its elements + * @param values + * @param type + * @param dimensions + * @return + * @param + * @throws SQLException + */ + public static T[] convertArray(Object values, Class type, int dimensions) throws SQLException { if (values == null) { return null; } - T[] convertedValues = (T[]) java.lang.reflect.Array.newInstance(type, values.length); - for (int i = 0; i < values.length; i++) { - convertedValues[i] = (T) convert(values[i], type); + + int[] arrayDimensions = new int[dimensions]; + arrayDimensions[0] = java.lang.reflect.Array.getLength(values); + T[] convertedValues = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions); + Stack stack = new Stack<>(); + stack.push(new ArrayProcessingCursor(convertedValues, values, arrayDimensions[0])); + + 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.getClass().isArray()) { + arrayDimensions = new int[Math.max(dimensions - stack.size() - 1, 1)]; + arrayDimensions[0] = java.lang.reflect.Array.getLength(value); + T[] targetArray = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions); + stack.push(new ArrayProcessingCursor(targetArray, value, arrayDimensions[0])); + java.lang.reflect.Array.set(cursor.targetArray, i, targetArray); + } else { + java.lang.reflect.Array.set(cursor.targetArray, i, convert(value, type)); + } + } } + return convertedValues; } private static final class ArrayProcessingCursor { private final Object targetArray; - private final Object srcArray; - private final int pos; private final int size; private final Function valueGetter; - public ArrayProcessingCursor(Object targetArray, Object srcArray, int pos, int size) { + public ArrayProcessingCursor(Object targetArray, Object srcArray, int size) { this.targetArray = targetArray; - this.srcArray = srcArray; - this.pos = pos; this.size = size; if (srcArray instanceof List) { List list = (List) srcArray; diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java index a6be0b82f..3b64a1fe4 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java @@ -50,16 +50,19 @@ public void testConvertToArray() throws Exception { @Test(groups = {"unit"}) public void testConvertArray() throws Exception { // primitive classes are unwrapped - assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Boolean.class), new Boolean[] { false, true }); - assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Byte.class), new Byte[] { 0, 1 }); - assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Short.class), new Short[] { 0, 1 }); - assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Integer.class), new Integer[] { 0, 1 }); - assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Long.class), new Long[] { 0L, 1L }); - assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Float.class), new Float[] { 0.0f, 1.0f }); - assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Double.class), new Double[] { 0.0, 1.0 }); - assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, String.class), new String[] { "0", "1" }); - assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, BigDecimal.class), new BigDecimal[] { BigDecimal.valueOf(0), BigDecimal.valueOf(1) }); - assertNull(JdbcUtils.convertArray(null, Integer.class)); + assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Boolean.class, 1), new Boolean[] { false, true }); + assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Byte.class, 1), new Byte[] { 0, 1 }); + assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Short.class, 1), new Short[] { 0, 1 }); + assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Integer.class, 1), new Integer[] { 0, 1 }); + assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Long.class, 1), new Long[] { 0L, 1L }); + assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Float.class, 1), new Float[] { 0.0f, 1.0f }); + assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, Double.class, 1), new Double[] { 0.0, 1.0 }); + assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, String.class, 1), new String[] { "0", "1" }); + assertEquals(JdbcUtils.convertArray(new Object[] { 0, 1 }, BigDecimal.class, 1), new BigDecimal[] { BigDecimal.valueOf(0), BigDecimal.valueOf(1) }); + assertEquals(JdbcUtils.convertArray(new Object[][] { new Object[] {1, 2, 3}, new Object[] { 4, 5, 6} }, String.class, 2), + new String[][] { new String[] {"1", "2", "3"}, new String[] {"4", "5", "6"} }); + + assertNull(JdbcUtils.convertArray(null, Integer.class, 1)); }