diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java index df6979412..51e1b1df0 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java @@ -240,74 +240,75 @@ public interface ClickHouseBinaryFormatReader extends AutoCloseable { ClickHouseGeoMultiPolygonValue getGeoMultiPolygon(String colName); /** - * Reads column with name `colName` as a string. - * + * @see #getList(int) * @param colName - column name - * @return + * @return list of values, or {@code null} if the value is null */ List getList(String colName); /** - * Reads column with name `colName` as a string. - * + * @see #getByteArray(int) * @param colName - column name - * @return + * @return array of bytes, or {@code null} if the value is null */ byte[] getByteArray(String colName); /** - * Reads column with name `colName` as a string. - * + * @see #getIntArray(int) * @param colName - column name - * @return + * @return array of int values, or {@code null} if the value is null */ int[] getIntArray(String colName); /** - * Reads column with name `colName` as a string. - * + * @see #getLongArray(int) * @param colName - column name - * @return + * @return array of long values, or {@code null} if the value is null */ long[] getLongArray(String colName); /** - * Reads column with name `colName` as a string. - * + * @see #getFloatArray(int) * @param colName - column name - * @return + * @return array of float values, or {@code null} if the value is null */ float[] getFloatArray(String colName); /** - * Reads column with name `colName` as a string. - * + * @see #getDoubleArray(int) * @param colName - column name - * @return + * @return array of double values, or {@code null} if the value is null */ double[] getDoubleArray(String colName); /** - * - * @param colName - * @return + * @see #getBooleanArray(int) + * @param colName - column name + * @return array of boolean values, or {@code null} if the value is null */ boolean[] getBooleanArray(String colName); /** - * - * @param colName - * @return + * @see #getShortArray(int) + * @param colName - column name + * @return array of short values, or {@code null} if the value is null */ short[] getShortArray(String colName); /** - * - * @param colName - * @return + * @see #getStringArray(int) + * @param colName - column name + * @return array of string values, or {@code null} if the value is null */ String[] getStringArray(String colName); + /** + * @see #getObjectArray(int) + * @param colName - column name + * @return array of objects, or {@code null} if the value is null + */ + Object[] getObjectArray(String colName); + /** * Reads column with name `colName` as a string. * @@ -483,59 +484,100 @@ public interface ClickHouseBinaryFormatReader extends AutoCloseable { ClickHouseGeoMultiPolygonValue getGeoMultiPolygon(int index); /** - * Reads column with name `colName` as a string. + * Returns the value of the specified column as a {@link List}. Suitable for reading Array columns of any type. + *

For nested arrays (e.g. {@code Array(Array(Int64))}), returns a {@code List>}. + * For nullable arrays (e.g. {@code Array(Nullable(Int32))}), list elements may be {@code null}.

* - * @param index - column name - * @return + * @param index - column index (1-based) + * @return list of values, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the column is not an array type */ List getList(int index); /** - * Reads column with name `colName` as a string. + * Returns the value of the specified column as a {@code byte[]}. Suitable for 1D Array columns only. * - * @param index - column name - * @return + * @param index - column index (1-based) + * @return array of bytes, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the value cannot be converted to a byte array */ byte[] getByteArray(int index); /** - * Reads column with name `colName` as a string. + * Returns the value of the specified column as an {@code int[]}. Suitable for 1D Array columns only. * - * @param index - column name - * @return + * @param index - column index (1-based) + * @return array of int values, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the value cannot be converted to an int array */ int[] getIntArray(int index); /** - * Reads column with name `colName` as a string. + * Returns the value of the specified column as a {@code long[]}. Suitable for 1D Array columns only. * - * @param index - column name - * @return + * @param index - column index (1-based) + * @return array of long values, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the value cannot be converted to a long array */ long[] getLongArray(int index); /** - * Reads column with name `colName` as a string. + * Returns the value of the specified column as a {@code float[]}. Suitable for 1D Array columns only. * - * @param index - column name - * @return + * @param index - column index (1-based) + * @return array of float values, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the value cannot be converted to a float array */ float[] getFloatArray(int index); /** - * Reads column with name `colName` as a string. + * Returns the value of the specified column as a {@code double[]}. Suitable for 1D Array columns only. * - * @param index - column name - * @return + * @param index - column index (1-based) + * @return array of double values, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the value cannot be converted to a double array */ double[] getDoubleArray(int index); + /** + * Returns the value of the specified column as a {@code boolean[]}. Suitable for 1D Array columns only. + * + * @param index - column index (1-based) + * @return array of boolean values, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the value cannot be converted to a boolean array + */ boolean[] getBooleanArray(int index); - short [] getShortArray(int index); + /** + * Returns the value of the specified column as a {@code short[]}. Suitable for 1D Array columns only. + * + * @param index - column index (1-based) + * @return array of short values, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the value cannot be converted to a short array + */ + short[] getShortArray(int index); + /** + * Returns the value of the specified column as a {@code String[]}. Suitable for 1D Array columns only. + * Cannot be used for none string element types. + * + * @param index - column index (1-based) + * @return array of string values, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the column is not an array type + */ String[] getStringArray(int index); + /** + * Returns the value of the specified column as an {@code Object[]}. Suitable for multidimensional Array columns. + * Nested arrays are recursively converted to {@code Object[]}. + * Note: result is not cached so avoid repetitive calls on same column. + * + * @param index - column index (1-based) + * @return array of objects, or {@code null} if the value is null + * @throws com.clickhouse.client.api.ClientException if the column is not an array type + */ + Object[] getObjectArray(int index); + Object[] getTuple(int index); Object[] getTuple(String colName); diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 2ebf2ffa0..23b05c983 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -562,6 +562,11 @@ public String[] getStringArray(String colName) { return getStringArray(schema.nameToColumnIndex(colName)); } + @Override + public Object[] getObjectArray(String colName) { + return getObjectArray(schema.nameToColumnIndex(colName)); + } + @Override public boolean hasValue(int colIndex) { return currentRecord[colIndex - 1] != null; @@ -816,16 +821,30 @@ public String[] getStringArray(int index) { } if (value instanceof BinaryStreamReader.ArrayValue) { BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) value; - int length = array.length; - if (!array.itemType.equals(String.class)) - throw new ClientException("Not A String type."); - String[] values = new String[length]; - for (int i = 0; i < length; i++) { - values[i] = (String)((BinaryStreamReader.ArrayValue) value).get(i); + if (array.itemType == String.class) { + return (String[]) array.getArray(); + } else if (array.itemType == BinaryStreamReader.EnumValue.class) { + BinaryStreamReader.EnumValue[] enumValues = (BinaryStreamReader.EnumValue[]) array.getArray(); + return Arrays.stream(enumValues).map(BinaryStreamReader.EnumValue::getName).toArray(String[]::new); + } else { + throw new ClientException("Not an array of strings"); } - return values; } - throw new ClientException("Not ArrayValue type."); + throw new ClientException("Column is not of array type"); + } + + @Override + public Object[] getObjectArray(int index) { + Object value = readValue(index); + if (value == null) { + return null; + } + if (value instanceof BinaryStreamReader.ArrayValue) { + return ((BinaryStreamReader.ArrayValue) value).toObjectArray(); + } else if (value instanceof List) { + return ((List) value).toArray(new Object[0]); + } + throw new ClientException("Column is not of array type"); } @Override diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecord.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecord.java index e06b5225a..7faa8e0fe 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecord.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecord.java @@ -175,6 +175,11 @@ public String[] getStringArray(String colName) { return reader.getStringArray(colName); } + @Override + public Object[] getObjectArray(String colName) { + return reader.getObjectArray(colName); + } + @Override public String getString(int index) { return reader.getString(index); @@ -335,6 +340,11 @@ public String[] getStringArray(int index) { return reader.getStringArray(index); } + @Override + public Object[] getObjectArray(int index) { + return reader.getObjectArray(index); + } + @Override public Object[] getTuple(int index) { return reader.getTuple(index); diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java index d0ba7eb95..2f43eb92b 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java @@ -752,6 +752,23 @@ public Object[] getArrayOfObjects() { return (Object[]) array; } } + + /** + * Returns array of objects, recursively converting nested ArrayValue elements to Object[]. + * This is useful for nested arrays (e.g. Array(Array(Int64))) where elements are ArrayValue instances. + * + * @return Object[] with nested ArrayValue elements converted to Object[] + */ + public Object[] toObjectArray() { + Object[] result = new Object[length]; + for (int i = 0; i < length; i++) { + Object item = get(i); + result[i] = (item instanceof ArrayValue) ? ((ArrayValue) item).toObjectArray() : item; + } + return result; + } + + } public static class EnumValue extends Number { @@ -785,6 +802,10 @@ public double doubleValue() { return value; } + public String getName() { + return name; + } + @Override public String toString() { return name; diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/MapBackedRecord.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/MapBackedRecord.java index 35d138846..07e65de39 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/MapBackedRecord.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/MapBackedRecord.java @@ -25,6 +25,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.TemporalAmount; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -295,16 +296,16 @@ public String[] getStringArray(String colName) { } if (value instanceof BinaryStreamReader.ArrayValue) { BinaryStreamReader.ArrayValue array = (BinaryStreamReader.ArrayValue) value; - int length = array.length; - if (!array.itemType.equals(String.class)) - throw new ClientException("Not A String type."); - String [] values = new String[length]; - for (int i = 0; i < length; i++) { - values[i] = (String)((BinaryStreamReader.ArrayValue) value).get(i); + if (array.itemType == String.class) { + return (String[]) array.getArray(); + } else if (array.itemType == BinaryStreamReader.EnumValue.class) { + BinaryStreamReader.EnumValue[] enumValues = (BinaryStreamReader.EnumValue[]) array.getArray(); + return Arrays.stream(enumValues).map(BinaryStreamReader.EnumValue::getName).toArray(String[]::new); + } else { + throw new ClientException("Not an array of strings"); } - return values; } - throw new ClientException("Not ArrayValue type."); + throw new ClientException("Column is not of array type"); } @Override @@ -477,7 +478,26 @@ public short[] getShortArray(int index) { @Override public String[] getStringArray(int index) { - return getPrimitiveArray(schema.columnIndexToName(index)); + return getStringArray(schema.columnIndexToName(index)); + } + + @Override + public Object[] getObjectArray(String colName) { + Object value = readValue(colName); + if (value == null) { + return null; + } + if (value instanceof BinaryStreamReader.ArrayValue) { + return ((BinaryStreamReader.ArrayValue) value).toObjectArray(); + } else if (value instanceof List) { + return ((List) value).toArray(new Object[0]); + } + throw new ClientException("Column is not of array type"); + } + + @Override + public Object[] getObjectArray(int index) { + return getObjectArray(schema.columnIndexToName(index)); } @Override diff --git a/client-v2/src/main/java/com/clickhouse/client/api/query/GenericRecord.java b/client-v2/src/main/java/com/clickhouse/client/api/query/GenericRecord.java index e50dc82ee..40ae7b87f 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/query/GenericRecord.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/query/GenericRecord.java @@ -248,8 +248,25 @@ public interface GenericRecord { short[] getShortArray(String colName); + /** + * Returns string array for columns {@code Array(String)}. + * This method doesn't make a conversion of other types to string. + * + * @param colName - column name + * @return String[] + */ String[] getStringArray(String colName); + /** + * Reads column with name `colName` as an array of objects. Works for any array element type + * including non-primitive types like DateTime, Enum, UInt64 (BigInteger), etc. + * For nested arrays, inner ArrayValue elements are recursively converted to Object[]. + * + * @param colName - column name + * @return array of objects or null if value is null + */ + Object[] getObjectArray(String colName); + /** * Reads column with name `colName` as a string. * @@ -490,6 +507,16 @@ public interface GenericRecord { String[] getStringArray(int index); + /** + * Reads column at the specified index as an array of objects. Works for any array element type + * including non-primitive types like DateTime, Enum, UInt64 (BigInteger), FixedString, etc. + * For nested arrays, inner ArrayValue elements are recursively converted to Object[]. + * + * @param index - column index (1-based) + * @return array of objects or null if value is null + */ + Object[] getObjectArray(int index); + Object[] getTuple(int index); Object[] getTuple(String colName); diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReaderTest.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReaderTest.java index f6206a3a4..55f4a2318 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReaderTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReaderTest.java @@ -12,6 +12,8 @@ import java.io.InputStream; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.TimeZone; import java.util.function.Consumer; @@ -246,4 +248,332 @@ public void testReadingArrays() throws Exception { Assert.assertEquals(reader.getLongArray("a5"), new long[] {1L, 2L}); } + + @Test + public void testGetObjectArray1D() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + String[] names = new String[]{"uint64_arr", "enum_arr", "dt_arr", "fstr_arr", "str_arr"}; + String[] types = new String[]{ + "Array(UInt64)", + "Array(Enum8('abc' = 1, 'cde' = 2))", + "Array(DateTime('UTC'))", + "Array(FixedString(4))", + "Array(String)" + }; + + BinaryStreamUtils.writeVarInt(out, names.length); + for (String name : names) { + BinaryStreamUtils.writeString(out, name); + } + for (String type : types) { + BinaryStreamUtils.writeString(out, type); + } + + // Array(UInt64): [100, 200] + BinaryStreamUtils.writeVarInt(out, 2); + BinaryStreamUtils.writeUnsignedInt64(out, BigInteger.valueOf(100)); + BinaryStreamUtils.writeUnsignedInt64(out, BigInteger.valueOf(200)); + + // Array(Enum8('abc' = 1, 'cde' = 2)): [1, 2] + BinaryStreamUtils.writeVarInt(out, 2); + BinaryStreamUtils.writeEnum8(out, (byte) 1); + BinaryStreamUtils.writeEnum8(out, (byte) 2); + + // Array(DateTime('UTC')): two timestamps + LocalDateTime dt1 = LocalDateTime.of(2030, 10, 9, 8, 7, 6); + LocalDateTime dt2 = LocalDateTime.of(2031, 10, 9, 8, 7, 6); + BinaryStreamUtils.writeVarInt(out, 2); + BinaryStreamUtils.writeDateTime32(out, dt1, TimeZone.getTimeZone("UTC")); + BinaryStreamUtils.writeDateTime32(out, dt2, TimeZone.getTimeZone("UTC")); + + // Array(FixedString(4)): ["abcd", "efgh"] + BinaryStreamUtils.writeVarInt(out, 2); + BinaryStreamUtils.writeFixedString(out, "abcd", 4); + BinaryStreamUtils.writeFixedString(out, "efgh", 4); + + // Array(String): ["hello", "world"] + BinaryStreamUtils.writeVarInt(out, 2); + BinaryStreamUtils.writeString(out, "hello"); + BinaryStreamUtils.writeString(out, "world"); + + InputStream in = new ByteArrayInputStream(out.toByteArray()); + QuerySettings querySettings = new QuerySettings().setUseTimeZone("UTC"); + RowBinaryWithNamesAndTypesFormatReader reader = + new RowBinaryWithNamesAndTypesFormatReader(in, querySettings, new BinaryStreamReader.CachingByteBufferAllocator()); + reader.next(); + + // Test Array(UInt64) via getObjectArray + Object[] uint64Result = reader.getObjectArray("uint64_arr"); + Assert.assertNotNull(uint64Result); + Assert.assertEquals(uint64Result.length, 2); + Assert.assertEquals(uint64Result[0], BigInteger.valueOf(100)); + Assert.assertEquals(uint64Result[1], BigInteger.valueOf(200)); + + // Test Array(Enum8) via getObjectArray + Object[] enumResult = reader.getObjectArray("enum_arr"); + Assert.assertNotNull(enumResult); + Assert.assertEquals(enumResult.length, 2); + Assert.assertTrue(enumResult[0] instanceof BinaryStreamReader.EnumValue); + Assert.assertEquals(enumResult[0].toString(), "abc"); + Assert.assertEquals(enumResult[1].toString(), "cde"); + + // Test Array(Enum8) via getStringArray (sugar) + String[] enumStrings = reader.getStringArray("enum_arr"); + Assert.assertEquals(enumStrings, new String[]{"abc", "cde"}); + + // Test Array(DateTime) via getObjectArray + Object[] dtResult = reader.getObjectArray("dt_arr"); + Assert.assertNotNull(dtResult); + Assert.assertEquals(dtResult.length, 2); + Assert.assertTrue(dtResult[0] instanceof ZonedDateTime); + ZonedDateTime zdt1 = (ZonedDateTime) dtResult[0]; + ZonedDateTime zdt2 = (ZonedDateTime) dtResult[1]; + Assert.assertEquals(zdt1.toLocalDateTime(), dt1); + Assert.assertEquals(zdt2.toLocalDateTime(), dt2); + + // Test Array(FixedString) via getObjectArray + Object[] fstrResult = reader.getObjectArray("fstr_arr"); + Assert.assertNotNull(fstrResult); + Assert.assertEquals(fstrResult.length, 2); + Assert.assertEquals(fstrResult[0], "abcd"); + Assert.assertEquals(fstrResult[1], "efgh"); + + // Test Array(String) via getObjectArray + Object[] strResult = reader.getObjectArray("str_arr"); + Assert.assertNotNull(strResult); + Assert.assertEquals(strResult.length, 2); + Assert.assertEquals(strResult[0], "hello"); + Assert.assertEquals(strResult[1], "world"); + + // Also verify getObjectArray works for primitive-backed arrays too + // (int arrays are still returned as boxed objects) + } + + @Test + public void testGetObjectArray2D() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + String[] names = new String[]{"arr2d_int", "arr2d_str"}; + String[] types = new String[]{"Array(Array(Int64))", "Array(Array(String))"}; + + BinaryStreamUtils.writeVarInt(out, names.length); + for (String name : names) { + BinaryStreamUtils.writeString(out, name); + } + for (String type : types) { + BinaryStreamUtils.writeString(out, type); + } + + // Array(Array(Int64)): [[1, 2, 3], [4, 5]] + BinaryStreamUtils.writeVarInt(out, 2); // outer array length + BinaryStreamUtils.writeVarInt(out, 3); // inner[0] length + BinaryStreamUtils.writeInt64(out, 1L); + BinaryStreamUtils.writeInt64(out, 2L); + BinaryStreamUtils.writeInt64(out, 3L); + BinaryStreamUtils.writeVarInt(out, 2); // inner[1] length + BinaryStreamUtils.writeInt64(out, 4L); + BinaryStreamUtils.writeInt64(out, 5L); + + // Array(Array(String)): [["a", "b"], ["c"]] + BinaryStreamUtils.writeVarInt(out, 2); // outer array length + BinaryStreamUtils.writeVarInt(out, 2); // inner[0] length + BinaryStreamUtils.writeString(out, "a"); + BinaryStreamUtils.writeString(out, "b"); + BinaryStreamUtils.writeVarInt(out, 1); // inner[1] length + BinaryStreamUtils.writeString(out, "c"); + + InputStream in = new ByteArrayInputStream(out.toByteArray()); + QuerySettings querySettings = new QuerySettings().setUseTimeZone("UTC"); + RowBinaryWithNamesAndTypesFormatReader reader = + new RowBinaryWithNamesAndTypesFormatReader(in, querySettings, new BinaryStreamReader.CachingByteBufferAllocator()); + reader.next(); + + // Test 2D int array + Object[] arr2dInt = reader.getObjectArray("arr2d_int"); + Assert.assertNotNull(arr2dInt); + Assert.assertEquals(arr2dInt.length, 2); + + // Inner arrays should be Object[] (recursively converted) + Assert.assertTrue(arr2dInt[0] instanceof Object[]); + Assert.assertTrue(arr2dInt[1] instanceof Object[]); + + Object[] inner0 = (Object[]) arr2dInt[0]; + Object[] inner1 = (Object[]) arr2dInt[1]; + Assert.assertEquals(inner0.length, 3); + Assert.assertEquals(inner0[0], 1L); + Assert.assertEquals(inner0[1], 2L); + Assert.assertEquals(inner0[2], 3L); + Assert.assertEquals(inner1.length, 2); + Assert.assertEquals(inner1[0], 4L); + Assert.assertEquals(inner1[1], 5L); + + // Test 2D string array + Object[] arr2dStr = reader.getObjectArray("arr2d_str"); + Assert.assertNotNull(arr2dStr); + Assert.assertEquals(arr2dStr.length, 2); + Assert.assertTrue(arr2dStr[0] instanceof Object[]); + Assert.assertTrue(arr2dStr[1] instanceof Object[]); + + Object[] strInner0 = (Object[]) arr2dStr[0]; + Object[] strInner1 = (Object[]) arr2dStr[1]; + Assert.assertEquals(strInner0, new Object[]{"a", "b"}); + Assert.assertEquals(strInner1, new Object[]{"c"}); + } + + @Test + public void testGetObjectArray3D() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + String[] names = new String[]{"arr3d"}; + String[] types = new String[]{"Array(Array(Array(Int32)))"}; + + BinaryStreamUtils.writeVarInt(out, names.length); + for (String name : names) { + BinaryStreamUtils.writeString(out, name); + } + for (String type : types) { + BinaryStreamUtils.writeString(out, type); + } + + // Array(Array(Array(Int32))): [[[1, 2], [3]], [[4]]] + BinaryStreamUtils.writeVarInt(out, 2); // dim1 length = 2 + // dim1[0] = [[1, 2], [3]] + BinaryStreamUtils.writeVarInt(out, 2); // dim2 length = 2 + BinaryStreamUtils.writeVarInt(out, 2); // dim3 length = 2 + BinaryStreamUtils.writeInt32(out, 1); + BinaryStreamUtils.writeInt32(out, 2); + BinaryStreamUtils.writeVarInt(out, 1); // dim3 length = 1 + BinaryStreamUtils.writeInt32(out, 3); + // dim1[1] = [[4]] + BinaryStreamUtils.writeVarInt(out, 1); // dim2 length = 1 + BinaryStreamUtils.writeVarInt(out, 1); // dim3 length = 1 + BinaryStreamUtils.writeInt32(out, 4); + + InputStream in = new ByteArrayInputStream(out.toByteArray()); + QuerySettings querySettings = new QuerySettings().setUseTimeZone("UTC"); + RowBinaryWithNamesAndTypesFormatReader reader = + new RowBinaryWithNamesAndTypesFormatReader(in, querySettings, new BinaryStreamReader.CachingByteBufferAllocator()); + reader.next(); + + // Test 3D array: [[[1, 2], [3]], [[4]]] + Object[] arr3d = reader.getObjectArray("arr3d"); + Assert.assertNotNull(arr3d); + Assert.assertEquals(arr3d.length, 2); + + // dim1[0] = [[1, 2], [3]] + Assert.assertTrue(arr3d[0] instanceof Object[]); + Object[] dim1_0 = (Object[]) arr3d[0]; + Assert.assertEquals(dim1_0.length, 2); + + // dim1[0][0] = [1, 2] + Assert.assertTrue(dim1_0[0] instanceof Object[]); + Object[] dim2_0_0 = (Object[]) dim1_0[0]; + Assert.assertEquals(dim2_0_0.length, 2); + Assert.assertEquals(dim2_0_0[0], 1); + Assert.assertEquals(dim2_0_0[1], 2); + + // dim1[0][1] = [3] + Assert.assertTrue(dim1_0[1] instanceof Object[]); + Object[] dim2_0_1 = (Object[]) dim1_0[1]; + Assert.assertEquals(dim2_0_1.length, 1); + Assert.assertEquals(dim2_0_1[0], 3); + + // dim1[1] = [[4]] + Assert.assertTrue(arr3d[1] instanceof Object[]); + Object[] dim1_1 = (Object[]) arr3d[1]; + Assert.assertEquals(dim1_1.length, 1); + + Assert.assertTrue(dim1_1[0] instanceof Object[]); + Object[] dim2_1_0 = (Object[]) dim1_1[0]; + Assert.assertEquals(dim2_1_0.length, 1); + Assert.assertEquals(dim2_1_0[0], 4); + } + + @Test + public void testGetObjectArrayEmpty() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + String[] names = new String[]{"empty_arr"}; + String[] types = new String[]{"Array(Int32)"}; + + BinaryStreamUtils.writeVarInt(out, names.length); + for (String name : names) { + BinaryStreamUtils.writeString(out, name); + } + for (String type : types) { + BinaryStreamUtils.writeString(out, type); + } + + // Empty array + BinaryStreamUtils.writeVarInt(out, 0); + + InputStream in = new ByteArrayInputStream(out.toByteArray()); + QuerySettings querySettings = new QuerySettings().setUseTimeZone("UTC"); + RowBinaryWithNamesAndTypesFormatReader reader = + new RowBinaryWithNamesAndTypesFormatReader(in, querySettings, new BinaryStreamReader.CachingByteBufferAllocator()); + reader.next(); + + Object[] result = reader.getObjectArray("empty_arr"); + Assert.assertNotNull(result); + Assert.assertEquals(result.length, 0); + } + + @Test + public void testGetObjectArrayPrimitiveTypes() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + String[] names = new String[]{"int_arr", "bool_arr", "float_arr"}; + String[] types = new String[]{"Array(Int32)", "Array(Bool)", "Array(Float64)"}; + + BinaryStreamUtils.writeVarInt(out, names.length); + for (String name : names) { + BinaryStreamUtils.writeString(out, name); + } + for (String type : types) { + BinaryStreamUtils.writeString(out, type); + } + + // Array(Int32): [10, 20, 30] + BinaryStreamUtils.writeVarInt(out, 3); + BinaryStreamUtils.writeInt32(out, 10); + BinaryStreamUtils.writeInt32(out, 20); + BinaryStreamUtils.writeInt32(out, 30); + + // Array(Bool): [true, false] + BinaryStreamUtils.writeVarInt(out, 2); + BinaryStreamUtils.writeBoolean(out, true); + BinaryStreamUtils.writeBoolean(out, false); + + // Array(Float64): [1.5, 2.5] + BinaryStreamUtils.writeVarInt(out, 2); + BinaryStreamUtils.writeFloat64(out, 1.5); + BinaryStreamUtils.writeFloat64(out, 2.5); + + InputStream in = new ByteArrayInputStream(out.toByteArray()); + QuerySettings querySettings = new QuerySettings().setUseTimeZone("UTC"); + RowBinaryWithNamesAndTypesFormatReader reader = + new RowBinaryWithNamesAndTypesFormatReader(in, querySettings, new BinaryStreamReader.CachingByteBufferAllocator()); + reader.next(); + + // getObjectArray should work for primitive-backed arrays too (auto-boxes) + Object[] intResult = reader.getObjectArray("int_arr"); + Assert.assertNotNull(intResult); + Assert.assertEquals(intResult.length, 3); + Assert.assertEquals(intResult[0], 10); + Assert.assertEquals(intResult[1], 20); + Assert.assertEquals(intResult[2], 30); + + Object[] boolResult = reader.getObjectArray("bool_arr"); + Assert.assertNotNull(boolResult); + Assert.assertEquals(boolResult.length, 2); + Assert.assertEquals(boolResult[0], true); + Assert.assertEquals(boolResult[1], false); + + Object[] floatResult = reader.getObjectArray("float_arr"); + Assert.assertNotNull(floatResult); + Assert.assertEquals(floatResult.length, 2); + Assert.assertEquals(floatResult[0], 1.5); + Assert.assertEquals(floatResult[1], 2.5); + } } \ No newline at end of file diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/RowBinaryTest.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/RowBinaryTest.java index d5acebe10..3235e2ec2 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/RowBinaryTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/RowBinaryTest.java @@ -5,15 +5,21 @@ import com.clickhouse.client.ClickHouseProtocol; import com.clickhouse.client.ClickHouseServerForTest; import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; import com.clickhouse.client.api.enums.Protocol; +import com.clickhouse.client.api.metadata.TableSchema; import com.clickhouse.client.api.query.GenericRecord; +import com.clickhouse.client.api.query.QueryResponse; +import com.clickhouse.client.api.query.QuerySettings; +import com.clickhouse.data.ClickHouseFormat; import lombok.Data; import org.testng.Assert; import org.testng.annotations.Test; +import java.math.BigInteger; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.List; -import java.util.Random; @Test(groups = {"integration"}) public class RowBinaryTest extends BaseIntegrationTest { @@ -63,6 +69,287 @@ void testDefaultWithFunction() { } } + @Test(groups = {"integration"}) + void testGetObjectArray1D() { + final String table = "test_get_object_array_1d"; + try (Client client = newClient().build()) { + client.execute("DROP TABLE IF EXISTS " + table); + client.execute("CREATE TABLE " + table + " (" + + "uint64_arr Array(UInt64), " + + "enum_arr Array(Enum8('abc' = 1, 'cde' = 2)), " + + "dt_arr Array(DateTime('UTC')), " + + "fstr_arr Array(FixedString(4)), " + + "str_arr Array(String), " + + "int_arr Array(Int32)" + + ") ENGINE = MergeTree() ORDER BY tuple()"); + + client.execute("INSERT INTO " + table + " VALUES (" + + "[100, 200, 18000044073709551615], " + + "['abc', 'cde'], " + + "['2030-10-09 08:07:06', '2031-10-09 08:07:06'], " + + "['abcd', 'efgh'], " + + "['hello', 'world'], " + + "[100, 200, 65536])"); + + QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.RowBinary); + TableSchema schema = client.getTableSchema(table); + try (QueryResponse response = client.query("SELECT * FROM " + table, settings).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response, schema); + reader.next(); + + // Array(UInt64) -> BigInteger elements + Object[] uint64Arr = reader.getObjectArray("uint64_arr"); + Assert.assertNotNull(uint64Arr); + Assert.assertEquals(uint64Arr.length, 3); + Assert.assertEquals(uint64Arr[0], BigInteger.valueOf(100)); + Assert.assertEquals(uint64Arr[1], BigInteger.valueOf(200)); + Assert.assertEquals(uint64Arr[2], new BigInteger("18000044073709551615")); + + // Array(Enum8) -> EnumValue elements via getObjectArray + Object[] enumArr = reader.getObjectArray("enum_arr"); + Assert.assertNotNull(enumArr); + Assert.assertEquals(enumArr.length, 2); + Assert.assertTrue(enumArr[0] instanceof BinaryStreamReader.EnumValue); + Assert.assertEquals(enumArr[0].toString(), "abc"); + Assert.assertEquals(enumArr[1].toString(), "cde"); + + // Array(Enum8) -> String[] via getStringArray + String[] enumStrings = reader.getStringArray("enum_arr"); + Assert.assertEquals(enumStrings, new String[]{"abc", "cde"}); + + // Array(DateTime) -> ZonedDateTime elements + Object[] dtArr = reader.getObjectArray("dt_arr"); + Assert.assertNotNull(dtArr); + Assert.assertEquals(dtArr.length, 2); + Assert.assertTrue(dtArr[0] instanceof ZonedDateTime); + Assert.assertTrue(dtArr[1] instanceof ZonedDateTime); + ZonedDateTime zdt1 = (ZonedDateTime) dtArr[0]; + ZonedDateTime zdt2 = (ZonedDateTime) dtArr[1]; + Assert.assertEquals(zdt1.getYear(), 2030); + Assert.assertEquals(zdt1.getMonthValue(), 10); + Assert.assertEquals(zdt1.getDayOfMonth(), 9); + Assert.assertEquals(zdt2.getYear(), 2031); + + // Array(FixedString(4)) -> String elements + Object[] fstrArr = reader.getObjectArray("fstr_arr"); + Assert.assertNotNull(fstrArr); + Assert.assertEquals(fstrArr.length, 2); + Assert.assertEquals(fstrArr[0], "abcd"); + Assert.assertEquals(fstrArr[1], "efgh"); + + // Array(String) -> String elements + Object[] strArr = reader.getObjectArray("str_arr"); + Assert.assertNotNull(strArr); + Assert.assertEquals(strArr[0], "hello"); + Assert.assertEquals(strArr[1], "world"); + + // getStringArray should also work for FixedString arrays + String[] fstrStrings = reader.getStringArray("fstr_arr"); + Assert.assertEquals(fstrStrings, new String[]{"abcd", "efgh"}); + + // Array(Int32) + Object[] intArrObj = reader.getObjectArray("int_arr"); + Assert.assertEquals(intArrObj, new Integer[]{100, 200, 65536}); + + Assert.assertNull(reader.next(), "Expected only one row"); + } + } catch (Exception e) { + Assert.fail("Unexpected exception", e); + } + } + + @Test(groups = {"integration"}) + void testGetObjectArray2D() { + final String table = "test_get_object_array_2d"; + try (Client client = newClient().build()) { + client.execute("DROP TABLE IF EXISTS " + table); + client.execute("CREATE TABLE " + table + " (" + + "arr2d_int Array(Array(Int64)), " + + "arr2d_str Array(Array(String))" + + ") ENGINE = MergeTree() ORDER BY tuple()"); + + client.execute("INSERT INTO " + table + " VALUES (" + + "[[1, 2, 3], [4, 5]], " + + "[['hello', 'world'], ['foo']])"); + + QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.RowBinary); + TableSchema schema = client.getTableSchema(table); + try (QueryResponse response = client.query("SELECT * FROM " + table, settings).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response, schema); + reader.next(); + + // Array(Array(Int64)) -> nested Object[] + Object[] arr2dInt = reader.getObjectArray("arr2d_int"); + Assert.assertNotNull(arr2dInt); + Assert.assertEquals(arr2dInt.length, 2); + Assert.assertTrue(arr2dInt[0] instanceof Object[]); + Assert.assertTrue(arr2dInt[1] instanceof Object[]); + + Object[] inner0 = (Object[]) arr2dInt[0]; + Assert.assertEquals(inner0.length, 3); + Assert.assertEquals(inner0[0], 1L); + Assert.assertEquals(inner0[1], 2L); + Assert.assertEquals(inner0[2], 3L); + + Object[] inner1 = (Object[]) arr2dInt[1]; + Assert.assertEquals(inner1.length, 2); + Assert.assertEquals(inner1[0], 4L); + Assert.assertEquals(inner1[1], 5L); + + // Array(Array(String)) -> nested Object[] + Object[] arr2dStr = reader.getObjectArray("arr2d_str"); + Assert.assertNotNull(arr2dStr); + Assert.assertEquals(arr2dStr.length, 2); + + Object[] strInner0 = (Object[]) arr2dStr[0]; + Assert.assertEquals(strInner0, new Object[]{"hello", "world"}); + + Object[] strInner1 = (Object[]) arr2dStr[1]; + Assert.assertEquals(strInner1, new Object[]{"foo"}); + + Assert.assertNull(reader.next(), "Expected only one row"); + } + } catch (Exception e) { + Assert.fail("Unexpected exception", e); + } + } + + @Test(groups = {"integration"}) + void testGetObjectArray3D() { + final String table = "test_get_object_array_3d"; + try (Client client = newClient().build()) { + client.execute("DROP TABLE IF EXISTS " + table); + client.execute("CREATE TABLE " + table + " (" + + "arr3d Array(Array(Array(Int32)))" + + ") ENGINE = MergeTree() ORDER BY tuple()"); + + client.execute("INSERT INTO " + table + " VALUES (" + + "[[[1, 2], [3]], [[4, 5, 6]]])"); + + QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.RowBinary); + TableSchema schema = client.getTableSchema(table); + try (QueryResponse response = client.query("SELECT * FROM " + table, settings).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response, schema); + reader.next(); + + // Array(Array(Array(Int32))) -> 3-level nested Object[] + Object[] arr3d = reader.getObjectArray("arr3d"); + Assert.assertNotNull(arr3d); + Assert.assertEquals(arr3d.length, 2); + + // dim1[0] = [[1, 2], [3]] + Assert.assertTrue(arr3d[0] instanceof Object[]); + Object[] dim1_0 = (Object[]) arr3d[0]; + Assert.assertEquals(dim1_0.length, 2); + + Assert.assertTrue(dim1_0[0] instanceof Object[]); + Object[] dim2_0_0 = (Object[]) dim1_0[0]; + Assert.assertEquals(dim2_0_0.length, 2); + Assert.assertEquals(dim2_0_0[0], 1); + Assert.assertEquals(dim2_0_0[1], 2); + + Assert.assertTrue(dim1_0[1] instanceof Object[]); + Object[] dim2_0_1 = (Object[]) dim1_0[1]; + Assert.assertEquals(dim2_0_1.length, 1); + Assert.assertEquals(dim2_0_1[0], 3); + + // dim1[1] = [[4, 5, 6]] + Assert.assertTrue(arr3d[1] instanceof Object[]); + Object[] dim1_1 = (Object[]) arr3d[1]; + Assert.assertEquals(dim1_1.length, 1); + + Assert.assertTrue(dim1_1[0] instanceof Object[]); + Object[] dim2_1_0 = (Object[]) dim1_1[0]; + Assert.assertEquals(dim2_1_0.length, 3); + Assert.assertEquals(dim2_1_0[0], 4); + Assert.assertEquals(dim2_1_0[1], 5); + Assert.assertEquals(dim2_1_0[2], 6); + + Assert.assertNull(reader.next(), "Expected only one row"); + } + } catch (Exception e) { + Assert.fail("Unexpected exception", e); + } + } + + @Test(groups = {"integration"}) + void testGetObjectArrayWithEmptyArrays() { + final String table = "test_get_object_array_empty"; + try (Client client = newClient().build()) { + client.execute("DROP TABLE IF EXISTS " + table); + client.execute("CREATE TABLE " + table + " (" + + "empty_arr Array(Int32), " + + "empty_2d Array(Array(String))" + + ") ENGINE = MergeTree() ORDER BY tuple()"); + + client.execute("INSERT INTO " + table + " VALUES ([], [])"); + + QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.RowBinary); + TableSchema schema = client.getTableSchema(table); + try (QueryResponse response = client.query("SELECT * FROM " + table, settings).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response, schema); + reader.next(); + + Object[] emptyArr = reader.getObjectArray("empty_arr"); + Assert.assertNotNull(emptyArr); + Assert.assertEquals(emptyArr.length, 0); + + Object[] empty2d = reader.getObjectArray("empty_2d"); + Assert.assertNotNull(empty2d); + Assert.assertEquals(empty2d.length, 0); + + Assert.assertNull(reader.next(), "Expected only one row"); + } + } catch (Exception e) { + Assert.fail("Unexpected exception", e); + } + } + + @Test(groups = {"integration"}) + void testGetObjectArrayMultipleRows() { + final String table = "test_get_object_array_multi_row"; + try (Client client = newClient().build()) { + client.execute("DROP TABLE IF EXISTS " + table); + client.execute("CREATE TABLE " + table + " (" + + "id UInt32, " + + "arr Array(UInt64)" + + ") ENGINE = MergeTree() ORDER BY id"); + + client.execute("INSERT INTO " + table + " VALUES " + + "(1, [100, 200]), " + + "(2, [300]), " + + "(3, [])"); + + QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.RowBinary); + TableSchema schema = client.getTableSchema(table); + try (QueryResponse response = client.query("SELECT * FROM " + table + " ORDER BY id", settings).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response, schema); + + // Row 1 + reader.next(); + Object[] arr1 = reader.getObjectArray("arr"); + Assert.assertEquals(arr1.length, 2); + Assert.assertEquals(arr1[0], BigInteger.valueOf(100)); + Assert.assertEquals(arr1[1], BigInteger.valueOf(200)); + + // Row 2 + reader.next(); + Object[] arr2 = reader.getObjectArray("arr"); + Assert.assertEquals(arr2.length, 1); + Assert.assertEquals(arr2[0], BigInteger.valueOf(300)); + + // Row 3 + reader.next(); + Object[] arr3 = reader.getObjectArray("arr"); + Assert.assertEquals(arr3.length, 0); + + Assert.assertNull(reader.next(), "Expected only three rows"); + } + } catch (Exception e) { + Assert.fail("Unexpected exception", e); + } + } + @Data public static class DefaultWithFunctionPojo { private String name; diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BaseReaderTests.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BaseReaderTests.java index befa6ee87..b3e9f0676 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BaseReaderTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BaseReaderTests.java @@ -11,6 +11,7 @@ import com.clickhouse.client.api.query.GenericRecord; import com.clickhouse.client.api.query.QueryResponse; import com.clickhouse.data.ClickHouseVersion; +import com.clickhouse.data.ClickHouseDataType; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; @@ -25,6 +26,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Arrays; +import java.util.Collections; import java.util.List; @Test(groups = {"integration"}) @@ -425,7 +427,125 @@ public void testReadingOffsetDateTimeFromVariant() throws Exception { OffsetDateTime actualOffsetDateTime = records.get(0).getOffsetDateTime("field"); Assert.assertEquals(actualOffsetDateTime, expectedOffsetDateTime); } - + + @Test(groups = {"integration"}) + public void testGetObjectArrayWithNullableElements() throws Exception { + final String table = "test_get_object_array_with_nullable_elements"; + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, + "id Int32", + "arr_nullable Array(Nullable(Int32))", + "arr2d_nullable Array(Array(Nullable(Int32)))")).get(); + + client.execute("INSERT INTO " + table + " VALUES (1, [1, NULL, 2], [[1, NULL], [NULL, 3]])").get(); + + try (QueryResponse response = client.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + Assert.assertNotNull(reader.next()); + + Object[] arrNullable = reader.getObjectArray("arr_nullable"); + Assert.assertNotNull(arrNullable); + Assert.assertEquals(arrNullable.length, 3); + Assert.assertEquals(arrNullable[0], 1); + Assert.assertNull(arrNullable[1]); + Assert.assertEquals(arrNullable[2], 2); + + Object[] arr2dNullable = reader.getObjectArray("arr2d_nullable"); + Assert.assertNotNull(arr2dNullable); + Assert.assertEquals(arr2dNullable.length, 2); + Assert.assertTrue(arr2dNullable[0] instanceof Object[]); + Assert.assertTrue(arr2dNullable[1] instanceof Object[]); + + Object[] inner0 = (Object[]) arr2dNullable[0]; + Assert.assertEquals(inner0.length, 2); + Assert.assertEquals(inner0[0], 1); + Assert.assertNull(inner0[1]); + + Object[] inner1 = (Object[]) arr2dNullable[1]; + Assert.assertEquals(inner1.length, 2); + Assert.assertNull(inner1[0]); + Assert.assertEquals(inner1[1], 3); + } + + List records = client.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + GenericRecord record = records.get(0); + + Object[] arrNullableRecord = record.getObjectArray("arr_nullable"); + Assert.assertNotNull(arrNullableRecord); + Assert.assertEquals(arrNullableRecord.length, 3); + Assert.assertEquals(arrNullableRecord[0], 1); + Assert.assertNull(arrNullableRecord[1]); + Assert.assertEquals(arrNullableRecord[2], 2); + + Object[] arr2dNullableRecord = record.getObjectArray("arr2d_nullable"); + Assert.assertNotNull(arr2dNullableRecord); + Assert.assertEquals(arr2dNullableRecord.length, 2); + + Object[] innerRecord0 = (Object[]) arr2dNullableRecord[0]; + Assert.assertEquals(innerRecord0.length, 2); + Assert.assertEquals(innerRecord0[0], 1); + Assert.assertNull(innerRecord0[1]); + + Object[] innerRecord1 = (Object[]) arr2dNullableRecord[1]; + Assert.assertEquals(innerRecord1.length, 2); + Assert.assertNull(innerRecord1[0]); + Assert.assertEquals(innerRecord1[1], 3); + } + + @Test(groups = {"integration"}) + public void testGetObjectArrayWhenValueIsList() throws Exception { + final String table = "test_get_object_array_when_value_is_list"; + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute(tableDefinition(table, + "id Int32", + "arr Array(Int32)", + "arr2d Array(Array(Int32))")).get(); + client.execute("INSERT INTO " + table + " VALUES (1, [10, 20, 30], [[1, 2], [3]])").get(); + + try (Client listClient = newClient() + .typeHintMapping(Collections.singletonMap(ClickHouseDataType.Array, Object.class)) + .build()) { + try (QueryResponse response = listClient.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = listClient.newBinaryFormatReader(response); + Assert.assertNotNull(reader.next()); + + Object[] arr = reader.getObjectArray("arr"); + Assert.assertNotNull(arr); + Assert.assertEquals(arr.length, 3); + Assert.assertEquals(arr[0], 10); + Assert.assertEquals(arr[1], 20); + Assert.assertEquals(arr[2], 30); + + Object[] arr2d = reader.getObjectArray("arr2d"); + Assert.assertNotNull(arr2d); + Assert.assertEquals(arr2d.length, 2); + Assert.assertTrue(arr2d[0] instanceof List); + Assert.assertTrue(arr2d[1] instanceof List); + Assert.assertEquals((List) arr2d[0], Arrays.asList(1, 2)); + Assert.assertEquals((List) arr2d[1], Collections.singletonList(3)); + } + + List records = listClient.queryAll("SELECT * FROM " + table); + Assert.assertEquals(records.size(), 1); + + Object[] arrRecord = records.get(0).getObjectArray("arr"); + Assert.assertNotNull(arrRecord); + Assert.assertEquals(arrRecord.length, 3); + Assert.assertEquals(arrRecord[0], 10); + Assert.assertEquals(arrRecord[1], 20); + Assert.assertEquals(arrRecord[2], 30); + + Object[] arr2dRecord = records.get(0).getObjectArray("arr2d"); + Assert.assertNotNull(arr2dRecord); + Assert.assertEquals(arr2dRecord.length, 2); + Assert.assertTrue(arr2dRecord[0] instanceof List); + Assert.assertTrue(arr2dRecord[1] instanceof List); + Assert.assertEquals((List) arr2dRecord[0], Arrays.asList(1, 2)); + Assert.assertEquals((List) arr2dRecord[1], Collections.singletonList(3)); + } + } + public static String tableDefinition(String table, String... columns) { StringBuilder sb = new StringBuilder(); diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecordTest.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecordTest.java new file mode 100644 index 000000000..a73b36f7b --- /dev/null +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/BinaryReaderBackedRecordTest.java @@ -0,0 +1,185 @@ +package com.clickhouse.client.api.data_formats.internal; + +import com.clickhouse.client.BaseIntegrationTest; +import com.clickhouse.client.ClickHouseNode; +import com.clickhouse.client.ClickHouseProtocol; +import com.clickhouse.client.ClickHouseServerForTest; +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.enums.Protocol; +import com.clickhouse.client.api.query.GenericRecord; +import com.clickhouse.client.api.query.Records; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.math.BigInteger; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Test(groups = {"integration"}) +public class BinaryReaderBackedRecordTest extends BaseIntegrationTest { + + private Client client; + + @BeforeMethod(groups = {"integration"}) + public void setUp() { + client = newClient().build(); + } + + @AfterMethod(groups = {"integration"}) + public void tearDown() { + if (client != null) { + client.close(); + } + } + + private Client.Builder newClient() { + ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); + return new Client.Builder() + .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), isCloud()) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .setDefaultDatabase(ClickHouseServerForTest.getDatabase()); + } + + @Test(groups = {"integration"}) + public void testGetObjectArray() throws Exception { + final String table = "test_binary_reader_backed_get_object_array"; + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute("CREATE TABLE " + table + " (" + + "rowId Int32, " + + "uint64_arr Array(UInt64), " + + "enum_arr Array(Enum8('abc' = 1, 'cde' = 2, 'xyz' = 3)), " + + "dt_arr Array(DateTime('UTC')), " + + "str_arr Array(String), " + + "int_arr Array(Int32), " + + "arr2d Array(Array(Int64)), " + + "arr3d Array(Array(Array(Int32)))" + + ") Engine = MergeTree ORDER BY rowId").get(); + + client.execute("INSERT INTO " + table + " VALUES " + + "(1, " + + "[100, 200], " + + "['abc', 'cde'], " + + "['2030-10-09 08:07:06', '2031-10-09 08:07:06'], " + + "['hello', 'world'], " + + "[10, 20, 30], " + + "[[1, 2, 3], [4, 5]], " + + "[[[1, 2], [3]], [[4, 5, 6]]]" + + ")").get(); + + List records = new ArrayList<>(); + try (Records rs = client.queryRecords("SELECT * FROM " + table + " ORDER BY rowId").get(10, TimeUnit.SECONDS)) { + for (GenericRecord record : rs) { + records.add(record); + } + } + + Assert.assertEquals(records.size(), 1); + GenericRecord row = records.get(0); + Assert.assertTrue(row instanceof BinaryReaderBackedRecord); + + // Array(UInt64) -> getObjectArray returns BigInteger[] + Object[] uint64Arr = row.getObjectArray("uint64_arr"); + Assert.assertNotNull(uint64Arr); + Assert.assertEquals(uint64Arr.length, 2); + Assert.assertEquals(uint64Arr[0], BigInteger.valueOf(100)); + Assert.assertEquals(uint64Arr[1], BigInteger.valueOf(200)); + + // Array(Enum8) -> getObjectArray returns EnumValue[] + Object[] enumArr = row.getObjectArray("enum_arr"); + Assert.assertNotNull(enumArr); + Assert.assertEquals(enumArr.length, 2); + Assert.assertEquals(enumArr[0].toString(), "abc"); + Assert.assertEquals(enumArr[1].toString(), "cde"); + + // Array(DateTime) -> getObjectArray returns ZonedDateTime[] + Object[] dtArr = row.getObjectArray("dt_arr"); + Assert.assertNotNull(dtArr); + Assert.assertEquals(dtArr.length, 2); + Assert.assertTrue(dtArr[0] instanceof ZonedDateTime); + ZonedDateTime zdt1 = (ZonedDateTime) dtArr[0]; + Assert.assertEquals(zdt1.getYear(), 2030); + Assert.assertEquals(zdt1.getMonthValue(), 10); + + // Array(String) -> getObjectArray returns String[] + Object[] strArr = row.getObjectArray("str_arr"); + Assert.assertNotNull(strArr); + Assert.assertEquals(strArr[0], "hello"); + Assert.assertEquals(strArr[1], "world"); + + // Array(Int32) -> getObjectArray returns boxed Integer[] + Object[] intArr = row.getObjectArray("int_arr"); + Assert.assertNotNull(intArr); + Assert.assertEquals(intArr.length, 3); + Assert.assertEquals(intArr[0], 10); + Assert.assertEquals(intArr[1], 20); + Assert.assertEquals(intArr[2], 30); + + // Array(Array(Int64)) 2D -> getObjectArray returns nested Object[] + Object[] arr2d = row.getObjectArray("arr2d"); + Assert.assertNotNull(arr2d); + Assert.assertEquals(arr2d.length, 2); + Assert.assertTrue(arr2d[0] instanceof Object[]); + Object[] inner0 = (Object[]) arr2d[0]; + Assert.assertEquals(inner0.length, 3); + Assert.assertEquals(inner0[0], 1L); + Assert.assertEquals(inner0[1], 2L); + Assert.assertEquals(inner0[2], 3L); + + // Array(Array(Array(Int32))) 3D -> getObjectArray returns 3-level nested Object[] + Object[] arr3d = row.getObjectArray("arr3d"); + Assert.assertNotNull(arr3d); + Assert.assertEquals(arr3d.length, 2); + Object[] dim1_0 = (Object[]) arr3d[0]; + Assert.assertEquals(dim1_0.length, 2); + Object[] dim2_0_0 = (Object[]) dim1_0[0]; + Assert.assertEquals(dim2_0_0[0], 1); + Assert.assertEquals(dim2_0_0[1], 2); + } + + @Test(groups = {"integration"}) + public void testGetObjectArrayEmptyAndEdgeCases() throws Exception { + final String table = "test_binary_reader_backed_get_object_array_empty"; + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute("CREATE TABLE " + table + " (" + + "rowId Int32, " + + "empty_arr Array(Int32), " + + "single_arr Array(String), " + + "arr2d_empty Array(Array(Int64))" + + ") Engine = MergeTree ORDER BY rowId").get(); + + client.execute("INSERT INTO " + table + " VALUES (1, [], ['single'], [[]])").get(); + + List records = new ArrayList<>(); + try (Records rs = client.queryRecords("SELECT * FROM " + table).get(10, TimeUnit.SECONDS)) { + for (GenericRecord record : rs) { + records.add(record); + } + } + + Assert.assertEquals(records.size(), 1); + GenericRecord row = records.get(0); + + // Empty array + Object[] emptyArr = row.getObjectArray("empty_arr"); + Assert.assertNotNull(emptyArr); + Assert.assertEquals(emptyArr.length, 0); + + // Single-element array + Object[] singleArr = row.getObjectArray("single_arr"); + Assert.assertNotNull(singleArr); + Assert.assertEquals(singleArr.length, 1); + Assert.assertEquals(singleArr[0], "single"); + + // 2D with inner empty: [[]] + Object[] arr2dEmpty = row.getObjectArray("arr2d_empty"); + Assert.assertNotNull(arr2dEmpty); + Assert.assertEquals(arr2dEmpty.length, 1); + Assert.assertTrue(arr2dEmpty[0] instanceof Object[]); + Assert.assertEquals(((Object[]) arr2dEmpty[0]).length, 0); + } +} \ No newline at end of file diff --git a/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java b/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java index 52347c729..4a7a07059 100644 --- a/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java @@ -1278,6 +1278,251 @@ public Object[][] testNestedArrays_dp() { }; } + @Test(groups = {"integration"}) + public void testGetObjectArrayMethods() throws Exception { + final String table = "test_get_object_array_methods"; + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute("CREATE TABLE " + table + " (" + + "rowId Int32, " + + "uint64_arr Array(UInt64), " + + "enum_arr Array(Enum8('abc' = 1, 'cde' = 2, 'xyz' = 3)), " + + "dt_arr Array(DateTime('UTC')), " + + "fstr_arr Array(FixedString(4)), " + + "str_arr Array(String), " + + "int_arr Array(Int32), " + + "arr2d Array(Array(Int64)), " + + "arr3d Array(Array(Array(Int32)))" + + ") Engine = MergeTree ORDER BY rowId").get(); + + client.execute("INSERT INTO " + table + " VALUES " + + "(1, " + + "[100, 200, 18000044073709551615], " + + "['abc', 'cde'], " + + "['2030-10-09 08:07:06', '2031-10-09 08:07:06'], " + + "['abcd', 'efgh'], " + + "['hello', 'world'], " + + "[10, 20, 30], " + + "[[1, 2, 3], [4, 5]], " + + "[[[1, 2], [3]], [[4, 5, 6]]]" + + "), " + + "(2, " + + "[], " + + "['xyz'], " + + "[], " + + "[], " + + "[], " + + "[], " + + "[[]], " + + "[[[]]]" + + ")").get(); + + List records = client.queryAll("SELECT * FROM " + table + " ORDER BY rowId"); + Assert.assertEquals(records.size(), 2); + + // --- Row 1: non-empty arrays --- + GenericRecord row1 = records.get(0); + Assert.assertEquals(row1.getInteger("rowId"), 1); + + // Array(UInt64) -> getObjectArray returns BigInteger[] + Object[] uint64Arr = row1.getObjectArray("uint64_arr"); + Assert.assertNotNull(uint64Arr); + Assert.assertEquals(uint64Arr.length, 3); + Assert.assertTrue(uint64Arr[0] instanceof java.math.BigInteger); + Assert.assertEquals(uint64Arr[0], java.math.BigInteger.valueOf(100)); + Assert.assertEquals(uint64Arr[1], java.math.BigInteger.valueOf(200)); + Assert.assertEquals(uint64Arr[2], new java.math.BigInteger("18000044073709551615")); + + // Array(Enum8) -> getObjectArray returns EnumValue[] + Object[] enumArr = row1.getObjectArray("enum_arr"); + Assert.assertNotNull(enumArr); + Assert.assertEquals(enumArr.length, 2); + Assert.assertTrue(enumArr[0] instanceof BinaryStreamReader.EnumValue); + Assert.assertEquals(enumArr[0].toString(), "abc"); + Assert.assertEquals(enumArr[1].toString(), "cde"); + + // Array(DateTime) -> getObjectArray returns ZonedDateTime[] + Object[] dtArr = row1.getObjectArray("dt_arr"); + Assert.assertNotNull(dtArr); + Assert.assertEquals(dtArr.length, 2); + Assert.assertTrue(dtArr[0] instanceof java.time.ZonedDateTime); + java.time.ZonedDateTime zdt1 = (java.time.ZonedDateTime) dtArr[0]; + java.time.ZonedDateTime zdt2 = (java.time.ZonedDateTime) dtArr[1]; + Assert.assertEquals(zdt1.getYear(), 2030); + Assert.assertEquals(zdt1.getMonthValue(), 10); + Assert.assertEquals(zdt1.getDayOfMonth(), 9); + Assert.assertEquals(zdt1.getHour(), 8); + Assert.assertEquals(zdt1.getMinute(), 7); + Assert.assertEquals(zdt1.getSecond(), 6); + Assert.assertEquals(zdt2.getYear(), 2031); + + // Array(FixedString) -> getObjectArray returns String[] + Object[] fstrArr = row1.getObjectArray("fstr_arr"); + Assert.assertNotNull(fstrArr); + Assert.assertEquals(fstrArr.length, 2); + Assert.assertEquals(fstrArr[0], "abcd"); + Assert.assertEquals(fstrArr[1], "efgh"); + + // Array(FixedString) -> getStringArray + String[] fstrStrings = row1.getStringArray("fstr_arr"); + Assert.assertEquals(fstrStrings, new String[]{"abcd", "efgh"}); + + // Array(String) -> getObjectArray returns String[] + Object[] strArr = row1.getObjectArray("str_arr"); + Assert.assertNotNull(strArr); + Assert.assertEquals(strArr[0], "hello"); + Assert.assertEquals(strArr[1], "world"); + + // Array(Int32) -> getObjectArray returns boxed Integer[] + Object[] intArr = row1.getObjectArray("int_arr"); + Assert.assertNotNull(intArr); + Assert.assertEquals(intArr.length, 3); + Assert.assertEquals(intArr[0], 10); + Assert.assertEquals(intArr[1], 20); + Assert.assertEquals(intArr[2], 30); + + // Array(Array(Int64)) 2D -> getObjectArray returns nested Object[] + Object[] arr2d = row1.getObjectArray("arr2d"); + Assert.assertNotNull(arr2d); + Assert.assertEquals(arr2d.length, 2); + Assert.assertTrue(arr2d[0] instanceof Object[]); + Assert.assertTrue(arr2d[1] instanceof Object[]); + Object[] inner2d_0 = (Object[]) arr2d[0]; + Assert.assertEquals(inner2d_0.length, 3); + Assert.assertEquals(inner2d_0[0], 1L); + Assert.assertEquals(inner2d_0[1], 2L); + Assert.assertEquals(inner2d_0[2], 3L); + Object[] inner2d_1 = (Object[]) arr2d[1]; + Assert.assertEquals(inner2d_1.length, 2); + Assert.assertEquals(inner2d_1[0], 4L); + Assert.assertEquals(inner2d_1[1], 5L); + + // Array(Array(Array(Int32))) 3D -> getObjectArray returns 3-level nested Object[] + Object[] arr3d = row1.getObjectArray("arr3d"); + Assert.assertNotNull(arr3d); + Assert.assertEquals(arr3d.length, 2); + + // [[[1, 2], [3]], [[4, 5, 6]]] + Object[] dim1_0 = (Object[]) arr3d[0]; + Assert.assertEquals(dim1_0.length, 2); + Object[] dim2_0_0 = (Object[]) dim1_0[0]; + Assert.assertEquals(dim2_0_0.length, 2); + Assert.assertEquals(dim2_0_0[0], 1); + Assert.assertEquals(dim2_0_0[1], 2); + Object[] dim2_0_1 = (Object[]) dim1_0[1]; + Assert.assertEquals(dim2_0_1.length, 1); + Assert.assertEquals(dim2_0_1[0], 3); + + Object[] dim1_1 = (Object[]) arr3d[1]; + Assert.assertEquals(dim1_1.length, 1); + Object[] dim2_1_0 = (Object[]) dim1_1[0]; + Assert.assertEquals(dim2_1_0.length, 3); + Assert.assertEquals(dim2_1_0[0], 4); + Assert.assertEquals(dim2_1_0[1], 5); + Assert.assertEquals(dim2_1_0[2], 6); + + // --- Row 2: edge cases (empty arrays, single elements) --- + GenericRecord row2 = records.get(1); + Assert.assertEquals(row2.getInteger("rowId"), 2); + + // Empty arrays + Object[] emptyUint64 = row2.getObjectArray("uint64_arr"); + Assert.assertNotNull(emptyUint64); + Assert.assertEquals(emptyUint64.length, 0); + + Object[] emptyDt = row2.getObjectArray("dt_arr"); + Assert.assertNotNull(emptyDt); + Assert.assertEquals(emptyDt.length, 0); + + Object[] emptyStr = row2.getObjectArray("str_arr"); + Assert.assertNotNull(emptyStr); + Assert.assertEquals(emptyStr.length, 0); + + Object[] emptyInt = row2.getObjectArray("int_arr"); + Assert.assertNotNull(emptyInt); + Assert.assertEquals(emptyInt.length, 0); + + // Single-element enum array + Object[] singleEnum = row2.getObjectArray("enum_arr"); + Assert.assertEquals(singleEnum.length, 1); + Assert.assertEquals(singleEnum[0].toString(), "xyz"); + + String[] singleEnumStr = row2.getStringArray("enum_arr"); + Assert.assertEquals(singleEnumStr, new String[]{"xyz"}); + + // 2D with inner empty: [[]] + Object[] arr2dEmpty = row2.getObjectArray("arr2d"); + Assert.assertNotNull(arr2dEmpty); + Assert.assertEquals(arr2dEmpty.length, 1); + Assert.assertTrue(arr2dEmpty[0] instanceof Object[]); + Assert.assertEquals(((Object[]) arr2dEmpty[0]).length, 0); + + // 3D with inner empty: [[[]]] + Object[] arr3dEmpty = row2.getObjectArray("arr3d"); + Assert.assertNotNull(arr3dEmpty); + Assert.assertEquals(arr3dEmpty.length, 1); + Object[] arr3dInner = (Object[]) arr3dEmpty[0]; + Assert.assertEquals(arr3dInner.length, 1); + Assert.assertTrue(arr3dInner[0] instanceof Object[]); + Assert.assertEquals(((Object[]) arr3dInner[0]).length, 0); + } + + @Test(groups = {"integration"}) + public void testGetStringArrayAndGetObjectArrayWhenValueIsList() throws Exception { + final String table = "test_get_string_array_and_object_array_when_value_is_list"; + client.execute("DROP TABLE IF EXISTS " + table).get(); + client.execute("CREATE TABLE " + table + " (" + + "rowId Int32, " + + "str_arr Array(String), " + + "arr2d Array(Array(Int32))" + + ") Engine = MergeTree ORDER BY rowId").get(); + + client.execute("INSERT INTO " + table + " VALUES " + + "(1, ['hello', 'world'], [[1, 2], [3]])").get(); + + try (Client listClient = newClient() + .typeHintMapping(Collections.singletonMap(ClickHouseDataType.Array, Object.class)) + .build()) { + // Reader path: arrays are decoded as List due to Array -> Object type hint mapping. + try (QueryResponse response = listClient.query("SELECT * FROM " + table).get()) { + ClickHouseBinaryFormatReader reader = listClient.newBinaryFormatReader(response); + Assert.assertNotNull(reader.next()); + + Object[] strObjectArr = reader.getObjectArray("str_arr"); + Assert.assertNotNull(strObjectArr); + Assert.assertEquals(strObjectArr, new Object[] {"hello", "world"}); + + Object[] arr2dObjectArr = reader.getObjectArray("arr2d"); + Assert.assertNotNull(arr2dObjectArr); + Assert.assertEquals(arr2dObjectArr.length, 2); + Assert.assertTrue(arr2dObjectArr[0] instanceof List); + Assert.assertTrue(arr2dObjectArr[1] instanceof List); + Assert.assertEquals((List) arr2dObjectArr[0], Arrays.asList(1, 2)); + Assert.assertEquals((List) arr2dObjectArr[1], Collections.singletonList(3)); + + Assert.expectThrows(ClientException.class, () -> reader.getStringArray("str_arr")); + } + + // queryAll path (MapBackedRecord): also list-backed values. + List records = listClient.queryAll("SELECT * FROM " + table + " ORDER BY rowId"); + Assert.assertEquals(records.size(), 1); + + GenericRecord row = records.get(0); + Object[] strObjectArr = row.getObjectArray("str_arr"); + Assert.assertNotNull(strObjectArr); + Assert.assertEquals(strObjectArr, new Object[] {"hello", "world"}); + + Object[] arr2dObjectArr = row.getObjectArray("arr2d"); + Assert.assertNotNull(arr2dObjectArr); + Assert.assertEquals(arr2dObjectArr.length, 2); + Assert.assertTrue(arr2dObjectArr[0] instanceof List); + Assert.assertTrue(arr2dObjectArr[1] instanceof List); + Assert.assertEquals((List) arr2dObjectArr[0], Arrays.asList(1, 2)); + Assert.assertEquals((List) arr2dObjectArr[1], Collections.singletonList(3)); + + Assert.expectThrows(ClientException.class, () -> row.getStringArray("str_arr")); + } + } + public static String tableDefinition(String table, String... columns) { StringBuilder sb = new StringBuilder(); sb.append("CREATE TABLE " + table + " ( ");