Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
import com.clickhouse.client.ClickHouseServerForTest;
import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.ClientException;
import com.clickhouse.client.api.DataTypeUtils;
import com.clickhouse.client.api.command.CommandSettings;
import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader;
import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader;
import com.clickhouse.client.api.data_formats.internal.SerializerUtils;
import com.clickhouse.client.api.enums.Protocol;
import com.clickhouse.client.api.insert.InsertSettings;
import com.clickhouse.client.api.metadata.TableSchema;
Expand All @@ -34,19 +32,13 @@
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Connection;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -1145,6 +1137,147 @@ public void testDates() throws Exception {
}
}

@Test(groups = {"integration"}, dataProvider = "testNestedArrays_dp")
public void testNestedArrays(String columnDef, String insertValues, String[] expectedStrValues,
String[] expectedListValues) throws Exception {
final String table = "test_nested_arrays";
client.execute("DROP TABLE IF EXISTS " + table).get();
client.execute(tableDefinition(table, "rowId Int32", columnDef)).get();

client.execute("INSERT INTO " + table + " VALUES " + insertValues).get().close();

List<GenericRecord> records = client.queryAll("SELECT * FROM " + table + " ORDER BY rowId");
Assert.assertEquals(records.size(), expectedStrValues.length);

for (GenericRecord record : records) {
int rowId = record.getInteger("rowId");

// Check getString() - includes quotes for string values
String actualValue = record.getString("arr");
Assert.assertEquals(actualValue, expectedStrValues[rowId - 1],
"getString() mismatch at row " + rowId + " for " + columnDef);

// Check getObject() - should return an ArrayValue
Object objValue = record.getObject("arr");
Assert.assertNotNull(objValue, "getObject() returned null at row " + rowId);
Assert.assertTrue(objValue instanceof BinaryStreamReader.ArrayValue,
"getObject() should return ArrayValue at row " + rowId + ", got: " + objValue.getClass().getName());
BinaryStreamReader.ArrayValue arrayValue = (BinaryStreamReader.ArrayValue) objValue;
Assert.assertEquals(arrayValue.asList().toString(), expectedListValues[rowId - 1],
"getObject().asList() mismatch at row " + rowId + " for " + columnDef);

// Check getList() - should return a List representation (no quotes for strings)
List<?> listValue = record.getList("arr");
Assert.assertNotNull(listValue, "getList() returned null at row " + rowId);
Assert.assertEquals(listValue.toString(), expectedListValues[rowId - 1],
"getList() mismatch at row " + rowId + " for " + columnDef);
}
}

@DataProvider
public Object[][] testNestedArrays_dp() {
return new Object[][] {
// 2D arrays of integers - Array(Array(Int32))
{
"arr Array(Array(Int32))",
"(1, [[1, 2], [3, 4]]), (2, [[5, 6, 7]]), (3, [[]]), (4, [[8], [], [9, 10]]), " +
"(5, [[11]]), (6, [[12, 13], [14, 15]]), (7, [[100, 200]]), (8, [[16]]), (9, [[17, 18]]), (10, [[19, 20, 21]])",
new String[] {
"[[1, 2], [3, 4]]", "[[5, 6, 7]]", "[[]]", "[[8], [], [9, 10]]",
"[[11]]", "[[12, 13], [14, 15]]", "[[100, 200]]", "[[16]]", "[[17, 18]]", "[[19, 20, 21]]"
},
new String[] {
"[[1, 2], [3, 4]]", "[[5, 6, 7]]", "[[]]", "[[8], [], [9, 10]]",
"[[11]]", "[[12, 13], [14, 15]]", "[[100, 200]]", "[[16]]", "[[17, 18]]", "[[19, 20, 21]]"
}
},
// 2D arrays of strings - Array(Array(String))
{
"arr Array(Array(String))",
"(1, [['a', 'b'], ['c', 'd']]), (2, [['hello', 'world']]), (3, [[]]), (4, [['x'], [], ['y', 'z']]), " +
"(5, [['test']]), (6, [['foo', 'bar']]), (7, [['single']]), (8, [['alpha', 'beta']]), (9, [['one']]), (10, [['end']])",
new String[] { // getString() format - with quotes
"[['a', 'b'], ['c', 'd']]", "[['hello', 'world']]", "[[]]", "[['x'], [], ['y', 'z']]",
"[['test']]", "[['foo', 'bar']]", "[['single']]", "[['alpha', 'beta']]", "[['one']]", "[['end']]"
},
new String[] { // getList() format - no quotes
"[[a, b], [c, d]]", "[[hello, world]]", "[[]]", "[[x], [], [y, z]]",
"[[test]]", "[[foo, bar]]", "[[single]]", "[[alpha, beta]]", "[[one]]", "[[end]]"
}
},
// 3D arrays of integers - Array(Array(Array(Int32)))
{
"arr Array(Array(Array(Int32)))",
"(1, [[[1, 2], [3]]]), (2, [[[4], [5, 6]], [[7]]]), (3, [[[]]]), (4, [[[8, 9]]]), " +
"(5, [[[10], [11, 12]]]), (6, [[[13]]]), (7, [[[14, 15], [16]]]), (8, [[[17]]]), (9, [[[18, 19]]]), (10, [[[]]])",
new String[] {
"[[[1, 2], [3]]]", "[[[4], [5, 6]], [[7]]]", "[[[]]]", "[[[8, 9]]]",
"[[[10], [11, 12]]]", "[[[13]]]", "[[[14, 15], [16]]]", "[[[17]]]", "[[[18, 19]]]", "[[[]]]"
},
new String[] {
"[[[1, 2], [3]]]", "[[[4], [5, 6]], [[7]]]", "[[[]]]", "[[[8, 9]]]",
"[[[10], [11, 12]]]", "[[[13]]]", "[[[14, 15], [16]]]", "[[[17]]]", "[[[18, 19]]]", "[[[]]]"
}
},
// 2D arrays of floats - Array(Array(Float64))
{
"arr Array(Array(Float64))",
"(1, [[1.1, 2.2], [3.3]]), (2, [[4.4]]), (3, [[5.5, 6.6, 7.7]]), (4, [[]]), " +
"(5, [[8.8]]), (6, [[9.9, 10.1]]), (7, [[11.2]]), (8, [[12.3, 13.4]]), (9, [[14.5]]), (10, [[15.6, 16.7]])",
new String[] {
"[[1.1, 2.2], [3.3]]", "[[4.4]]", "[[5.5, 6.6, 7.7]]", "[[]]",
"[[8.8]]", "[[9.9, 10.1]]", "[[11.2]]", "[[12.3, 13.4]]", "[[14.5]]", "[[15.6, 16.7]]"
},
new String[] {
"[[1.1, 2.2], [3.3]]", "[[4.4]]", "[[5.5, 6.6, 7.7]]", "[[]]",
"[[8.8]]", "[[9.9, 10.1]]", "[[11.2]]", "[[12.3, 13.4]]", "[[14.5]]", "[[15.6, 16.7]]"
}
},
// 3D arrays of strings - Array(Array(Array(String)))
{
"arr Array(Array(Array(String)))",
"(1, [[['a', 'b']]]), (2, [[['c'], ['d', 'e']]]), (3, [[[]]]), (4, [[['f']]]), " +
"(5, [[['g', 'h']]]), (6, [[['i']]]), (7, [[['a', 'b'], ['c']], [['d', 'e', 'f']]]), (8, [[[]]]), (9, [[['m']]]), (10, [[['n', 'o']]])",
new String[] { // getString() format - with quotes
"[[['a', 'b']]]", "[[['c'], ['d', 'e']]]", "[[[]]]", "[[['f']]]",
"[[['g', 'h']]]", "[[['i']]]", "[[['a', 'b'], ['c']], [['d', 'e', 'f']]]", "[[[]]]", "[[['m']]]", "[[['n', 'o']]]"
},
new String[] { // getList() format - no quotes
"[[[a, b]]]", "[[[c], [d, e]]]", "[[[]]]", "[[[f]]]",
"[[[g, h]]]", "[[[i]]]", "[[[a, b], [c]], [[d, e, f]]]", "[[[]]]", "[[[m]]]", "[[[n, o]]]"
}
},
// 2D arrays of nullable integers - Array(Array(Nullable(Int32)))
{
"arr Array(Array(Nullable(Int32)))",
"(1, [[1, NULL, 2]]), (2, [[NULL]]), (3, [[3, 4, NULL]]), (4, [[]]), " +
"(5, [[NULL, NULL]]), (6, [[5]]), (7, [[6, NULL]]), (8, [[NULL, 7]]), (9, [[8, 9]]), (10, [[NULL]])",
new String[] {
"[[1, NULL, 2]]", "[[NULL]]", "[[3, 4, NULL]]", "[[]]",
"[[NULL, NULL]]", "[[5]]", "[[6, NULL]]", "[[NULL, 7]]", "[[8, 9]]", "[[NULL]]"
},
new String[] {
"[[1, null, 2]]", "[[null]]", "[[3, 4, null]]", "[[]]",
"[[null, null]]", "[[5]]", "[[6, null]]", "[[null, 7]]", "[[8, 9]]", "[[null]]"
}
},
// 4D arrays of integers - Array(Array(Array(Array(Int32))))
{
"arr Array(Array(Array(Array(Int32))))",
"(1, [[[[1, 2]]]]), (2, [[[[3], [4, 5]]]]), (3, [[[[]]]]), (4, [[[[6]]]]), " +
"(5, [[[[7, 8]]]]), (6, [[[[9]]]]), (7, [[[[10, 11]]]]), (8, [[[[]]]]), (9, [[[[12]]]]), (10, [[[[13, 14]]]])",
new String[] {
"[[[[1, 2]]]]", "[[[[3], [4, 5]]]]", "[[[[]]]]", "[[[[6]]]]",
"[[[[7, 8]]]]", "[[[[9]]]]", "[[[[10, 11]]]]", "[[[[]]]]", "[[[[12]]]]", "[[[[13, 14]]]]"
},
new String[] {
"[[[[1, 2]]]]", "[[[[3], [4, 5]]]]", "[[[[]]]]", "[[[[6]]]]",
"[[[[7, 8]]]]", "[[[[9]]]]", "[[[[10, 11]]]]", "[[[[]]]]", "[[[[12]]]]", "[[[[13, 14]]]]"
}
}
};
}

public static String tableDefinition(String table, String... columns) {
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE " + table + " ( ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,12 +407,15 @@ public static <T> T[] convertList(List<?> values, Class<T> type, int dimensions)
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 = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions);
Stack<ArrayProcessingCursor> stack = new Stack<>();
stack.push(new ArrayProcessingCursor(convertedValues, values, values.size()));

stack.push(new ArrayProcessingCursor(convertedValues, values, values.size(), dimensions));
while (!stack.isEmpty()) {
ArrayProcessingCursor cursor = stack.pop();

Expand All @@ -422,10 +425,14 @@ public static <T> T[] convertList(List<?> values, Class<T> type, int dimensions)
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)];
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()));
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));
Expand All @@ -450,11 +457,15 @@ public static <T> T[] convertArray(Object values, Class<T> type, int dimensions)
return null;
}

if (dimensions <= 0) {
throw new IllegalArgumentException("Cannot convert list to array with less then 1D");
}

int[] arrayDimensions = new int[dimensions];
arrayDimensions[0] = java.lang.reflect.Array.getLength(values);
T[] convertedValues = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions);
Stack<ArrayProcessingCursor> stack = new Stack<>();
stack.push(new ArrayProcessingCursor(convertedValues, values, arrayDimensions[0]));
stack.push(new ArrayProcessingCursor(convertedValues, values, arrayDimensions[0], dimensions));

while (!stack.isEmpty()) {
ArrayProcessingCursor cursor = stack.pop();
Expand All @@ -464,10 +475,14 @@ public static <T> T[] convertArray(Object values, Class<T> type, int dimensions)
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)];
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] = java.lang.reflect.Array.getLength(value);
T[] targetArray = (T[]) java.lang.reflect.Array.newInstance(type, arrayDimensions);
stack.push(new ArrayProcessingCursor(targetArray, value, arrayDimensions[0]));
stack.push(new ArrayProcessingCursor(targetArray, value, arrayDimensions[0], depth));
java.lang.reflect.Array.set(cursor.targetArray, i, targetArray);
} else {
java.lang.reflect.Array.set(cursor.targetArray, i, convert(value, type));
Expand All @@ -482,10 +497,12 @@ private static final class ArrayProcessingCursor {
private final Object targetArray;
private final int size;
private final Function<Integer, Object> valueGetter;
private final int depth;

public ArrayProcessingCursor(Object targetArray, Object srcArray, int size) {
public ArrayProcessingCursor(Object targetArray, Object srcArray, int size, int depth) {
this.targetArray = targetArray;
this.size = size;
this.depth = depth;
if (srcArray instanceof List<?>) {
List<?> list = (List<?>) srcArray;
this.valueGetter = list::get;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1219,11 +1219,6 @@ public void testNestedArrays() throws Exception {
}
}

/**
* Test for https://github.com/ClickHouse/clickhouse-java/issues/2723
* getString() on nested arrays was failing with NullPointerException due to re-entrancy bug
* in DataTypeConverter when converting nested arrays to string representation.
*/
@Test(groups = { "integration" })
public void testNestedArrayToString() throws SQLException {
// Test 1: Simple nested array - getString on Array(Array(Int32))
Expand Down Expand Up @@ -1273,6 +1268,8 @@ public void testNestedArrayToString() throws SQLException {
assertTrue(rs.next());
String result = rs.getString("deep_nested");
assertEquals(result, "[[['a', 'b'], ['c']], [['d', 'e', 'f']]]");
Array arr = rs.getArray(1);
assertTrue(Arrays.deepEquals((String[][][])arr.getArray(), new String[][][] {{{"a", "b"}, {"c"}}, {{ "d", "e", "f"}}}));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import com.clickhouse.data.ClickHouseColumn;
import com.clickhouse.data.ClickHouseDataType;
import org.testng.Assert;
import org.testng.annotations.*;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.math.BigDecimal;
import java.sql.SQLException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.testng.Assert.assertEquals;
Expand Down Expand Up @@ -65,6 +67,9 @@ public void testConvertArray() throws Exception {
new String[][] { new String[] {"1", "2", "3"}, new String[] {"4", "5", "6"} });

assertNull(JdbcUtils.convertArray(null, Integer.class, 1));

Assert.assertThrows(IllegalArgumentException.class, () -> JdbcUtils.convertArray(new Object[0], String.class, 0 ));
Assert.assertThrows(IllegalStateException.class, () -> JdbcUtils.convertArray(new Object[] { new Object[] { 1, 2, 3}}, String.class, 1 ));
}


Expand All @@ -79,6 +84,9 @@ public void testConvertList() throws Exception {
assertEquals(dst[2], src.get(2));

assertNull(JdbcUtils.convertList(null, Integer.class, 1));

Assert.assertThrows(IllegalArgumentException.class, () -> JdbcUtils.convertList(Collections.emptyList(), String.class, 0));
Assert.assertThrows(IllegalStateException.class, () -> JdbcUtils.convertList(Arrays.asList(Collections.singletonList(1)), String.class, 1));
}


Expand Down
Loading