diff --git a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java index 3baff8680..01a90e971 100644 --- a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java +++ b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java @@ -54,6 +54,7 @@ /** * This class represents a column defined in database. */ +@SuppressWarnings("deprecation") public final class ClickHouseColumn implements Serializable { public static final String TYPE_NAME = "Column"; public static final ClickHouseColumn[] EMPTY_ARRAY = new ClickHouseColumn[0]; @@ -105,6 +106,8 @@ public final class ClickHouseColumn implements Serializable { private Map, Integer> arrayToVariantOrdNumMap; + private Map geometryTypeDimensionsToVariantOrdNumMap; + private Map, Integer> mapKeyToVariantOrdNumMap; private Map, Integer> mapValueToVariantOrdNumMap; @@ -293,12 +296,28 @@ private static ClickHouseColumn update(ClickHouseColumn column) { case Ring: column.template = ClickHouseGeoRingValue.ofEmpty(); break; + case LineString: + column.template = ClickHouseGeoRingValue.ofEmpty(); + break; case Polygon: column.template = ClickHouseGeoPolygonValue.ofEmpty(); break; + case MultiLineString: + column.template = ClickHouseGeoPolygonValue.ofEmpty(); + break; case MultiPolygon: column.template = ClickHouseGeoMultiPolygonValue.ofEmpty(); break; + case Geometry: + ClickHouseColumn geometryVariantHelper = GEOMETRY_VARIANT_COLUMN; + column.template = ClickHouseTupleValue.of(); + column.nested = geometryVariantHelper.nested; + column.classToVariantOrdNumMap = geometryVariantHelper.classToVariantOrdNumMap; + column.arrayToVariantOrdNumMap = geometryVariantHelper.arrayToVariantOrdNumMap; + column.geometryTypeDimensionsToVariantOrdNumMap = geometryVariantHelper.geometryTypeDimensionsToVariantOrdNumMap; + column.mapKeyToVariantOrdNumMap = geometryVariantHelper.mapKeyToVariantOrdNumMap; + column.mapValueToVariantOrdNumMap = geometryVariantHelper.mapValueToVariantOrdNumMap; + break; case Nested: column.template = ClickHouseNestedValue.ofEmpty(column.nested); break; @@ -318,6 +337,28 @@ private static ClickHouseColumn update(ClickHouseColumn column) { return column; } + private static ClickHouseColumn createGeometryVariantColumn() { + ClickHouseColumn column = ClickHouseColumn.of("v", + "Variant(Point, Ring, LineString, MultiLineString, Polygon, MultiPolygon)"); + Map map = new HashMap<>(); + map.put(1, getVariantOrdNum(column.nested, ClickHouseDataType.Point)); + map.put(2, getVariantOrdNum(column.nested, ClickHouseDataType.Ring)); + map.put(3, getVariantOrdNum(column.nested, ClickHouseDataType.Polygon)); + map.put(4, getVariantOrdNum(column.nested, ClickHouseDataType.MultiPolygon)); + column.geometryTypeDimensionsToVariantOrdNumMap = Collections.unmodifiableMap(map); + return column; + } + + private static int getVariantOrdNum(List nestedColumns, ClickHouseDataType dataType) { + for (int i = 0; i < nestedColumns.size(); i++) { + if (nestedColumns.get(i).getDataType() == dataType) { + return i; + } + } + + throw new IllegalArgumentException("Missing nested geometry type: " + dataType); + } + protected static int readColumn(String args, int startIndex, int len, String name, List list) { ClickHouseColumn column = null; @@ -374,7 +415,7 @@ protected static int readColumn(String args, int startIndex, int len, String nam nestedColumns.add(ClickHouseColumn.of("", p)); } } - column = new ClickHouseColumn(ClickHouseDataType.valueOf(matchedKeyword), name, + column = new ClickHouseColumn(ClickHouseDataType.of(matchedKeyword), name, args.substring(startIndex, i), nullable, lowCardinality, params, nestedColumns); column.aggFuncType = aggFunc; if (!nestedColumns.isEmpty()) { @@ -468,7 +509,7 @@ protected static int readColumn(String args, int startIndex, int len, String nam variantDataTypes.add(c.dataType); }); } - column = new ClickHouseColumn(ClickHouseDataType.valueOf(matchedKeyword), name, + column = new ClickHouseColumn(ClickHouseDataType.of(matchedKeyword), name, args.substring(startIndex, endIndex + 1), nullable, lowCardinality, null, nestedColumns); for (ClickHouseColumn n : nestedColumns) { estimatedLength += n.estimatedByteLength; @@ -821,6 +862,9 @@ public boolean isAggregateFunction() { public int getVariantOrdNum(Object value) { if (value != null && value.getClass().isArray()) { + if (arrayToVariantOrdNumMap == null) { + return -1; + } // TODO: add cache by value class Class c = value.getClass(); while (c.isArray()) { @@ -828,11 +872,14 @@ public int getVariantOrdNum(Object value) { } return arrayToVariantOrdNumMap.getOrDefault(c, -1); } else if (value != null && value instanceof List) { + if (arrayToVariantOrdNumMap == null) { + return -1; + } // TODO: add cache by instance of the list - Object tmpV = ((List) value).get(0); + Object tmpV = ((List) value).get(0); Class valueClass = tmpV.getClass(); while (tmpV instanceof List) { - tmpV = ((List) tmpV).get(0); + tmpV = ((List) tmpV).get(0); valueClass = tmpV.getClass(); } return arrayToVariantOrdNumMap.getOrDefault(valueClass, -1); @@ -865,6 +912,29 @@ public int getVariantOrdNum(Object value) { } } + public int getGeometryVariantOrdNum(Object value) { + if (value == null) { + return -1; + } + + Integer ordNum = classToVariantOrdNumMap.get(value.getClass()); + if (ordNum != null) { + return ordNum; + } + + return -1; + } + + public int getGeometryVariantOrdNum(int dimensions) { + if (dimensions < 1 || geometryTypeDimensionsToVariantOrdNumMap == null) { + return -1; + } + + return geometryTypeDimensionsToVariantOrdNumMap.getOrDefault(dimensions, -1); + } + + private static final ClickHouseColumn GEOMETRY_VARIANT_COLUMN = createGeometryVariantColumn(); + public boolean isArray() { return dataType == ClickHouseDataType.Array; } diff --git a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataType.java b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataType.java index 4bf1a6686..c93963fa1 100644 --- a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataType.java +++ b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataType.java @@ -106,6 +106,7 @@ public enum ClickHouseDataType implements SQLType { Ring(Object.class, false, true, true, 0, 0, 0, 0, 0, true), // same as Array(Point) LineString( Object.class, false, true, true, 0, 0, 0, 0, 0, true), // same as Array(Point) MultiLineString(Object.class, false, true, true, 0, 0, 0, 0, 0, true), // same as Array(Ring) + Geometry(Object.class, false, true, true, 0, 0, 0, 0, 0, true), // same as Variant(Point, ...) JSON(Object.class, false, false, false, 0, 0, 0, 0, 0, true, 0x30), @Deprecated // (since = "CH 25.11") Object(Object.class, true, true, false, 0, 0, 0, 0, 0, true), @@ -130,7 +131,6 @@ public enum ClickHouseDataType implements SQLType { Time(LocalDateTime.class, true, false, false, 4, 9, 0, 0, 9, false, 0x32), // 0x33 for Time(Timezone) Time64(LocalDateTime.class, true, false, false, 8, 9, 0, 0, 0, false, 0x34), // 0x35 for Time64(P, Timezone) QBit(Double.class, true, true, false, 0, 0, 0, 0, 0, false, 0x36), - Geometry(Object.class, false, false, false, 0, 0, 0, 0, 0, true), ; public static final List ORDERED_BY_RANGE_INT_TYPES = @@ -214,8 +214,19 @@ static Map>> dataTypeClassMap() { map.put(Point, setOf(double[].class, ClickHouseGeoPointValue.class)); map.put(Ring, setOf(double[][].class, ClickHouseGeoRingValue.class)); + map.put(LineString, setOf(double[][].class, ClickHouseGeoRingValue.class)); map.put(Polygon, setOf(double[][][].class, ClickHouseGeoPolygonValue.class)); + map.put(MultiLineString, setOf(double[][][].class, ClickHouseGeoPolygonValue.class)); map.put(MultiPolygon, setOf(double[][][][].class, ClickHouseGeoMultiPolygonValue.class)); + map.put(Geometry, setOf( + double[].class, + double[][].class, + double[][][].class, + double[][][][].class, + ClickHouseGeoPointValue.class, + ClickHouseGeoRingValue.class, + ClickHouseGeoPolygonValue.class, + ClickHouseGeoMultiPolygonValue.class)); map.put(Date, setOf(LocalDateTime.class, LocalDate.class, ZonedDateTime.class)); map.put(Date32, setOf(LocalDateTime.class, LocalDate.class, ZonedDateTime.class)); diff --git a/clickhouse-data/src/test/java/com/clickhouse/data/ClickHouseColumnTest.java b/clickhouse-data/src/test/java/com/clickhouse/data/ClickHouseColumnTest.java index 2c08cd48d..6a3508b11 100644 --- a/clickhouse-data/src/test/java/com/clickhouse/data/ClickHouseColumnTest.java +++ b/clickhouse-data/src/test/java/com/clickhouse/data/ClickHouseColumnTest.java @@ -1,468 +1,511 @@ -package com.clickhouse.data; - -import java.math.BigInteger; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import com.clickhouse.data.value.ClickHouseArrayValue; -import com.clickhouse.data.value.ClickHouseLongValue; -import com.clickhouse.data.value.UnsignedLong; -import com.clickhouse.data.value.array.ClickHouseLongArrayValue; - -public class ClickHouseColumnTest { - @DataProvider(name = "enumTypesProvider") - private Object[][] getEnumTypes() { - return new Object[][] { { "Enum" }, { "Enum8" }, { "Enum16" } }; - } - - @DataProvider(name = "objectTypesProvider") - private Object[][] getObjectTypes() { - return new Object[][] { - { "Tuple(not NChar Large Object)" }, - { "nchar Large Object" }, - { "Tuple(int Int32)" }, - { "a Tuple(i Int32)" }, - { "b Tuple(i1 Int32)" }, - { "Tuple(i Int32)" }, - { "Tuple(i1 Int32)" }, - { "Tuple(i Int32, a Array(Int32), m Map(LowCardinality(String), Int32))" }, - { "Int8" }, { "TINYINT SIGNED" }, - { "k1 Int8" }, { "k1 TINYINT SIGNED" }, - { "k1 Nullable(Int8)" }, { "k1 Nullable( Int8 )" }, { "k1 TINYINT SIGNED null" }, - { "k1 TINYINT SIGNED not null" }, - { "k1 LowCardinality(Nullable(String))" }, - { "k1 Tuple(k2 Int32, k3 Nullable(String), k4 TINYINT SIGNED not null, k5 Tuple (k6 UInt64))" } - }; - } - - @Test(groups = { "unit" }) - public void testReadColumn() { - String args = "AggregateFunction(max, UInt64), cc LowCardinality(Nullable(String)), a UInt8 null"; - List list = new LinkedList<>(); - Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.indexOf("cc") - 2); - Assert.assertEquals(list.size(), 1); - Assert.assertFalse(list.get(0).isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(list.get(0).getEstimatedLength(), 1); - list.clear(); - Assert.assertEquals(ClickHouseColumn.readColumn(args, args.indexOf("cc") + 3, args.length(), null, list), - args.lastIndexOf(',')); - list.clear(); - Assert.assertEquals(ClickHouseColumn.readColumn(args, args.lastIndexOf('U'), args.length(), null, list), - args.length() - 1); - Assert.assertEquals(list.size(), 1); - ClickHouseColumn column = list.get(0); - Assert.assertNotNull(column); - Assert.assertFalse(column.isLowCardinality()); - Assert.assertTrue(column.isNullable()); - Assert.assertEquals(column.getDataType(), ClickHouseDataType.UInt8); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - list.clear(); - - args = "INT1 unsigned not null, b DateTime64(3) NULL"; - Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.indexOf(',')); - Assert.assertEquals(list.size(), 1); - column = list.get(0); - Assert.assertFalse(column.isNullable()); - Assert.assertEquals(column.getDataType(), ClickHouseDataType.UInt8); - Assert.assertTrue(column.isFixedLength(), "Should have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - list.clear(); - - Assert.assertEquals(ClickHouseColumn.readColumn(args, args.indexOf('D'), args.length(), null, list), - args.length() - 1); - Assert.assertEquals(list.size(), 1); - column = list.get(0); - Assert.assertTrue(column.isNullable()); - Assert.assertEquals(column.getDataType(), ClickHouseDataType.DateTime64); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - } - - @Test(groups = { "unit" }) - public void testReadNestedColumn() { - String args = "Array(Array(Nullable(UInt8)))"; - List list = new LinkedList<>(); - Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); - Assert.assertEquals(list.size(), 1); - ClickHouseColumn column = list.get(0); - Assert.assertEquals(column.getNestedColumns().size(), 1); - Assert.assertEquals(column.getNestedColumns().get(0).getNestedColumns().size(), 1); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - list.clear(); - - args = "Array(FixedString(2))"; - Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); - Assert.assertEquals(list.size(), 1); - column = list.get(0); - Assert.assertEquals(column.getNestedColumns().size(), 1); - Assert.assertTrue(column.getArrayBaseColumn() == column.getNestedColumns().get(0), - "Nested column should be same as base column of the array"); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - column = column.getNestedColumns().get(0); - Assert.assertTrue(column.isFixedLength(), "FixedString should have fixed length in byte"); - Assert.assertEquals(column.getNestedColumns().size(), 0); - Assert.assertEquals(column.getEstimatedLength(), 2); - list.clear(); - - args = "Array(String)"; - Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); - Assert.assertEquals(list.size(), 1); - column = list.get(0); - Assert.assertEquals(column.getNestedColumns().size(), 1); - Assert.assertTrue(column.getArrayBaseColumn() == column.getNestedColumns().get(0), - "Nested column should be same as base column of the array"); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - column = column.getNestedColumns().get(0); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getNestedColumns().size(), 0); - Assert.assertEquals(column.getEstimatedLength(), 1); - list.clear(); - - args = " Tuple(Nullable(FixedString(3)), Array(UInt8),String not null) "; - Assert.assertEquals(ClickHouseColumn.readColumn(args, 1, args.length(), null, list), args.length() - 2); - Assert.assertEquals(list.size(), 1); - column = list.get(0); - Assert.assertEquals(column.getOriginalTypeName(), args.trim()); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 3); - list.clear(); - - args = "Map(UInt8 , UInt8)"; - Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); - Assert.assertEquals(list.size(), 1); - column = list.get(0); - Assert.assertEquals(column.getOriginalTypeName(), args); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - list.clear(); - - args = "Map(String, FixedString(233))"; - Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); - Assert.assertEquals(list.size(), 1); - column = list.get(0); - Assert.assertEquals(column.getOriginalTypeName(), args); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - list.clear(); - - args = "Map(String, Tuple(UInt8, Nullable(String), UInt16 null))"; - Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); - Assert.assertEquals(list.size(), 1); - column = list.get(0); - Assert.assertEquals(column.getOriginalTypeName(), args); - Assert.assertEquals(column.getNestedColumns().size(), 2); - Assert.assertEquals(column.getKeyInfo().getOriginalTypeName(), "String"); - Assert.assertEquals(column.getValueInfo().getOriginalTypeName(), "Tuple(UInt8, Nullable(String), UInt16 null)"); - Assert.assertEquals(column.getValueInfo().getNestedColumns().size(), 3); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - list.clear(); - - args = "Nested(\na Array(Nullable(UInt8)), `b b` LowCardinality(Nullable(DateTime64(3))))"; - Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); - Assert.assertEquals(list.size(), 1); - column = list.get(0); - Assert.assertEquals(column.getOriginalTypeName(), args); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - } - - @Test(groups = { "unit" }) - public void testParse() { - ClickHouseColumn column = ClickHouseColumn.of("arr", "Nullable(Array(Nullable(UInt8))"); - Assert.assertNotNull(column); - - List list = ClickHouseColumn.parse("a String not null, b String null"); - Assert.assertEquals(list.size(), 2); - list = ClickHouseColumn.parse("a String not null, b Int8"); - Assert.assertEquals(list.size(), 2); - list = ClickHouseColumn.parse("a String, b String null"); - Assert.assertEquals(list.size(), 2); - list = ClickHouseColumn.parse("a String default 'cc', b String null"); - Assert.assertEquals(list.size(), 2); - } - - @Test(groups = { "unit" }) - public void testAggregationFunction() { - ClickHouseColumn column = ClickHouseColumn.of("aggFunc", "AggregateFunction(groupBitmap, UInt32)"); - Assert.assertTrue(column.isAggregateFunction()); - Assert.assertEquals(column.getDataType(), ClickHouseDataType.AggregateFunction); - Assert.assertEquals(column.getAggregateFunction(), ClickHouseAggregateFunction.groupBitmap); - Assert.assertEquals(column.getFunction(), "groupBitmap"); - Assert.assertEquals(column.getNestedColumns(), Collections.singletonList(ClickHouseColumn.of("", "UInt32"))); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - - column = ClickHouseColumn.of("aggFunc", "AggregateFunction(quantiles(0.5, 0.9), Nullable(UInt64))"); - Assert.assertTrue(column.isAggregateFunction()); - Assert.assertEquals(column.getDataType(), ClickHouseDataType.AggregateFunction); - Assert.assertEquals(column.getAggregateFunction(), ClickHouseAggregateFunction.quantiles); - Assert.assertEquals(column.getFunction(), "quantiles(0.5,0.9)"); - Assert.assertEquals(column.getNestedColumns(), - Collections.singletonList(ClickHouseColumn.of("", "Nullable(UInt64)"))); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - } - - @Test(groups = { "unit" }) - public void testArray() { - ClickHouseColumn column = ClickHouseColumn.of("arr", - "Array(Array(Array(Array(Array(Map(LowCardinality(String), Tuple(Array(UInt8),LowCardinality(String))))))))"); - Assert.assertTrue(column.isArray()); - Assert.assertEquals(column.getDataType(), ClickHouseDataType.Array); - Assert.assertEquals(column.getArrayNestedLevel(), 5); - Assert.assertEquals(column.getArrayBaseColumn().getOriginalTypeName(), - "Map(LowCardinality(String), Tuple(Array(UInt8),LowCardinality(String)))"); - Assert.assertFalse(column.getArrayBaseColumn().isArray()); - - Assert.assertEquals(column.getArrayBaseColumn().getArrayNestedLevel(), 0); - Assert.assertEquals(column.getArrayBaseColumn().getArrayBaseColumn(), null); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - - ClickHouseColumn c = ClickHouseColumn.of("arr", "Array(LowCardinality(Nullable(String)))"); - Assert.assertTrue(c.isArray()); - Assert.assertEquals(c.getDataType(), ClickHouseDataType.Array); - Assert.assertEquals(c.getArrayNestedLevel(), 1); - Assert.assertEquals(c.getArrayBaseColumn().getOriginalTypeName(), "LowCardinality(Nullable(String))"); - Assert.assertFalse(c.getArrayBaseColumn().isArray()); - Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); - Assert.assertEquals(column.getEstimatedLength(), 1); - } - - @Test(dataProvider = "enumTypesProvider", groups = { "unit" }) - public void testEnum(String typeName) { - Assert.assertThrows(IllegalArgumentException.class, - () -> ClickHouseColumn.of("e", typeName + "('Query''Start' = a)")); - Assert.assertThrows(IllegalArgumentException.class, () -> ClickHouseColumn.of("e", typeName + "(aa,1)")); - ClickHouseColumn column = ClickHouseColumn.of("e", typeName + "('Query''Start' = 1, 'Query\\'Finish' = 10)"); - Assert.assertTrue(column.isEnum()); - Assert.assertEquals(column.getDataType(), ClickHouseDataType.of(typeName)); - Assert.assertThrows(IllegalArgumentException.class, () -> column.getEnumConstants().name(2)); - Assert.assertThrows(IllegalArgumentException.class, () -> column.getEnumConstants().value("")); - Assert.assertEquals(column.getEnumConstants().name(1), "Query'Start"); - Assert.assertEquals(column.getEnumConstants().name(10), "Query'Finish"); - Assert.assertEquals(column.getEnumConstants().value("Query'Start"), 1); - Assert.assertEquals(column.getEnumConstants().value("Query'Finish"), 10); - if (column.getDataType() != ClickHouseDataType.Enum) { // virtual type - Assert.assertTrue(column.isFixedLength(), "Should have fixed length in byte"); - } - Assert.assertEquals(column.getEstimatedLength(), column.getDataType().getByteLength()); - } - - @Test(dataProvider = "objectTypesProvider", groups = { "unit" }) - public void testObjectType(String typeName) { - ClickHouseColumn.of("o", "Tuple(TINYINT SIGNED null)"); - for (String prefix : new String[] { "Tuple(", "Tuple (", "Tuple ( " }) { - for (String suffix : new String[] { ")", " )", " ) " }) { - ClickHouseColumn innerColumn = ClickHouseColumn.of("", - typeName + suffix.substring(0, suffix.lastIndexOf(')'))); - ClickHouseColumn column = ClickHouseColumn.of("o", prefix + typeName + suffix); - Assert.assertTrue(column.isTuple()); - Assert.assertEquals(column.getNestedColumns().get(0), innerColumn); - } - } - } - - @Test(groups = { "unit" }) - public void testSimpleAggregationFunction() { - ClickHouseColumn c = ClickHouseColumn.of("a", "SimpleAggregateFunction(max, UInt64)"); - Assert.assertEquals(c.getDataType(), ClickHouseDataType.SimpleAggregateFunction); - Assert.assertEquals(c.getNestedColumns().get(0).getDataType(), ClickHouseDataType.UInt64); - - // https://github.com/ClickHouse/clickhouse-java/issues/1389 - c = ClickHouseColumn.of("a", "SimpleAggregateFunction(anyLast, Nested(a String, b String))"); - Assert.assertEquals(c.getDataType(), ClickHouseDataType.SimpleAggregateFunction); - Assert.assertEquals(c.getNestedColumns().get(0).getDataType(), ClickHouseDataType.Nested); - Assert.assertEquals(c.getNestedColumns().get(0).getNestedColumns(), - ClickHouseColumn.parse("a String, b String")); - Assert.assertEquals( - ClickHouseColumn.of("a", "SimpleAggregateFunction ( anyLast , Nested ( a String , b String ) )") - .getParameters(), - c.getParameters()); - Assert.assertEquals( - ClickHouseColumn.of("a", - "SimpleAggregateFunction(anyLast,Nested(a String,b String,`c c` Nested(d Int32, e Tuple(UInt32, Nullable(String)))))") - .getParameters(), - ClickHouseColumn.of("a", - " SimpleAggregateFunction ( /** test **/anyLast -- test\n , Nested ( a String , b String,\n\t `c c` \t Nested(d Int32, e Tuple(UInt32, Nullable(String))) ) )") - .getParameters()); - } - - @Test(groups = { "unit" }) - public void testGetObjectClassForArray() { - ClickHouseDataConfig defaultConfig = new ClickHouseTestDataConfig(); - ClickHouseDataConfig widenUnsignedConfig = new ClickHouseTestDataConfig() { - @Override - public boolean isWidenUnsignedTypes() { - return true; - }; - }; - ClickHouseDataConfig binStringConfig = new ClickHouseTestDataConfig() { - @Override - public boolean isUseBinaryString() { - return true; - }; - }; - ClickHouseDataConfig objArrayConfig = new ClickHouseTestDataConfig() { - @Override - public boolean isUseObjectsInArray() { - return true; - }; - }; - - Assert.assertEquals(ClickHouseColumn.of("a", "UInt64").getObjectClassForArray(defaultConfig), long.class); - Assert.assertEquals( - ClickHouseColumn.of("a", "Array(UInt64)").getArrayBaseColumn().getObjectClassForArray(defaultConfig), - long.class); - Assert.assertEquals(ClickHouseColumn.of("a", "UInt64").getObjectClassForArray(widenUnsignedConfig), long.class); - Assert.assertEquals(ClickHouseColumn.of("a", "Array(UInt64)").getArrayBaseColumn() - .getObjectClassForArray(widenUnsignedConfig), long.class); - Assert.assertEquals(ClickHouseColumn.of("a", "UInt64").getObjectClassForArray(objArrayConfig), - UnsignedLong.class); - Assert.assertEquals( - ClickHouseColumn.of("a", "Array(UInt64)").getArrayBaseColumn().getObjectClassForArray(objArrayConfig), - UnsignedLong.class); - - Assert.assertEquals(ClickHouseColumn.of("a", "FixedString(2)").getObjectClassForArray(defaultConfig), - String.class); - Assert.assertEquals(ClickHouseColumn.of("a", "Array(FixedString(2))").getArrayBaseColumn() - .getObjectClassForArray(defaultConfig), String.class); - Assert.assertEquals(ClickHouseColumn.of("a", "FixedString(2)").getObjectClassForArray(binStringConfig), - Object.class); - Assert.assertEquals(ClickHouseColumn.of("a", "Array(FixedString(2))").getArrayBaseColumn() - .getObjectClassForArray(binStringConfig), Object.class); - Assert.assertEquals(ClickHouseColumn.of("a", "String").getObjectClassForArray(defaultConfig), - String.class); - Assert.assertEquals(ClickHouseColumn.of("a", "Array(String)").getArrayBaseColumn() - .getObjectClassForArray(defaultConfig), String.class); - Assert.assertEquals(ClickHouseColumn.of("a", "String").getObjectClassForArray(binStringConfig), - Object.class); - Assert.assertEquals(ClickHouseColumn.of("a", "Array(String)").getArrayBaseColumn() - .getObjectClassForArray(binStringConfig), Object.class); - } - - @Test(groups = { "unit" }) - public void testNewArray() { - ClickHouseDataConfig config = new ClickHouseTestDataConfig() { - @Override - public boolean isWidenUnsignedTypes() { - return true; - }; - }; - ClickHouseValue v = ClickHouseColumn.of("a", "Array(UInt32)").newValue(config); - Assert.assertEquals(v.update(new long[] { 1L }).asObject(), new long[] { 1L }); - v = ClickHouseColumn.of("a", "Array(Nullable(UInt64))").newValue(config); - Assert.assertEquals(v.update(new Long[] { 1L }).asObject(), new Long[] { 1L }); - v = ClickHouseColumn.of("a", "Array(Array(UInt16))").newValue(config); - Assert.assertEquals(v.asObject(), new int[0][]); - v = ClickHouseColumn.of("a", "Array(UInt64)").newValue(config); - Assert.assertEquals(v.asObject(), new UnsignedLong[0]); - Assert.assertEquals(((ClickHouseLongArrayValue) v).allocate(1) - .setValue(0, ClickHouseLongValue.of(1L)).asObject(), new long[] { 1L }); - Assert.assertEquals(((ClickHouseLongArrayValue) v).allocate(1) - .setValue(0, ClickHouseLongValue.ofUnsigned(1L)).asObject(), new long[] { 1L }); - v = ClickHouseColumn.of("a", "Array(Array(UInt64))").newValue(config); - Assert.assertEquals(v.asObject(), new long[0][]); - Assert.assertEquals(((ClickHouseArrayValue) v).allocate(1) - .setValue(0, ClickHouseLongArrayValue.of(new long[] { 1L })).asObject(), new long[][] { { 1L } }); - v = ClickHouseColumn.of("a", "Array(Array(Array(UInt8)))").newValue(config); - Assert.assertEquals(v.asObject(), new short[0][][]); - v = ClickHouseColumn.of("a", "Array(Array(Array(Nullable(UInt8))))").newValue(config); - Assert.assertEquals(v.update(new Short[][][] { new Short[][] { new Short[] { (short) 1 } } }).asObject(), - new Short[][][] { new Short[][] { new Short[] { (short) 1 } } }); - v = ClickHouseColumn.of("a", "Array(Array(Array(Array(LowCardinality(String)))))").newValue(config); - Assert.assertEquals(v.asObject(), new String[0][][][]); - - config = new ClickHouseTestDataConfig() { - @Override - public boolean isWidenUnsignedTypes() { - return false; - }; - }; - v = ClickHouseColumn.of("", "Array(UInt8)").newValue(config); - Assert.assertEquals(v.update(new byte[] { Byte.MIN_VALUE, 0, Byte.MAX_VALUE }).asObject(), - new byte[] { Byte.MIN_VALUE, 0, Byte.MAX_VALUE }); - Assert.assertEquals(v.update(new int[] { -1, 0, 1 }).asObject(), new byte[] { -1, 0, 1 }); - v = ClickHouseColumn.of("", "Array(UInt16)").newValue(config); - Assert.assertEquals(v.update(new byte[] { -1, 0, 1 }).asObject(), new short[] { 255, 0, 1 }); - Assert.assertEquals(v.update(new int[] { -1, 0, 1 }).asObject(), new short[] { -1, 0, 1 }); - v = ClickHouseColumn.of("", "Array(UInt32)").newValue(config); - Assert.assertEquals(v.update(new short[] { -1, 0, 1 }).asObject(), new int[] { 65535, 0, 1 }); - Assert.assertEquals(v.update(new int[] { -1, 0, 1 }).asObject(), new int[] { -1, 0, 1 }); - v = ClickHouseColumn.of("", "Array(UInt64)").newValue(config); - Assert.assertEquals(v.update(new int[] { -1, 0, 1 }).asObject(), new long[] { 4294967295L, 0, 1 }); - Assert.assertEquals(v.update(new long[] { -1L, 0L, 1L }).asObject(), new long[] { -1L, 0L, 1L }); - Assert.assertEquals( - v.update(new BigInteger[] { new BigInteger("18446744073709551615"), BigInteger.ZERO, BigInteger.ONE }) - .asObject(), - new long[] { -1L, 0L, 1L }); - } - - @Test(groups = { "unit" }) - public void testNewBasicValues() { - ClickHouseDataConfig config = new ClickHouseTestDataConfig() { - @Override - public boolean isWidenUnsignedTypes() { - return true; - }; - }; - for (ClickHouseDataType type : ClickHouseDataType.values()) { - // skip advanced types - if (type.isNested() || type == ClickHouseDataType.AggregateFunction - || type == ClickHouseDataType.SimpleAggregateFunction || type == ClickHouseDataType.Enum - || type == ClickHouseDataType.Nullable || type == ClickHouseDataType.BFloat16 || - type == ClickHouseDataType.Time || type == ClickHouseDataType.Time64 || type == ClickHouseDataType.QBit) { - continue; - } - - ClickHouseValue value = ClickHouseColumn.of("", type, false).newValue(config); - Assert.assertNotNull(value); - - if (type == ClickHouseDataType.Point) { - Assert.assertEquals(value.asObject(), new double[] { 0D, 0D }); - } else if (type == ClickHouseDataType.Ring) { - Assert.assertEquals(value.asObject(), new double[0][]); - } else if (type == ClickHouseDataType.Polygon) { - Assert.assertEquals(value.asObject(), new double[0][][]); - } else if (type == ClickHouseDataType.MultiPolygon) { - Assert.assertEquals(value.asObject(), new double[0][][][]); - } else { - Assert.assertNull(value.asObject()); - } - } - } - - @Test(groups = {"unit"}, dataProvider = "testJSONBinaryFormat_dp") - public void testJSONBinaryFormat(String jsonDef, int params, List predefinedPaths) throws Exception { - ClickHouseColumn column = ClickHouseColumn.of("v", jsonDef); - Assert.assertEquals(column.getNestedColumns().size(), predefinedPaths.size(), "predefined paths count mismatch"); - Assert.assertEquals(column.getParameters().size(), params, "parameters count mismatch"); - } - - @DataProvider - public Object[][] testJSONBinaryFormat_dp() { - - return new Object[][] { - {"JSON", 0, Collections.emptyList()}, - {"JSON()", 0, Collections.emptyList()}, - {"JSON(stat.name String, count Int32)", 0, Arrays.asList("stat.name", "count")}, - {"JSON(stat.name String, `comments` String)", 0, Arrays.asList("stat.name", "comments")}, - {"JSON(max_dynamic_paths=3, stat.name String, count Int8, SKIP alt_count)", 1, Arrays.asList("stat.name", "count")}, - {"JSON(max_dynamic_paths=3, stat.name String, SKIP REGEXP '^-.*')", 1, Arrays.asList("stat.name")}, - {"JSON(max_dynamic_paths=3,SKIP REGEXP '^-.*',SKIP ff, flags Array(Array(Array(Int8))), SKIP alt_count)", 1, Arrays.asList("flags")}, - {"JSON(max_dynamic_types=3,max_dynamic_paths=3, SKIP REGEXP '^-.*',SKIP ff, flags Array(Array(Array(Int8))), SKIP alt_count)", 2, Arrays.asList("flags")}, - }; - } -} +package com.clickhouse.data; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.clickhouse.data.value.ClickHouseArrayValue; +import com.clickhouse.data.value.ClickHouseGeoMultiPolygonValue; +import com.clickhouse.data.value.ClickHouseGeoPointValue; +import com.clickhouse.data.value.ClickHouseGeoPolygonValue; +import com.clickhouse.data.value.ClickHouseGeoRingValue; +import com.clickhouse.data.value.ClickHouseLongValue; +import com.clickhouse.data.value.UnsignedLong; +import com.clickhouse.data.value.array.ClickHouseLongArrayValue; + +public class ClickHouseColumnTest { + @DataProvider(name = "enumTypesProvider") + private Object[][] getEnumTypes() { + return new Object[][] { { "Enum" }, { "Enum8" }, { "Enum16" } }; + } + + @DataProvider(name = "objectTypesProvider") + private Object[][] getObjectTypes() { + return new Object[][] { + { "Tuple(not NChar Large Object)" }, + { "nchar Large Object" }, + { "Tuple(int Int32)" }, + { "a Tuple(i Int32)" }, + { "b Tuple(i1 Int32)" }, + { "Tuple(i Int32)" }, + { "Tuple(i1 Int32)" }, + { "Tuple(i Int32, a Array(Int32), m Map(LowCardinality(String), Int32))" }, + { "Int8" }, { "TINYINT SIGNED" }, + { "k1 Int8" }, { "k1 TINYINT SIGNED" }, + { "k1 Nullable(Int8)" }, { "k1 Nullable( Int8 )" }, { "k1 TINYINT SIGNED null" }, + { "k1 TINYINT SIGNED not null" }, + { "k1 LowCardinality(Nullable(String))" }, + { "k1 Tuple(k2 Int32, k3 Nullable(String), k4 TINYINT SIGNED not null, k5 Tuple (k6 UInt64))" } + }; + } + + @Test(groups = { "unit" }) + public void testReadColumn() { + String args = "AggregateFunction(max, UInt64), cc LowCardinality(Nullable(String)), a UInt8 null"; + List list = new LinkedList<>(); + Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.indexOf("cc") - 2); + Assert.assertEquals(list.size(), 1); + Assert.assertFalse(list.get(0).isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(list.get(0).getEstimatedLength(), 1); + list.clear(); + Assert.assertEquals(ClickHouseColumn.readColumn(args, args.indexOf("cc") + 3, args.length(), null, list), + args.lastIndexOf(',')); + list.clear(); + Assert.assertEquals(ClickHouseColumn.readColumn(args, args.lastIndexOf('U'), args.length(), null, list), + args.length() - 1); + Assert.assertEquals(list.size(), 1); + ClickHouseColumn column = list.get(0); + Assert.assertNotNull(column); + Assert.assertFalse(column.isLowCardinality()); + Assert.assertTrue(column.isNullable()); + Assert.assertEquals(column.getDataType(), ClickHouseDataType.UInt8); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + list.clear(); + + args = "INT1 unsigned not null, b DateTime64(3) NULL"; + Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.indexOf(',')); + Assert.assertEquals(list.size(), 1); + column = list.get(0); + Assert.assertFalse(column.isNullable()); + Assert.assertEquals(column.getDataType(), ClickHouseDataType.UInt8); + Assert.assertTrue(column.isFixedLength(), "Should have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + list.clear(); + + Assert.assertEquals(ClickHouseColumn.readColumn(args, args.indexOf('D'), args.length(), null, list), + args.length() - 1); + Assert.assertEquals(list.size(), 1); + column = list.get(0); + Assert.assertTrue(column.isNullable()); + Assert.assertEquals(column.getDataType(), ClickHouseDataType.DateTime64); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + } + + @Test(groups = { "unit" }) + public void testReadNestedColumn() { + String args = "Array(Array(Nullable(UInt8)))"; + List list = new LinkedList<>(); + Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); + Assert.assertEquals(list.size(), 1); + ClickHouseColumn column = list.get(0); + Assert.assertEquals(column.getNestedColumns().size(), 1); + Assert.assertEquals(column.getNestedColumns().get(0).getNestedColumns().size(), 1); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + list.clear(); + + args = "Array(FixedString(2))"; + Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); + Assert.assertEquals(list.size(), 1); + column = list.get(0); + Assert.assertEquals(column.getNestedColumns().size(), 1); + Assert.assertTrue(column.getArrayBaseColumn() == column.getNestedColumns().get(0), + "Nested column should be same as base column of the array"); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + column = column.getNestedColumns().get(0); + Assert.assertTrue(column.isFixedLength(), "FixedString should have fixed length in byte"); + Assert.assertEquals(column.getNestedColumns().size(), 0); + Assert.assertEquals(column.getEstimatedLength(), 2); + list.clear(); + + args = "Array(String)"; + Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); + Assert.assertEquals(list.size(), 1); + column = list.get(0); + Assert.assertEquals(column.getNestedColumns().size(), 1); + Assert.assertTrue(column.getArrayBaseColumn() == column.getNestedColumns().get(0), + "Nested column should be same as base column of the array"); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + column = column.getNestedColumns().get(0); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getNestedColumns().size(), 0); + Assert.assertEquals(column.getEstimatedLength(), 1); + list.clear(); + + args = " Tuple(Nullable(FixedString(3)), Array(UInt8),String not null) "; + Assert.assertEquals(ClickHouseColumn.readColumn(args, 1, args.length(), null, list), args.length() - 2); + Assert.assertEquals(list.size(), 1); + column = list.get(0); + Assert.assertEquals(column.getOriginalTypeName(), args.trim()); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 3); + list.clear(); + + args = "Map(UInt8 , UInt8)"; + Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); + Assert.assertEquals(list.size(), 1); + column = list.get(0); + Assert.assertEquals(column.getOriginalTypeName(), args); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + list.clear(); + + args = "Map(String, FixedString(233))"; + Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); + Assert.assertEquals(list.size(), 1); + column = list.get(0); + Assert.assertEquals(column.getOriginalTypeName(), args); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + list.clear(); + + args = "Map(String, Tuple(UInt8, Nullable(String), UInt16 null))"; + Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); + Assert.assertEquals(list.size(), 1); + column = list.get(0); + Assert.assertEquals(column.getOriginalTypeName(), args); + Assert.assertEquals(column.getNestedColumns().size(), 2); + Assert.assertEquals(column.getKeyInfo().getOriginalTypeName(), "String"); + Assert.assertEquals(column.getValueInfo().getOriginalTypeName(), "Tuple(UInt8, Nullable(String), UInt16 null)"); + Assert.assertEquals(column.getValueInfo().getNestedColumns().size(), 3); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + list.clear(); + + args = "Nested(\na Array(Nullable(UInt8)), `b b` LowCardinality(Nullable(DateTime64(3))))"; + Assert.assertEquals(ClickHouseColumn.readColumn(args, 0, args.length(), null, list), args.length()); + Assert.assertEquals(list.size(), 1); + column = list.get(0); + Assert.assertEquals(column.getOriginalTypeName(), args); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + } + + @Test(groups = { "unit" }) + public void testParse() { + ClickHouseColumn column = ClickHouseColumn.of("arr", "Nullable(Array(Nullable(UInt8))"); + Assert.assertNotNull(column); + + List list = ClickHouseColumn.parse("a String not null, b String null"); + Assert.assertEquals(list.size(), 2); + list = ClickHouseColumn.parse("a String not null, b Int8"); + Assert.assertEquals(list.size(), 2); + list = ClickHouseColumn.parse("a String, b String null"); + Assert.assertEquals(list.size(), 2); + list = ClickHouseColumn.parse("a String default 'cc', b String null"); + Assert.assertEquals(list.size(), 2); + } + + @Test(groups = { "unit" }) + public void testAggregationFunction() { + ClickHouseColumn column = ClickHouseColumn.of("aggFunc", "AggregateFunction(groupBitmap, UInt32)"); + Assert.assertTrue(column.isAggregateFunction()); + Assert.assertEquals(column.getDataType(), ClickHouseDataType.AggregateFunction); + Assert.assertEquals(column.getAggregateFunction(), ClickHouseAggregateFunction.groupBitmap); + Assert.assertEquals(column.getFunction(), "groupBitmap"); + Assert.assertEquals(column.getNestedColumns(), Collections.singletonList(ClickHouseColumn.of("", "UInt32"))); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + + column = ClickHouseColumn.of("aggFunc", "AggregateFunction(quantiles(0.5, 0.9), Nullable(UInt64))"); + Assert.assertTrue(column.isAggregateFunction()); + Assert.assertEquals(column.getDataType(), ClickHouseDataType.AggregateFunction); + Assert.assertEquals(column.getAggregateFunction(), ClickHouseAggregateFunction.quantiles); + Assert.assertEquals(column.getFunction(), "quantiles(0.5,0.9)"); + Assert.assertEquals(column.getNestedColumns(), + Collections.singletonList(ClickHouseColumn.of("", "Nullable(UInt64)"))); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + } + + @Test(groups = { "unit" }) + public void testArray() { + ClickHouseColumn column = ClickHouseColumn.of("arr", + "Array(Array(Array(Array(Array(Map(LowCardinality(String), Tuple(Array(UInt8),LowCardinality(String))))))))"); + Assert.assertTrue(column.isArray()); + Assert.assertEquals(column.getDataType(), ClickHouseDataType.Array); + Assert.assertEquals(column.getArrayNestedLevel(), 5); + Assert.assertEquals(column.getArrayBaseColumn().getOriginalTypeName(), + "Map(LowCardinality(String), Tuple(Array(UInt8),LowCardinality(String)))"); + Assert.assertFalse(column.getArrayBaseColumn().isArray()); + + Assert.assertEquals(column.getArrayBaseColumn().getArrayNestedLevel(), 0); + Assert.assertEquals(column.getArrayBaseColumn().getArrayBaseColumn(), null); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + + ClickHouseColumn c = ClickHouseColumn.of("arr", "Array(LowCardinality(Nullable(String)))"); + Assert.assertTrue(c.isArray()); + Assert.assertEquals(c.getDataType(), ClickHouseDataType.Array); + Assert.assertEquals(c.getArrayNestedLevel(), 1); + Assert.assertEquals(c.getArrayBaseColumn().getOriginalTypeName(), "LowCardinality(Nullable(String))"); + Assert.assertFalse(c.getArrayBaseColumn().isArray()); + Assert.assertFalse(column.isFixedLength(), "Should not have fixed length in byte"); + Assert.assertEquals(column.getEstimatedLength(), 1); + } + + @Test(dataProvider = "enumTypesProvider", groups = { "unit" }) + public void testEnum(String typeName) { + Assert.assertThrows(IllegalArgumentException.class, + () -> ClickHouseColumn.of("e", typeName + "('Query''Start' = a)")); + Assert.assertThrows(IllegalArgumentException.class, () -> ClickHouseColumn.of("e", typeName + "(aa,1)")); + ClickHouseColumn column = ClickHouseColumn.of("e", typeName + "('Query''Start' = 1, 'Query\\'Finish' = 10)"); + Assert.assertTrue(column.isEnum()); + Assert.assertEquals(column.getDataType(), ClickHouseDataType.of(typeName)); + Assert.assertThrows(IllegalArgumentException.class, () -> column.getEnumConstants().name(2)); + Assert.assertThrows(IllegalArgumentException.class, () -> column.getEnumConstants().value("")); + Assert.assertEquals(column.getEnumConstants().name(1), "Query'Start"); + Assert.assertEquals(column.getEnumConstants().name(10), "Query'Finish"); + Assert.assertEquals(column.getEnumConstants().value("Query'Start"), 1); + Assert.assertEquals(column.getEnumConstants().value("Query'Finish"), 10); + if (column.getDataType() != ClickHouseDataType.Enum) { // virtual type + Assert.assertTrue(column.isFixedLength(), "Should have fixed length in byte"); + } + Assert.assertEquals(column.getEstimatedLength(), column.getDataType().getByteLength()); + } + + @Test(dataProvider = "objectTypesProvider", groups = { "unit" }) + public void testObjectType(String typeName) { + ClickHouseColumn.of("o", "Tuple(TINYINT SIGNED null)"); + for (String prefix : new String[] { "Tuple(", "Tuple (", "Tuple ( " }) { + for (String suffix : new String[] { ")", " )", " ) " }) { + ClickHouseColumn innerColumn = ClickHouseColumn.of("", + typeName + suffix.substring(0, suffix.lastIndexOf(')'))); + ClickHouseColumn column = ClickHouseColumn.of("o", prefix + typeName + suffix); + Assert.assertTrue(column.isTuple()); + Assert.assertEquals(column.getNestedColumns().get(0), innerColumn); + } + } + } + + @Test(groups = { "unit" }) + public void testSimpleAggregationFunction() { + ClickHouseColumn c = ClickHouseColumn.of("a", "SimpleAggregateFunction(max, UInt64)"); + Assert.assertEquals(c.getDataType(), ClickHouseDataType.SimpleAggregateFunction); + Assert.assertEquals(c.getNestedColumns().get(0).getDataType(), ClickHouseDataType.UInt64); + + // https://github.com/ClickHouse/clickhouse-java/issues/1389 + c = ClickHouseColumn.of("a", "SimpleAggregateFunction(anyLast, Nested(a String, b String))"); + Assert.assertEquals(c.getDataType(), ClickHouseDataType.SimpleAggregateFunction); + Assert.assertEquals(c.getNestedColumns().get(0).getDataType(), ClickHouseDataType.Nested); + Assert.assertEquals(c.getNestedColumns().get(0).getNestedColumns(), + ClickHouseColumn.parse("a String, b String")); + Assert.assertEquals( + ClickHouseColumn.of("a", "SimpleAggregateFunction ( anyLast , Nested ( a String , b String ) )") + .getParameters(), + c.getParameters()); + Assert.assertEquals( + ClickHouseColumn.of("a", + "SimpleAggregateFunction(anyLast,Nested(a String,b String,`c c` Nested(d Int32, e Tuple(UInt32, Nullable(String)))))") + .getParameters(), + ClickHouseColumn.of("a", + " SimpleAggregateFunction ( /** test **/anyLast -- test\n , Nested ( a String , b String,\n\t `c c` \t Nested(d Int32, e Tuple(UInt32, Nullable(String))) ) )") + .getParameters()); + } + + @Test(groups = { "unit" }) + public void testGetObjectClassForArray() { + ClickHouseDataConfig defaultConfig = new ClickHouseTestDataConfig(); + ClickHouseDataConfig widenUnsignedConfig = new ClickHouseTestDataConfig() { + @Override + public boolean isWidenUnsignedTypes() { + return true; + }; + }; + ClickHouseDataConfig binStringConfig = new ClickHouseTestDataConfig() { + @Override + public boolean isUseBinaryString() { + return true; + }; + }; + ClickHouseDataConfig objArrayConfig = new ClickHouseTestDataConfig() { + @Override + public boolean isUseObjectsInArray() { + return true; + }; + }; + + Assert.assertEquals(ClickHouseColumn.of("a", "UInt64").getObjectClassForArray(defaultConfig), long.class); + Assert.assertEquals( + ClickHouseColumn.of("a", "Array(UInt64)").getArrayBaseColumn().getObjectClassForArray(defaultConfig), + long.class); + Assert.assertEquals(ClickHouseColumn.of("a", "UInt64").getObjectClassForArray(widenUnsignedConfig), long.class); + Assert.assertEquals(ClickHouseColumn.of("a", "Array(UInt64)").getArrayBaseColumn() + .getObjectClassForArray(widenUnsignedConfig), long.class); + Assert.assertEquals(ClickHouseColumn.of("a", "UInt64").getObjectClassForArray(objArrayConfig), + UnsignedLong.class); + Assert.assertEquals( + ClickHouseColumn.of("a", "Array(UInt64)").getArrayBaseColumn().getObjectClassForArray(objArrayConfig), + UnsignedLong.class); + + Assert.assertEquals(ClickHouseColumn.of("a", "FixedString(2)").getObjectClassForArray(defaultConfig), + String.class); + Assert.assertEquals(ClickHouseColumn.of("a", "Array(FixedString(2))").getArrayBaseColumn() + .getObjectClassForArray(defaultConfig), String.class); + Assert.assertEquals(ClickHouseColumn.of("a", "FixedString(2)").getObjectClassForArray(binStringConfig), + Object.class); + Assert.assertEquals(ClickHouseColumn.of("a", "Array(FixedString(2))").getArrayBaseColumn() + .getObjectClassForArray(binStringConfig), Object.class); + Assert.assertEquals(ClickHouseColumn.of("a", "String").getObjectClassForArray(defaultConfig), + String.class); + Assert.assertEquals(ClickHouseColumn.of("a", "Array(String)").getArrayBaseColumn() + .getObjectClassForArray(defaultConfig), String.class); + Assert.assertEquals(ClickHouseColumn.of("a", "String").getObjectClassForArray(binStringConfig), + Object.class); + Assert.assertEquals(ClickHouseColumn.of("a", "Array(String)").getArrayBaseColumn() + .getObjectClassForArray(binStringConfig), Object.class); + } + + @Test(groups = { "unit" }) + public void testNewArray() { + ClickHouseDataConfig config = new ClickHouseTestDataConfig() { + @Override + public boolean isWidenUnsignedTypes() { + return true; + }; + }; + ClickHouseValue v = ClickHouseColumn.of("a", "Array(UInt32)").newValue(config); + Assert.assertEquals(v.update(new long[] { 1L }).asObject(), new long[] { 1L }); + v = ClickHouseColumn.of("a", "Array(Nullable(UInt64))").newValue(config); + Assert.assertEquals(v.update(new Long[] { 1L }).asObject(), new Long[] { 1L }); + v = ClickHouseColumn.of("a", "Array(Array(UInt16))").newValue(config); + Assert.assertEquals(v.asObject(), new int[0][]); + v = ClickHouseColumn.of("a", "Array(UInt64)").newValue(config); + Assert.assertEquals(v.asObject(), new UnsignedLong[0]); + Assert.assertEquals(((ClickHouseLongArrayValue) v).allocate(1) + .setValue(0, ClickHouseLongValue.of(1L)).asObject(), new long[] { 1L }); + Assert.assertEquals(((ClickHouseLongArrayValue) v).allocate(1) + .setValue(0, ClickHouseLongValue.ofUnsigned(1L)).asObject(), new long[] { 1L }); + v = ClickHouseColumn.of("a", "Array(Array(UInt64))").newValue(config); + Assert.assertEquals(v.asObject(), new long[0][]); + Assert.assertEquals(((ClickHouseArrayValue) v).allocate(1) + .setValue(0, ClickHouseLongArrayValue.of(new long[] { 1L })).asObject(), new long[][] { { 1L } }); + v = ClickHouseColumn.of("a", "Array(Array(Array(UInt8)))").newValue(config); + Assert.assertEquals(v.asObject(), new short[0][][]); + v = ClickHouseColumn.of("a", "Array(Array(Array(Nullable(UInt8))))").newValue(config); + Assert.assertEquals(v.update(new Short[][][] { new Short[][] { new Short[] { (short) 1 } } }).asObject(), + new Short[][][] { new Short[][] { new Short[] { (short) 1 } } }); + v = ClickHouseColumn.of("a", "Array(Array(Array(Array(LowCardinality(String)))))").newValue(config); + Assert.assertEquals(v.asObject(), new String[0][][][]); + + config = new ClickHouseTestDataConfig() { + @Override + public boolean isWidenUnsignedTypes() { + return false; + }; + }; + v = ClickHouseColumn.of("", "Array(UInt8)").newValue(config); + Assert.assertEquals(v.update(new byte[] { Byte.MIN_VALUE, 0, Byte.MAX_VALUE }).asObject(), + new byte[] { Byte.MIN_VALUE, 0, Byte.MAX_VALUE }); + Assert.assertEquals(v.update(new int[] { -1, 0, 1 }).asObject(), new byte[] { -1, 0, 1 }); + v = ClickHouseColumn.of("", "Array(UInt16)").newValue(config); + Assert.assertEquals(v.update(new byte[] { -1, 0, 1 }).asObject(), new short[] { 255, 0, 1 }); + Assert.assertEquals(v.update(new int[] { -1, 0, 1 }).asObject(), new short[] { -1, 0, 1 }); + v = ClickHouseColumn.of("", "Array(UInt32)").newValue(config); + Assert.assertEquals(v.update(new short[] { -1, 0, 1 }).asObject(), new int[] { 65535, 0, 1 }); + Assert.assertEquals(v.update(new int[] { -1, 0, 1 }).asObject(), new int[] { -1, 0, 1 }); + v = ClickHouseColumn.of("", "Array(UInt64)").newValue(config); + Assert.assertEquals(v.update(new int[] { -1, 0, 1 }).asObject(), new long[] { 4294967295L, 0, 1 }); + Assert.assertEquals(v.update(new long[] { -1L, 0L, 1L }).asObject(), new long[] { -1L, 0L, 1L }); + Assert.assertEquals( + v.update(new BigInteger[] { new BigInteger("18446744073709551615"), BigInteger.ZERO, BigInteger.ONE }) + .asObject(), + new long[] { -1L, 0L, 1L }); + } + + @Test(groups = { "unit" }) + public void testNewBasicValues() { + ClickHouseDataConfig config = new ClickHouseTestDataConfig() { + @Override + public boolean isWidenUnsignedTypes() { + return true; + }; + }; + for (ClickHouseDataType type : java.util.EnumSet.allOf(ClickHouseDataType.class)) { + // skip advanced types + if (type.isNested() || type == ClickHouseDataType.AggregateFunction + || type == ClickHouseDataType.SimpleAggregateFunction || type == ClickHouseDataType.Enum + || type == ClickHouseDataType.Nullable || type == ClickHouseDataType.BFloat16 || + type == ClickHouseDataType.Time || type == ClickHouseDataType.Time64 || type == ClickHouseDataType.QBit) { + continue; + } + + ClickHouseValue value = ClickHouseColumn.of("", type, false).newValue(config); + Assert.assertNotNull(value); + + if (type == ClickHouseDataType.Point) { + Assert.assertEquals(value.asObject(), new double[] { 0D, 0D }); + } else if (type == ClickHouseDataType.Ring) { + Assert.assertEquals(value.asObject(), new double[0][]); + } else if (type == ClickHouseDataType.Polygon) { + Assert.assertEquals(value.asObject(), new double[0][][]); + } else if (type == ClickHouseDataType.MultiPolygon) { + Assert.assertEquals(value.asObject(), new double[0][][][]); + } else { + Assert.assertNull(value.asObject()); + } + } + } + + @Test(groups = { "unit" }) + public void testGeometryVariantOrdNumUsesArrayDimensions() { + ClickHouseColumn geometry = ClickHouseColumn.of("v", "Geometry"); + + Assert.assertEquals(geometry.getGeometryVariantOrdNum(1), + getVariantOrdNum(geometry, ClickHouseDataType.Point)); + Assert.assertEquals(geometry.getGeometryVariantOrdNum(2), + getVariantOrdNum(geometry, ClickHouseDataType.Ring)); + Assert.assertEquals(geometry.getGeometryVariantOrdNum(3), + getVariantOrdNum(geometry, ClickHouseDataType.Polygon)); + Assert.assertEquals(geometry.getGeometryVariantOrdNum(4), + getVariantOrdNum(geometry, ClickHouseDataType.MultiPolygon)); + Assert.assertEquals(geometry.getGeometryVariantOrdNum(0), -1); + Assert.assertEquals(geometry.getGeometryVariantOrdNum(5), -1); + Assert.assertEquals(geometry.getGeometryVariantOrdNum((Object) null), -1); + Assert.assertEquals(geometry.getGeometryVariantOrdNum( + ClickHouseGeoPointValue.of(new double[] { 1D, 2D })), + getVariantOrdNum(geometry, ClickHouseDataType.Point)); + Assert.assertEquals(geometry.getGeometryVariantOrdNum( + ClickHouseGeoRingValue.of(new double[][] { { 1D, 2D }, { 3D, 4D } })), + getVariantOrdNum(geometry, ClickHouseDataType.Ring)); + Assert.assertEquals(geometry.getGeometryVariantOrdNum( + ClickHouseGeoPolygonValue.of(new double[][][] { { { 1D, 2D }, { 3D, 4D } } })), + getVariantOrdNum(geometry, ClickHouseDataType.Polygon)); + Assert.assertEquals(geometry.getGeometryVariantOrdNum( + ClickHouseGeoMultiPolygonValue.of(new double[][][][] { { { { 1D, 2D }, { 3D, 4D } } } })), + getVariantOrdNum(geometry, ClickHouseDataType.MultiPolygon)); + Assert.assertEquals(geometry.getGeometryVariantOrdNum(new Object()), -1); + } + + private static int getVariantOrdNum(ClickHouseColumn column, ClickHouseDataType dataType) { + for (int i = 0; i < column.getNestedColumns().size(); i++) { + if (column.getNestedColumns().get(i).getDataType() == dataType) { + return i; + } + } + + throw new IllegalArgumentException("No nested variant type found for " + dataType); + } + + @Test(groups = {"unit"}, dataProvider = "testJSONBinaryFormat_dp") + public void testJSONBinaryFormat(String jsonDef, int params, List predefinedPaths) throws Exception { + ClickHouseColumn column = ClickHouseColumn.of("v", jsonDef); + Assert.assertEquals(column.getNestedColumns().size(), predefinedPaths.size(), "predefined paths count mismatch"); + Assert.assertEquals(column.getParameters().size(), params, "parameters count mismatch"); + } + + @DataProvider + public Object[][] testJSONBinaryFormat_dp() { + + return new Object[][] { + {"JSON", 0, Collections.emptyList()}, + {"JSON()", 0, Collections.emptyList()}, + {"JSON(stat.name String, count Int32)", 0, Arrays.asList("stat.name", "count")}, + {"JSON(stat.name String, `comments` String)", 0, Arrays.asList("stat.name", "comments")}, + {"JSON(max_dynamic_paths=3, stat.name String, count Int8, SKIP alt_count)", 1, Arrays.asList("stat.name", "count")}, + {"JSON(max_dynamic_paths=3, stat.name String, SKIP REGEXP '^-.*')", 1, Arrays.asList("stat.name")}, + {"JSON(max_dynamic_paths=3,SKIP REGEXP '^-.*',SKIP ff, flags Array(Array(Array(Int8))), SKIP alt_count)", 1, Arrays.asList("flags")}, + {"JSON(max_dynamic_types=3,max_dynamic_paths=3, SKIP REGEXP '^-.*',SKIP ff, flags Array(Array(Array(Int8))), SKIP alt_count)", 2, Arrays.asList("flags")}, + }; + } +} 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 04acf3a2b..0a26199f3 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 @@ -246,6 +246,7 @@ public T readValue(ClickHouseColumn column, Class typeHint) throws IOExce case AggregateFunction: return (T) readBitmap( actualColumn); case Variant: + case Geometry: return (T) readVariant(actualColumn); case Dynamic: return (T) readValue(actualColumn, typeHint); @@ -624,7 +625,10 @@ public ArrayValue readArray(ClickHouseColumn column) throws IOException { public ArrayValue readArrayItem(ClickHouseColumn itemTypeColumn, int len) throws IOException { ArrayValue array; - if (itemTypeColumn.isNullable() || itemTypeColumn.getDataType() == ClickHouseDataType.Variant) { + if (itemTypeColumn.isNullable() + || itemTypeColumn.getDataType() == ClickHouseDataType.Variant + || itemTypeColumn.getDataType() == ClickHouseDataType.Dynamic + || itemTypeColumn.getDataType() == ClickHouseDataType.Geometry) { array = new ArrayValue(Object.class, len); for (int i = 0; i < len; i++) { array.set(i, readValue(itemTypeColumn)); diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java index 54c1e93a8..c2432a9cf 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java @@ -77,6 +77,9 @@ public static void serializeData(OutputStream stream, Object value, ClickHouseCo case Variant: serializerVariant(stream, column, value); break; + case Geometry: + serializerGeometry(stream, column, value); + break; case Point: value = value instanceof ClickHouseGeoPointValue ? ((ClickHouseGeoPointValue)value).getValue() : value; serializeTupleData(stream, value, GEO_POINT_TUPLE); @@ -305,9 +308,12 @@ public static void writeDynamicTypeTag(OutputStream stream, ClickHouseColumn typ if (binTag == -1) { switch (dt) { case Point: + case LineString: + case MultiLineString: case Polygon: case Ring: case MultiPolygon: + case Geometry: stream.write(ClickHouseDataType.CUSTOM_TYPE_BIN_TAG); BinaryStreamUtils.writeString(stream, dt.name()); return; @@ -718,6 +724,65 @@ public static void serializerVariant(OutputStream out, ClickHouseColumn column, } } + public static void serializerGeometry(OutputStream out, ClickHouseColumn column, Object value) throws IOException { + int typeOrdNum = column.getGeometryVariantOrdNum(value); + if (typeOrdNum == -1 && value != null && value.getClass().isArray()) { + typeOrdNum = column.getGeometryVariantOrdNum(getArrayDimensions(value)); + } + if (typeOrdNum != -1) { + BinaryStreamUtils.writeUnsignedInt8(out, typeOrdNum); + serializeData(out, value, column.getNestedColumns().get(typeOrdNum)); + } else { + throw new IllegalArgumentException( + "Cannot write value of class " + value.getClass() + " into column with geometry type " + + column.getOriginalTypeName()); + } + } + + static int getArrayDimensions(Object value) { + if (value instanceof double[] || value instanceof Double[]) { + return 1; + } else if (value instanceof double[][] || value instanceof Double[][]) { + return 2; + } else if (value instanceof double[][][] || value instanceof Double[][][]) { + return 3; + } else if (value instanceof double[][][][] || value instanceof Double[][][][]) { + return 4; + } else if (value == null || !value.getClass().isArray()) { + return -1; + } + + Class componentType = value.getClass(); + int dimensions = 0; + while (componentType.isArray()) { + dimensions++; + componentType = componentType.getComponentType(); + } + + if (componentType != Object.class) { + return dimensions; + } + + Object nestedValue = firstNonNullArrayElement(value); + if (nestedValue == null) { + return dimensions; + } + + int nestedDimensions = getArrayDimensions(nestedValue); + return nestedDimensions > 0 ? dimensions + nestedDimensions : dimensions; + } + + private static Object firstNonNullArrayElement(Object value) { + for (int i = 0, len = Array.getLength(value); i < len; i++) { + Object item = Array.get(value, i); + if (item != null) { + return item; + } + } + + return null; + } + private static final ClickHouseColumn GEO_POINT_TUPLE = ClickHouseColumn.parse("geopoint Tuple(Float64, Float64)").get(0); private static final ClickHouseColumn GEO_RING_ARRAY = ClickHouseColumn.parse("georing Array(Tuple(Float64, Float64))").get(0); private static final ClickHouseColumn GEO_POLYGON_ARRAY = ClickHouseColumn.parse("geopolygin Array(Array(Tuple(Float64, Float64)))").get(0); diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java index de4830e9f..b1f6a8520 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/DataTypeConverter.java @@ -7,6 +7,7 @@ import com.clickhouse.data.ClickHouseDataType; import java.io.IOException; +import java.lang.reflect.Array; import java.net.InetAddress; import java.time.Instant; import java.time.LocalDate; @@ -63,6 +64,14 @@ public String convertToString(Object value, ClickHouseColumn column) { return ipvToString(value, column); case Array: return arrayToString(value, column); + case Point: + case Ring: + case LineString: + case Polygon: + case MultiLineString: + case MultiPolygon: + case Geometry: + return geoToString(value, column); case Variant: case Dynamic: return variantOrDynamicToString(value, column); @@ -212,6 +221,11 @@ public String arrayToString(Object value, String columnDef) { return arrayToString(value, column); } + public String geoToString(Object value, ClickHouseColumn column) { + String geoValue = tryGeoToString(value, column); + return geoValue != null ? geoValue : value.toString(); + } + /** * * @param value not null object value to convert @@ -222,9 +236,129 @@ public String variantOrDynamicToString(Object value, ClickHouseColumn column) { if (value instanceof BinaryStreamReader.ArrayValue) { return arrayToString(value, column); } + String geoValue = tryGeoToString(value, column); + if (geoValue != null) { + return geoValue; + } + if (value.getClass().isArray()) { + return arrayToString(value, column); + } return value.toString(); } + private String tryGeoToString(Object value, ClickHouseColumn column) { + if (value == null || !value.getClass().isArray()) { + return null; + } + + int dimensions = getArrayDimensions(value); + if (dimensions < 1 || dimensions > 4 || !isGeoShape(value, dimensions)) { + return null; + } + + ClickHouseDataType dataType = column.getDataType(); + if (isGeoType(dataType)) { + return geoArrayToString(value); + } + + if (dataType == ClickHouseDataType.Variant && matchesGeoVariant(column, dimensions)) { + return geoArrayToString(value); + } + + if (dataType == ClickHouseDataType.Dynamic) { + return geoArrayToString(value); + } + + return null; + } + + private boolean matchesGeoVariant(ClickHouseColumn column, int dimensions) { + for (ClickHouseColumn nestedColumn : column.getNestedColumns()) { + if (isGeoTypeForDimensions(nestedColumn.getDataType(), dimensions)) { + return true; + } + } + return false; + } + + private boolean isGeoType(ClickHouseDataType dataType) { + switch (dataType) { + case Point: + case Ring: + case LineString: + case Polygon: + case MultiLineString: + case MultiPolygon: + case Geometry: + return true; + default: + return false; + } + } + + private boolean isGeoTypeForDimensions(ClickHouseDataType dataType, int dimensions) { + switch (dimensions) { + case 1: + return dataType == ClickHouseDataType.Point; + case 2: + return dataType == ClickHouseDataType.Ring || dataType == ClickHouseDataType.LineString; + case 3: + return dataType == ClickHouseDataType.Polygon || dataType == ClickHouseDataType.MultiLineString; + case 4: + return dataType == ClickHouseDataType.MultiPolygon; + default: + return false; + } + } + + private int getArrayDimensions(Object value) { + int dimensions = 0; + Class arrayClass = value.getClass(); + while (arrayClass.isArray()) { + dimensions++; + arrayClass = arrayClass.getComponentType(); + } + return dimensions; + } + + private boolean isGeoShape(Object value, int dimensions) { + if (dimensions == 1) { + return Array.getLength(value) == 2 + && Array.get(value, 0) instanceof Double + && Array.get(value, 1) instanceof Double; + } + + for (int i = 0, len = Array.getLength(value); i < len; i++) { + Object item = Array.get(value, i); + if (item == null || !item.getClass().isArray() || !isGeoShape(item, dimensions - 1)) { + return false; + } + } + return true; + } + + private String geoArrayToString(Object value) { + int dimensions = getArrayDimensions(value); + if (dimensions == 1) { + return new StringBuilder() + .append('(') + .append(Array.get(value, 0)) + .append(',') + .append(Array.get(value, 1)) + .append(')') + .toString(); + } + + StringBuilder builder = new StringBuilder().append('['); + for (int i = 0, len = Array.getLength(value); i < len; i++) { + if (i > 0) { + builder.append(','); + } + builder.append(geoArrayToString(Array.get(value, i))); + } + return builder.append(']').toString(); + } + private static void appendEnquotedArrayElement(String value, ClickHouseColumn elementColumn, Appendable appendable) { try { if (elementColumn != null && elementColumn.getDataType() == ClickHouseDataType.String) { diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java new file mode 100644 index 000000000..981f64052 --- /dev/null +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTest.java @@ -0,0 +1,104 @@ +package com.clickhouse.client.api.data_formats.internal; + +import com.clickhouse.data.ClickHouseColumn; +import com.clickhouse.data.ClickHouseDataType; +import com.clickhouse.data.value.ClickHouseGeoPolygonValue; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.TimeZone; + +@Test(groups = {"unit"}) +public class SerializerUtilsTest { + private BinaryStreamReader newReader(byte[] data) { + return new BinaryStreamReader(new ByteArrayInputStream(data), TimeZone.getTimeZone("UTC"), null, + new BinaryStreamReader.DefaultByteBufferAllocator(), false, null); + } + + @Test + public void testGeometryRoundTrip() throws Exception { + ClickHouseColumn geometry = ClickHouseColumn.of("v", "Geometry"); + double[] point = new double[] {1.5D, 2.5D}; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + SerializerUtils.serializeData(out, point, geometry); + + Object value = newReader(out.toByteArray()).readValue(geometry); + Assert.assertEquals((double[]) value, point); + } + + @Test + public void testGeometryRoundTripWithBoxedArray() throws Exception { + ClickHouseColumn geometry = ClickHouseColumn.of("v", "Geometry"); + Double[][] ring = new Double[][] {{1D, 2D}, {3D, 4D}}; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + SerializerUtils.serializeData(out, ring, geometry); + + Object value = newReader(out.toByteArray()).readValue(geometry); + Assert.assertTrue(Arrays.deepEquals((double[][]) value, new double[][] {{1D, 2D}, {3D, 4D}})); + } + + @Test + public void testGeometryRoundTripWithMultiPolygonArray() throws Exception { + ClickHouseColumn geometry = ClickHouseColumn.of("v", "Geometry"); + double[][][][] multiPolygon = new double[][][][] {{{{1D, 2D}, {3D, 4D}}}}; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + SerializerUtils.serializeData(out, multiPolygon, geometry); + + Object value = newReader(out.toByteArray()).readValue(geometry); + Assert.assertTrue(Arrays.deepEquals((double[][][][]) value, multiPolygon)); + } + + @Test + public void testGeometryArrayDimensions() { + Assert.assertEquals(SerializerUtils.getArrayDimensions(new Double[] {1D, 2D}), 1); + Assert.assertEquals(SerializerUtils.getArrayDimensions(new Double[][] {{1D, 2D}}), 2); + Assert.assertEquals(SerializerUtils.getArrayDimensions(new Double[][][] {{{1D, 2D}}}), 3); + Assert.assertEquals(SerializerUtils.getArrayDimensions(new Double[][][][] {{{{1D, 2D}}}}), 4); + Assert.assertEquals(SerializerUtils.getArrayDimensions(new Object[] {new Double[] {1D, 2D}}), 2); + Assert.assertEquals(SerializerUtils.getArrayDimensions(new Object[] {null, new Object[] {new Double[] {1D, 2D}}}), 3); + Assert.assertEquals(SerializerUtils.getArrayDimensions(new Object[] {null, null}), 1); + Assert.assertEquals(SerializerUtils.getArrayDimensions("not an array"), -1); + Assert.assertEquals(SerializerUtils.getArrayDimensions(null), -1); + } + + @Test + public void testDynamicWithGeoCustomTypeRoundTrip() throws Exception { + ClickHouseColumn dynamic = ClickHouseColumn.of("v", "Dynamic"); + double[][][] polygon = new double[][][] {{{1D, 2D}, {3D, 4D}}}; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + SerializerUtils.serializeData(out, ClickHouseGeoPolygonValue.of(polygon), dynamic); + + Object value = newReader(out.toByteArray()).readValue(dynamic); + Assert.assertTrue(Arrays.deepEquals((double[][][]) value, polygon)); + } + + @Test + public void testDynamicTypeTagUsesCustomEncodingForGeoTypes() throws Exception { + assertCustomGeoTypeTag("LineString"); + assertCustomGeoTypeTag("MultiLineString"); + assertCustomGeoTypeTag("Geometry"); + } + + @Test + public void testGeometrySerializationRejectsUnsupportedValue() { + Assert.assertThrows(IllegalArgumentException.class, + () -> SerializerUtils.serializeData(new ByteArrayOutputStream(), "not-geometry", + ClickHouseColumn.of("v", "Geometry"))); + } + + private void assertCustomGeoTypeTag(String typeName) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + SerializerUtils.writeDynamicTypeTag(out, ClickHouseColumn.of("v", typeName)); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + Assert.assertEquals(in.read(), ClickHouseDataType.CUSTOM_TYPE_BIN_TAG & 0xFF); + Assert.assertEquals(BinaryStreamReader.readString(in), typeName); + } +} diff --git a/client-v2/src/test/java/com/clickhouse/client/api/internal/DataTypeConverterTest.java b/client-v2/src/test/java/com/clickhouse/client/api/internal/DataTypeConverterTest.java index 254f44fdd..59699addf 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/internal/DataTypeConverterTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/internal/DataTypeConverterTest.java @@ -126,4 +126,42 @@ public void testEnumToString() { assertEquals(converter.convertToString("1234567", column), "1234567"); assertEquals(converter.convertToString(2, column), "2"); } + + @Test + public void testGeoToString() { + DataTypeConverter converter = new DataTypeConverter(); + + assertEquals(converter.convertToString(new double[] {1D, 2D}, ClickHouseColumn.of("field", "Point")), "(1.0,2.0)"); + assertEquals(converter.convertToString(new double[][] {{1D, 2D}, {3D, 4D}}, ClickHouseColumn.of("field", "Ring")), + "[(1.0,2.0),(3.0,4.0)]"); + assertEquals(converter.convertToString(new double[][][] {{{1D, 2D}, {3D, 4D}}}, ClickHouseColumn.of("field", "Polygon")), + "[[(1.0,2.0),(3.0,4.0)]]"); + assertEquals( + converter.convertToString(new double[][][][] {{{{1D, 2D}, {3D, 4D}}}}, ClickHouseColumn.of("field", "MultiPolygon")), + "[[[(1.0,2.0),(3.0,4.0)]]]"); + assertEquals(converter.convertToString(new double[] {1D, 2D}, ClickHouseColumn.of("field", "Geometry")), "(1.0,2.0)"); + } + + @Test + public void testVariantOrDynamicGeoToString() { + DataTypeConverter converter = new DataTypeConverter(); + + assertEquals(converter.convertToString(new double[] {1D, 2D}, ClickHouseColumn.of("field", "Variant(String, Point)")), + "(1.0,2.0)"); + assertEquals( + converter.convertToString(new double[][] {{1D, 2D}, {3D, 4D}}, ClickHouseColumn.of("field", "Dynamic")), + "[(1.0,2.0),(3.0,4.0)]"); + assertEquals( + converter.convertToString(new double[][][] {{{1D, 2D}, {3D, 4D}}}, ClickHouseColumn.of("field", "Variant(String, Polygon)")), + "[[(1.0,2.0),(3.0,4.0)]]"); + assertEquals( + converter.convertToString(new double[][][][] {{{{1D, 2D}, {3D, 4D}}}}, ClickHouseColumn.of("field", "Dynamic")), + "[[[(1.0,2.0),(3.0,4.0)]]]"); + assertEquals( + converter.convertToString(new int[] {1, 2, 3}, ClickHouseColumn.of("field", "Variant(String, Array(Int32))")), + "[1, 2, 3]"); + assertEquals( + converter.convertToString(new double[][] {{1D, 2D, 3D}}, ClickHouseColumn.of("field", "Dynamic")), + "[[1.0, 2.0, 3.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 2045a0d29..09e65f12f 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 @@ -32,7 +32,6 @@ import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.RoundingMode; -import java.sql.Connection; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; @@ -1061,6 +1060,109 @@ public static Object[][] testDataTypesAsStringDP() { }; } + @Test(groups = {"integration"}) + public void testGeometryReadFromTable() throws Exception { + if (isVersionMatch("(,25.10]")) { + return; + } + + final String table = "test_geometry_read"; + final CommandSettings geometrySettings = (CommandSettings) new CommandSettings() + .serverSetting("allow_suspicious_variant_types", "1"); + final Object[] expectedValues = new Object[] { + new double[] {1D, 2D}, + new double[][] {{1D, 2D}, {3D, 4D}, {1D, 2D}}, + new double[][][] {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + new double[][][][] { + {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + {{{5D, 6D}, {7D, 8D}, {5D, 6D}}} + } + }; + + client.execute("DROP TABLE IF EXISTS " + table).get().close(); + client.execute(tableDefinition(table, "rowId Int32", "geom Geometry"), geometrySettings).get().close(); + client.execute("INSERT INTO " + table + " VALUES " + + "(0, (1, 2)), " + + "(1, [(1, 2), (3, 4), (1, 2)]), " + + "(2, [[(1, 2), (3, 4), (1, 2)]]), " + + "(3, [[[(1, 2), (3, 4), (1, 2)]], [[(5, 6), (7, 8), (5, 6)]]])").get().close(); + + List records = client.queryAll("SELECT * FROM " + table + " ORDER BY rowId"); + Assert.assertEquals(records.size(), expectedValues.length); + for (GenericRecord record : records) { + int rowId = record.getInteger("rowId"); + Object actual = record.getObject("geom"); + Assert.assertNotNull(record.getString("geom")); + assertGeometryValue(actual, expectedValues[rowId]); + } + + try (QueryResponse response = client.query("SELECT * FROM " + table + " ORDER BY rowId").get()) { + ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); + int rowId = 0; + while (reader.next() != null) { + Object actual = reader.readValue("geom"); + Assert.assertNotNull(reader.getString("geom")); + assertGeometryValue(actual, expectedValues[rowId]); + rowId++; + } + Assert.assertEquals(rowId, expectedValues.length); + } + } + + @Data + @AllArgsConstructor + public static class DTOForGeometryTests { + private int rowId; + private Object geom; + } + + @Test(groups = {"integration"}) + public void testGeometryWriteToTable() throws Exception { + if (isVersionMatch("(,25.10]")) { + return; + } + + final String table = "test_geometry_write"; + final CommandSettings geometrySettings = (CommandSettings) new CommandSettings() + .serverSetting("allow_suspicious_variant_types", "1"); + final Object[] valuesToInsert = new Object[] { + new Double[] {1D, 2D}, + new Double[][] {{1D, 2D}, {3D, 4D}, {1D, 2D}}, + new Double[][][] {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + new Double[][][][] { + {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + {{{5D, 6D}, {7D, 8D}, {5D, 6D}}} + } + }; + final Object[] expectedValues = new Object[] { + new double[] {1D, 2D}, + new double[][] {{1D, 2D}, {3D, 4D}, {1D, 2D}}, + new double[][][] {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + new double[][][][] { + {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + {{{5D, 6D}, {7D, 8D}, {5D, 6D}}} + } + }; + + client.execute("DROP TABLE IF EXISTS " + table).get().close(); + client.execute(tableDefinition(table, "rowId Int32", "geom Geometry"), geometrySettings).get().close(); + client.register(DTOForGeometryTests.class, client.getTableSchema(table)); + + List data = new ArrayList<>(); + for (int i = 0; i < valuesToInsert.length; i++) { + data.add(new DTOForGeometryTests(i, valuesToInsert[i])); + } + client.insert(table, data).get().close(); + + List records = client.queryAll("SELECT * FROM " + table + " ORDER BY rowId"); + Assert.assertEquals(records.size(), expectedValues.length); + for (GenericRecord record : records) { + int rowId = record.getInteger("rowId"); + Assert.assertNotNull(record.getString("geom")); + assertGeometryValue(record.getObject("geom"), expectedValues[rowId]); + } + } + @Test(groups = {"integration"}) public void testDates() throws Exception { LocalDate date = LocalDate.of(2024, 1, 15); @@ -1643,6 +1745,23 @@ public static String tableDefinition(String table, String... columns) { return sb.toString(); } + private static void assertGeometryValue(Object actual, Object expected) { + Assert.assertNotNull(actual); + Assert.assertEquals(actual.getClass(), expected.getClass()); + + if (expected instanceof double[]) { + Assert.assertEquals((double[]) actual, (double[]) expected); + } else if (expected instanceof double[][]) { + Assert.assertTrue(Arrays.deepEquals((double[][]) actual, (double[][]) expected)); + } else if (expected instanceof double[][][]) { + Assert.assertTrue(Arrays.deepEquals((double[][][]) actual, (double[][][]) expected)); + } else if (expected instanceof double[][][][]) { + Assert.assertTrue(Arrays.deepEquals((double[][][][]) actual, (double[][][][]) expected)); + } else { + Assert.fail("Unexpected geometry type: " + expected.getClass()); + } + } + private boolean isVersionMatch(String versionExpression) { List serverVersion = client.queryAll("SELECT version()"); return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java index 4d8454e31..e57282967 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -1504,6 +1504,7 @@ public T getObjectImpl(String columnLabel, Class type, Map> getDataTypeClassMap() { break; case IPv4: case IPv6: + case Geometry: // should be mapped to Object because require conversion. default: map.put(e.getKey(), Object.class); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java index 7de1201ea..e282c4e9c 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcDataTypeTests.java @@ -1,2720 +1,2885 @@ -package com.clickhouse.jdbc; - -import com.clickhouse.client.api.ClientConfigProperties; -import com.clickhouse.client.api.DataTypeUtils; -import com.clickhouse.client.api.internal.ServerSettings; -import com.clickhouse.client.api.sql.SQLUtils; -import com.clickhouse.data.ClickHouseDataType; -import com.clickhouse.data.ClickHouseVersion; -import com.clickhouse.data.Tuple; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testng.Assert; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.sql.Array; -import java.sql.Connection; -import java.sql.Date; -import java.sql.JDBCType; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Struct; -import java.sql.Time; -import java.sql.Timestamp; -import java.sql.Types; -import java.text.DecimalFormat; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Random; -import java.util.TimeZone; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertThrows; -import static org.testng.Assert.assertTrue; - -@Test(groups = { "integration" }) -public class JdbcDataTypeTests extends JdbcIntegrationTest { - private static final Logger log = LoggerFactory.getLogger(JdbcDataTypeTests.class); - - @BeforeClass(groups = { "integration" }) - public static void setUp() throws SQLException { - Driver.load(); - } - - private int insertData(String sql) throws SQLException { - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - return stmt.executeUpdate(sql); - } - } - } - - @Test(groups = { "integration" }) - public void testIntegerTypes() throws SQLException { - runQuery("CREATE TABLE test_integers (order Int8, " - + "int8 Int8, int16 Int16, int32 Int32, int64 Int64, int128 Int128, int256 Int256, " - + "uint8 UInt8, uint16 UInt16, uint32 UInt32, uint64 UInt64, uint128 UInt128, uint256 UInt256" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert minimum values - insertData("INSERT INTO test_integers VALUES ( 1, " - + "-128, -32768, -2147483648, -9223372036854775808, -170141183460469231731687303715884105728, -57896044618658097711785492504343953926634992332820282019728792003956564819968, " - + "0, 0, 0, 0, 0, 0" - + ")"); - - // Insert maximum values - insertData("INSERT INTO test_integers VALUES ( 2, " - + "127, 32767, 2147483647, 9223372036854775807, 170141183460469231731687303715884105727, 57896044618658097711785492504343953926634992332820282019728792003956564819967, " - + "255, 65535, 4294967295, 18446744073709551615, 340282366920938463463374607431768211455, 115792089237316195423570985008687907853269984665640564039457584007913129639935" - + ")"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - int int8 = rand.nextInt(256) - 128; - int int16 = rand.nextInt(65536) - 32768; - int int32 = rand.nextInt(); - long int64 = rand.nextLong(); - BigInteger int128 = new BigInteger(127, rand); - BigInteger int256 = new BigInteger(255, rand); - Short uint8 = Integer.valueOf(rand.nextInt(256)).shortValue(); - int uint16 = rand.nextInt(65536); - long uint32 = rand.nextInt() & 0xFFFFFFFFL; - BigInteger uint64 = BigInteger.valueOf(rand.nextLong(Long.MAX_VALUE)); - BigInteger uint128 = new BigInteger(128, rand); - BigInteger uint256 = new BigInteger(256, rand); - - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_integers VALUES ( 3, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { - stmt.setInt(1, int8); - stmt.setInt(2, int16); - stmt.setInt(3, int32); - stmt.setLong(4, int64); - stmt.setBigDecimal(5, new BigDecimal(int128)); - stmt.setBigDecimal(6, new BigDecimal(int256)); - stmt.setInt(7, uint8); - stmt.setInt(8, uint16); - stmt.setLong(9, uint32); - stmt.setBigDecimal(10, new BigDecimal(uint64)); - stmt.setBigDecimal(11, new BigDecimal(uint128)); - stmt.setBigDecimal(12, new BigDecimal(uint256)); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_integers ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getByte("int8"), Byte.MIN_VALUE); - assertEquals(rs.getShort("int16"), Short.MIN_VALUE); - assertEquals(rs.getInt("int32"), Integer.MIN_VALUE); - assertEquals(rs.getLong("int64"), Long.MIN_VALUE); - assertEquals(rs.getBigDecimal("int128"), new BigDecimal("-170141183460469231731687303715884105728")); - assertEquals(rs.getBigDecimal("int256"), new BigDecimal("-57896044618658097711785492504343953926634992332820282019728792003956564819968")); - assertEquals(rs.getShort("uint8"), 0); - assertEquals(rs.getInt("uint16"), 0); - assertEquals(rs.getLong("uint32"), 0); - assertEquals(rs.getBigDecimal("uint64"), new BigDecimal("0")); - assertEquals(rs.getBigDecimal("uint128"), new BigDecimal("0")); - assertEquals(rs.getBigDecimal("uint256"), new BigDecimal("0")); - - assertTrue(rs.next()); - assertEquals(rs.getByte("int8"), Byte.MAX_VALUE); - assertEquals(rs.getShort("int16"), Short.MAX_VALUE); - assertEquals(rs.getInt("int32"), Integer.MAX_VALUE); - assertEquals(rs.getLong("int64"), Long.MAX_VALUE); - assertEquals(rs.getBigDecimal("int128"), new BigDecimal("170141183460469231731687303715884105727")); - assertEquals(rs.getBigDecimal("int256"), new BigDecimal("57896044618658097711785492504343953926634992332820282019728792003956564819967")); - assertEquals(rs.getShort("uint8"), 255); - assertEquals(rs.getInt("uint16"), 65535); - assertEquals(rs.getLong("uint32"), 4294967295L); - assertEquals(rs.getBigDecimal("uint64"), new BigDecimal("18446744073709551615")); - assertEquals(rs.getBigDecimal("uint128"), new BigDecimal("340282366920938463463374607431768211455")); - assertEquals(rs.getBigDecimal("uint256"), new BigDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935")); - - assertTrue(rs.next()); - assertEquals(rs.getByte("int8"), int8); - assertEquals(rs.getShort("int16"), int16); - assertEquals(rs.getInt("int32"), int32); - assertEquals(rs.getLong("int64"), int64); - assertEquals(rs.getBigDecimal("int128"), new BigDecimal(int128)); - assertEquals(rs.getBigDecimal("int256"), new BigDecimal(int256)); - assertEquals(rs.getShort("uint8"), uint8); - assertEquals(rs.getInt("uint16"), uint16); - assertEquals(rs.getLong("uint32"), uint32); - assertEquals(rs.getBigDecimal("uint64"), new BigDecimal(uint64)); - assertEquals(rs.getBigDecimal("uint128"), new BigDecimal(uint128)); - assertEquals(rs.getBigDecimal("uint256"), new BigDecimal(uint256)); - - assertFalse(rs.next()); - } - } - } - - // Check the with getObject - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_integers ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getObject("int8"), Byte.MIN_VALUE); - assertEquals(rs.getObject("int16"), Short.MIN_VALUE); - assertEquals(rs.getObject("int32"), Integer.MIN_VALUE); - assertEquals(rs.getObject("int64"), Long.MIN_VALUE); - assertEquals(rs.getObject("int128"), new BigInteger("-170141183460469231731687303715884105728")); - assertEquals(rs.getObject("int256"), new BigInteger("-57896044618658097711785492504343953926634992332820282019728792003956564819968")); - assertEquals(rs.getObject("uint8"), Short.valueOf("0")); - assertEquals(rs.getObject("uint16"), 0); - assertEquals(rs.getObject("uint32"), 0L); - assertEquals(rs.getObject("uint64"), new BigInteger("0")); - assertEquals(rs.getObject("uint128"), new BigInteger("0")); - assertEquals(rs.getObject("uint256"), new BigInteger("0")); - - assertTrue(rs.next()); - assertEquals(rs.getObject("int8"), Byte.MAX_VALUE); - assertEquals(rs.getObject("int16"), Short.MAX_VALUE); - assertEquals(rs.getObject("int32"), Integer.MAX_VALUE); - assertEquals(rs.getObject("int64"), Long.MAX_VALUE); - assertEquals(rs.getObject("int128"), new BigInteger("170141183460469231731687303715884105727")); - assertEquals(rs.getObject("int256"), new BigInteger("57896044618658097711785492504343953926634992332820282019728792003956564819967")); - assertEquals(rs.getObject("uint8"), Short.valueOf("255")); - assertEquals(rs.getObject("uint16"), 65535); - assertEquals(rs.getObject("uint32"), 4294967295L); - assertEquals(rs.getObject("uint64"), new BigInteger("18446744073709551615")); - assertEquals(rs.getObject("uint128"), new BigInteger("340282366920938463463374607431768211455")); - assertEquals(rs.getObject("uint256"), new BigInteger("115792089237316195423570985008687907853269984665640564039457584007913129639935")); - - assertTrue(rs.next()); - assertEquals(rs.getObject("int8"), (byte)int8); - assertEquals(rs.getObject("int16"), (short)int16); - assertEquals(rs.getObject("int32"), int32); - assertEquals(rs.getObject("int64"), int64); - assertEquals(rs.getObject("int128"), int128); - assertEquals(rs.getObject("int256"), int256); - assertEquals(rs.getObject("uint8"), uint8); - assertEquals(rs.getObject("uint16"), uint16); - assertEquals(rs.getObject("uint32"), uint32); - assertEquals(rs.getObject("uint64"), uint64); - assertEquals(rs.getObject("uint128"), uint128); - assertEquals(rs.getObject("uint256"), uint256); - - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testBigIntegerTypesMapping() throws SQLException { - String tableName = "test_biginteger_mapping"; - - // Create table with all large integer types - runQuery("CREATE TABLE " + tableName + " (" - + "id Int32, " - + "int128_col Int128, " - + "int256_col Int256, " - + "uint64_col UInt64, " - + "uint128_col UInt128, " - + "uint256_col UInt256, " - + "int128_null Nullable(Int128), " - + "int256_null Nullable(Int256), " - + "uint64_null Nullable(UInt64), " - + "uint128_null Nullable(UInt128), " - + "uint256_null Nullable(UInt256)" - + ") ENGINE = MergeTree ORDER BY id"); - - // Test values - BigInteger int128Min = new BigInteger("-170141183460469231731687303715884105728"); // -2^127 - BigInteger int128Max = new BigInteger("170141183460469231731687303715884105727"); // 2^127 - 1 - BigInteger int256Min = new BigInteger("-57896044618658097711785492504343953926634992332820282019728792003956564819968"); // -2^255 - BigInteger int256Max = new BigInteger("57896044618658097711785492504343953926634992332820282019728792003956564819967"); // 2^255 - 1 - BigInteger uint64Max = new BigInteger("18446744073709551615"); // 2^64 - 1 - BigInteger uint128Max = new BigInteger("340282366920938463463374607431768211455"); // 2^128 - 1 - BigInteger uint256Max = new BigInteger("115792089237316195423570985008687907853269984665640564039457584007913129639935"); // 2^256 - 1 - - // Insert minimum values - insertData("INSERT INTO " + tableName + " VALUES (" - + "1, " - + int128Min + ", " + int256Min + ", 0, 0, 0, " - + "NULL, NULL, NULL, NULL, NULL" - + ")"); - - // Insert maximum values - insertData("INSERT INTO " + tableName + " VALUES (" - + "2, " - + int128Max + ", " + int256Max + ", " + uint64Max + ", " + uint128Max + ", " + uint256Max + ", " - + "NULL, NULL, NULL, NULL, NULL" - + ")"); - - // Insert random values with PreparedStatement - Random rand = new Random(System.currentTimeMillis()); - BigInteger int128Random = new BigInteger(127, rand); - BigInteger int256Random = new BigInteger(255, rand); - BigInteger uint64Random = BigInteger.valueOf(rand.nextLong(Long.MAX_VALUE)); - BigInteger uint128Random = new BigInteger(128, rand); - BigInteger uint256Random = new BigInteger(256, rand); - - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement pstmt = conn.prepareStatement( - "INSERT INTO " + tableName + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { - pstmt.setInt(1, 3); - pstmt.setObject(2, int128Random); - pstmt.setObject(3, int256Random); - pstmt.setObject(4, uint64Random); - pstmt.setObject(5, uint128Random); - pstmt.setObject(6, uint256Random); - pstmt.setObject(7, int128Random); - pstmt.setObject(8, int256Random); - pstmt.setObject(9, uint64Random); - pstmt.setObject(10, uint128Random); - pstmt.setObject(11, uint256Random); - pstmt.executeUpdate(); - } - } - - // Verify results and metadata - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName + " ORDER BY id")) { - ResultSetMetaData meta = rs.getMetaData(); - - // Verify metadata for each large integer column - // Int128 - assertEquals(meta.getColumnType(2), Types.NUMERIC, "Int128 should map to Types.NUMERIC"); - assertEquals(meta.getColumnTypeName(2), "Int128", "Int128 column type name"); - assertEquals(meta.getColumnClassName(2), BigInteger.class.getName(), "Int128 should map to BigInteger class"); - - // Int256 - assertEquals(meta.getColumnType(3), Types.NUMERIC, "Int256 should map to Types.NUMERIC"); - assertEquals(meta.getColumnTypeName(3), "Int256", "Int256 column type name"); - assertEquals(meta.getColumnClassName(3), BigInteger.class.getName(), "Int256 should map to BigInteger class"); - - // UInt64 - assertEquals(meta.getColumnType(4), Types.NUMERIC, "UInt64 should map to Types.NUMERIC"); - assertEquals(meta.getColumnTypeName(4), "UInt64", "UInt64 column type name"); - assertEquals(meta.getColumnClassName(4), BigInteger.class.getName(), "UInt64 should map to BigInteger class"); - - // UInt128 - assertEquals(meta.getColumnType(5), Types.NUMERIC, "UInt128 should map to Types.NUMERIC"); - assertEquals(meta.getColumnTypeName(5), "UInt128", "UInt128 column type name"); - assertEquals(meta.getColumnClassName(5), BigInteger.class.getName(), "UInt128 should map to BigInteger class"); - - // UInt256 - assertEquals(meta.getColumnType(6), Types.NUMERIC, "UInt256 should map to Types.NUMERIC"); - assertEquals(meta.getColumnTypeName(6), "UInt256", "UInt256 column type name"); - assertEquals(meta.getColumnClassName(6), BigInteger.class.getName(), "UInt256 should map to BigInteger class"); - - // Verify first row (minimum values) - assertTrue(rs.next(), "Should have first row"); - assertEquals(rs.getInt("id"), 1); - - // Verify that getObject() returns BigInteger instances - Object int128Obj = rs.getObject("int128_col"); - assertTrue(int128Obj instanceof BigInteger, "Int128 getObject() should return BigInteger, got: " + int128Obj.getClass().getName()); - assertEquals(int128Obj, int128Min, "Int128 min value"); - - Object int256Obj = rs.getObject("int256_col"); - assertTrue(int256Obj instanceof BigInteger, "Int256 getObject() should return BigInteger, got: " + int256Obj.getClass().getName()); - assertEquals(int256Obj, int256Min, "Int256 min value"); - - Object uint64Obj = rs.getObject("uint64_col"); - assertTrue(uint64Obj instanceof BigInteger, "UInt64 getObject() should return BigInteger, got: " + uint64Obj.getClass().getName()); - assertEquals(uint64Obj, BigInteger.ZERO, "UInt64 zero value"); - - Object uint128Obj = rs.getObject("uint128_col"); - assertTrue(uint128Obj instanceof BigInteger, "UInt128 getObject() should return BigInteger, got: " + uint128Obj.getClass().getName()); - assertEquals(uint128Obj, BigInteger.ZERO, "UInt128 zero value"); - - Object uint256Obj = rs.getObject("uint256_col"); - assertTrue(uint256Obj instanceof BigInteger, "UInt256 getObject() should return BigInteger, got: " + uint256Obj.getClass().getName()); - assertEquals(uint256Obj, BigInteger.ZERO, "UInt256 zero value"); - - // Verify nullable columns - assertNull(rs.getObject("int128_null"), "Nullable Int128 should be null"); - assertNull(rs.getObject("int256_null"), "Nullable Int256 should be null"); - assertNull(rs.getObject("uint64_null"), "Nullable UInt64 should be null"); - assertNull(rs.getObject("uint128_null"), "Nullable UInt128 should be null"); - assertNull(rs.getObject("uint256_null"), "Nullable UInt256 should be null"); - - // Verify second row (maximum values) - assertTrue(rs.next(), "Should have second row"); - assertEquals(rs.getInt("id"), 2); - assertEquals(rs.getObject("int128_col"), int128Max, "Int128 max value"); - assertEquals(rs.getObject("int256_col"), int256Max, "Int256 max value"); - assertEquals(rs.getObject("uint64_col"), uint64Max, "UInt64 max value"); - assertEquals(rs.getObject("uint128_col"), uint128Max, "UInt128 max value"); - assertEquals(rs.getObject("uint256_col"), uint256Max, "UInt256 max value"); - - // Verify third row (random values) - assertTrue(rs.next(), "Should have third row"); - assertEquals(rs.getInt("id"), 3); - assertEquals(rs.getObject("int128_col"), int128Random, "Int128 random value"); - assertEquals(rs.getObject("int256_col"), int256Random, "Int256 random value"); - assertEquals(rs.getObject("uint64_col"), uint64Random, "UInt64 random value"); - assertEquals(rs.getObject("uint128_col"), uint128Random, "UInt128 random value"); - assertEquals(rs.getObject("uint256_col"), uint256Random, "UInt256 random value"); - - // Verify that nullable columns contain the inserted values - assertEquals(rs.getObject("int128_null"), int128Random, "Nullable Int128 with value"); - assertEquals(rs.getObject("int256_null"), int256Random, "Nullable Int256 with value"); - assertEquals(rs.getObject("uint64_null"), uint64Random, "Nullable UInt64 with value"); - assertEquals(rs.getObject("uint128_null"), uint128Random, "Nullable UInt128 with value"); - assertEquals(rs.getObject("uint256_null"), uint256Random, "Nullable UInt256 with value"); - - assertFalse(rs.next(), "Should have no more rows"); - } - } - } - - // Additional test: verify getObject(index, BigInteger.class) works correctly - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT int128_col, int256_col, uint64_col, uint128_col, uint256_col FROM " + tableName + " WHERE id = 2")) { - assertTrue(rs.next()); - - // Verify that getObject with specific class works - assertEquals(rs.getObject(1, BigInteger.class), int128Max, "getObject(index, BigInteger.class) for Int128"); - assertEquals(rs.getObject(2, BigInteger.class), int256Max, "getObject(index, BigInteger.class) for Int256"); - assertEquals(rs.getObject(3, BigInteger.class), uint64Max, "getObject(index, BigInteger.class) for UInt64"); - assertEquals(rs.getObject(4, BigInteger.class), uint128Max, "getObject(index, BigInteger.class) for UInt128"); - assertEquals(rs.getObject(5, BigInteger.class), uint256Max, "getObject(index, BigInteger.class) for UInt256"); - - assertFalse(rs.next()); - } - } - } - - log.info("BigInteger types mapping test completed successfully"); - } - - @Test(groups = { "integration" }) - public void testUnsignedIntegerTypes() throws Exception { - Random rand = new Random(); - runQuery("CREATE TABLE test_unsigned_integers (order Int8, " - + "uint8 Nullable(UInt8), " - + "uint16 Nullable(UInt16), " - + "uint32 Nullable(UInt32), " - + "uint64 Nullable(UInt64), " - + "uint128 Nullable(UInt128), " - + "uint256 Nullable(UInt256)" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert null values - insertData("INSERT INTO test_unsigned_integers VALUES ( 1, " - + "NULL, NULL, NULL, NULL, NULL, NULL)"); - - // Insert minimum values - insertData("INSERT INTO test_unsigned_integers VALUES ( 2, " - + "0, 0, 0, 0, 0, 0)"); - - // Insert random values - int uint8 = rand.nextInt(256); - int uint16 = rand.nextInt(65536); - long uint32 = rand.nextLong() & 0xFFFFFFFFL; - long uint64 = rand.nextLong() & 0xFFFFFFFFFFFFL; - BigInteger uint128 = new BigInteger(38, rand); - BigInteger uint256 = new BigInteger(77, rand); - insertData("INSERT INTO test_unsigned_integers VALUES ( 3, " - + uint8 + ", " + uint16 + ", " + uint32 + ", " + uint64 + ", " + uint128 + ", " + uint256 + ")"); - - try (Connection conn = getJdbcConnection(); - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT uint8, uint16, uint32, uint64, uint128, uint256 FROM test_unsigned_integers ORDER BY order")) { - - List> expectedTypes = Arrays.asList( - Short.class, Integer.class, Long.class, BigInteger.class, BigInteger.class, BigInteger.class); - List> actualTypes = new ArrayList<>(); - ResultSetMetaData rsmd = rs.getMetaData(); - for (int i = 0; i < rsmd.getColumnCount(); i++) { - actualTypes.add(Class.forName(rsmd.getColumnClassName(i + 1))); - } - assertEquals(actualTypes, expectedTypes); - - - assertTrue(rs.next()); - assertEquals(rs.getObject("uint8"), null); - assertEquals(rs.getObject("uint16"), null); - assertEquals(rs.getObject("uint32"), null); - assertEquals(rs.getObject("uint64"), null); - assertEquals(rs.getObject("uint128"), null); - assertEquals(rs.getObject("uint256"), null); - - assertTrue(rs.next()); - assertEquals((Short) rs.getObject("uint8"), (byte) 0); - assertEquals((Integer) rs.getObject("uint16"), (short) 0); - assertEquals((Long) rs.getObject("uint32"), 0); - assertEquals(rs.getObject("uint64"), BigInteger.ZERO); - assertEquals(rs.getObject("uint128"), BigInteger.ZERO); - assertEquals(rs.getObject("uint256"), BigInteger.ZERO); - - assertTrue(rs.next()); - assertEquals(((Short) rs.getObject("uint8")).intValue(), uint8); - assertEquals((Integer) rs.getObject("uint16"), uint16); - assertEquals((Long) rs.getObject("uint32"), uint32); - assertEquals(rs.getObject("uint64"), BigInteger.valueOf(uint64)); - assertEquals(rs.getObject("uint128"), uint128); - assertEquals(rs.getObject("uint256"), uint256); - - assertFalse(rs.next()); - } - } - - - @Test(groups = { "integration" }) - public void testUUIDTypes() throws Exception { - Random rand = new Random(); - runQuery("CREATE TABLE test_uuids (order Int8, " - + "uuid Nullable(UUID) " - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert null values - insertData("INSERT INTO test_uuids VALUES ( 1, NULL)"); - - // Insert random values - UUID uuid = UUID.randomUUID(); - insertData("INSERT INTO test_uuids VALUES ( 2, " - + "'" + uuid + "')"); - - try (Connection conn = getJdbcConnection(); - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT uuid FROM test_uuids ORDER BY order")) { - - assertTrue(rs.next()); - assertNull(rs.getObject("uuid")); - - - assertTrue(rs.next()); - assertEquals(rs.getObject("uuid"), uuid); - - assertFalse(rs.next()); - } - } - - - @Test(groups = {"integration"}) - public void testArrayOfUUID() throws Exception { - try (Connection connection = getJdbcConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT '2d1f626d-eb07-4c81-be3d-ac1173f0d018'::UUID f_elem, ['2d1f626d-eb07-4c81-be3d-ac1173f0d018']::Array(UUID) arr")) { - - Assert.assertTrue(rs.next()); - UUID fElem = (UUID) rs.getObject(1); - Array colValue = rs.getArray(2); - Object[] arr = (Object[]) colValue.getArray(); - Assert.assertEquals(fElem, arr[0]); - - ResultSet arrRs = colValue.getResultSet(); - arrRs.next(); - Assert.assertEquals(fElem, arrRs.getObject(2)); - } - } - - @Test(groups = { "integration" }) - public void testDecimalTypes() throws SQLException { - runQuery("CREATE TABLE test_decimals (order Int8, " - + "dec Decimal(9, 2), dec32 Decimal32(4), dec64 Decimal64(8), dec128 Decimal128(18), dec256 Decimal256(18)" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert minimum values - insertData("INSERT INTO test_decimals VALUES ( 1, -9999999.99, -99999.9999, -9999999999.99999999, -99999999999999999999.999999999999999999, " + - "-9999999999999999999999999999999999999999999999999999999999.999999999999999999)"); - - // Insert maximum values - insertData("INSERT INTO test_decimals VALUES ( 2, 9999999.99, 99999.9999, 9999999999.99999999, 99999999999999999999.999999999999999999, " + - "9999999999999999999999999999999999999999999999999999999999.999999999999999999)"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - BigDecimal dec = new BigDecimal(new BigInteger(7, rand) + "." + rand.nextInt(10,100));//P - S; 9 - 2 - BigDecimal dec32 = new BigDecimal(new BigInteger(5, rand) + "." + rand.nextInt(1000, 10000)); - BigDecimal dec64 = new BigDecimal(new BigInteger(18, rand) + "." + rand.nextInt(10000000, 100000000)); - BigDecimal dec128 = new BigDecimal(new BigInteger(20, rand) + "." + rand.nextLong(100000000000000000L, 1000000000000000000L)); - BigDecimal dec256 = new BigDecimal(new BigInteger(58, rand) + "." + rand.nextLong(100000000000000000L, 1000000000000000000L)); - - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_decimals VALUES ( 3, ?, ?, ?, ?, ?)")) { - stmt.setBigDecimal(1, dec); - stmt.setBigDecimal(2, dec32); - stmt.setBigDecimal(3, dec64); - stmt.setBigDecimal(4, dec128); - stmt.setBigDecimal(5, dec256); - stmt.executeUpdate(); - } - } - - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_decimals ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getBigDecimal("dec"), new BigDecimal("-9999999.99")); - assertEquals(rs.getBigDecimal("dec32"), new BigDecimal("-99999.9999")); - assertEquals(rs.getBigDecimal("dec64"), new BigDecimal("-9999999999.99999999")); - assertEquals(rs.getBigDecimal("dec128"), new BigDecimal("-99999999999999999999.999999999999999999")); - assertEquals(rs.getBigDecimal("dec256"), new BigDecimal("-9999999999999999999999999999999999999999999999999999999999.999999999999999999")); - - assertTrue(rs.next()); - assertEquals(rs.getBigDecimal("dec"), new BigDecimal("9999999.99")); - assertEquals(rs.getBigDecimal("dec32"), new BigDecimal("99999.9999")); - assertEquals(rs.getBigDecimal("dec64"), new BigDecimal("9999999999.99999999")); - assertEquals(rs.getBigDecimal("dec128"), new BigDecimal("99999999999999999999.999999999999999999")); - assertEquals(rs.getBigDecimal("dec256"), new BigDecimal("9999999999999999999999999999999999999999999999999999999999.999999999999999999")); - - assertTrue(rs.next()); - assertEquals(rs.getBigDecimal("dec"), dec); - assertEquals(rs.getBigDecimal("dec32"), dec32); - assertEquals(rs.getBigDecimal("dec64"), dec64); - assertEquals(rs.getBigDecimal("dec128"), dec128); - assertEquals(rs.getBigDecimal("dec256"), dec256); - - assertFalse(rs.next()); - } - } - } - - // Check the results with getObject - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_decimals ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getObject("dec"), new BigDecimal("-9999999.99")); - assertEquals(rs.getObject("dec32"), new BigDecimal("-99999.9999")); - assertEquals(rs.getObject("dec64"), new BigDecimal("-9999999999.99999999")); - assertEquals(rs.getObject("dec128"), new BigDecimal("-99999999999999999999.999999999999999999")); - assertEquals(rs.getObject("dec256"), new BigDecimal("-9999999999999999999999999999999999999999999999999999999999.999999999999999999")); - - assertTrue(rs.next()); - assertEquals(rs.getObject("dec"), new BigDecimal("9999999.99")); - assertEquals(rs.getObject("dec32"), new BigDecimal("99999.9999")); - assertEquals(rs.getObject("dec64"), new BigDecimal("9999999999.99999999")); - assertEquals(rs.getObject("dec128"), new BigDecimal("99999999999999999999.999999999999999999")); - assertEquals(rs.getObject("dec256"), new BigDecimal("9999999999999999999999999999999999999999999999999999999999.999999999999999999")); - - assertTrue(rs.next()); - assertEquals(rs.getObject("dec"), dec); - assertEquals(rs.getObject("dec32"), dec32); - assertEquals(rs.getObject("dec64"), dec64); - assertEquals(rs.getObject("dec128"), dec128); - assertEquals(rs.getObject("dec256"), dec256); - - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testDateTimeTypes() throws SQLException { - runQuery("CREATE TABLE test_datetimes (order Int8, " + - "dateTime DateTime, dateTime32 DateTime32, " + - "dateTime643 DateTime64(3), dateTime646 DateTime64(6), dateTime649 DateTime64(9)" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert minimum values - insertData("INSERT INTO test_datetimes VALUES ( 1, " + - "'1970-01-01 00:00:00', '1970-01-01 00:00:00', " + - "'1970-01-01 00:00:00.000', '1970-01-01 00:00:00.000000', '1970-01-01 00:00:00.000000000' )"); - - // Insert maximum values - insertData("INSERT INTO test_datetimes VALUES ( 2," + - "'2106-02-07 06:28:15', '2106-02-07 06:28:15', " + - "'2261-12-31 23:59:59.999', '2261-12-31 23:59:59.999999', '2261-12-31 23:59:59.999999999' )"); - - // Insert random (valid) values - final ZoneId zoneId = ZoneId.of("America/Los_Angeles"); - final LocalDateTime now = LocalDateTime.now(zoneId); - final java.sql.Timestamp dateTime = Timestamp.valueOf(now); - dateTime.setNanos(0); - final java.sql.Timestamp dateTime32 = Timestamp.valueOf(now); - dateTime32.setNanos(0); - final java.sql.Timestamp dateTime643 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); - dateTime643.setNanos(333000000); - final java.sql.Timestamp dateTime646 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); - dateTime646.setNanos(333333000); - final java.sql.Timestamp dateTime649 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); - dateTime649.setNanos(333333333); - - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_datetimes VALUES ( 4, ?, ?, ?, ?, ?)")) { - stmt.setTimestamp(1, dateTime); - stmt.setTimestamp(2, dateTime32); - stmt.setTimestamp(3, dateTime643); - stmt.setTimestamp(4, dateTime646); - stmt.setTimestamp(5, dateTime649); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_datetimes ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getTimestamp("dateTime").toString(), "1970-01-01 00:00:00.0"); - assertEquals(rs.getTimestamp("dateTime32").toString(), "1970-01-01 00:00:00.0"); - assertEquals(rs.getTimestamp("dateTime643").toString(), "1970-01-01 00:00:00.0"); - assertEquals(rs.getTimestamp("dateTime646").toString(), "1970-01-01 00:00:00.0"); - assertEquals(rs.getTimestamp("dateTime649").toString(), "1970-01-01 00:00:00.0"); - - assertTrue(rs.next()); - assertEquals(rs.getTimestamp("dateTime").toString(), "2106-02-07 06:28:15.0"); - assertEquals(rs.getTimestamp("dateTime32").toString(), "2106-02-07 06:28:15.0"); - assertEquals(rs.getTimestamp("dateTime643").toString(), "2261-12-31 23:59:59.999"); - assertEquals(rs.getTimestamp("dateTime646").toString(), "2261-12-31 23:59:59.999999"); - assertEquals(rs.getTimestamp("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); - - assertTrue(rs.next()); - assertEquals(rs.getTimestamp("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - assertEquals(rs.getTimestamp("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - assertEquals(rs.getTimestamp("dateTime643").toString(), Timestamp.valueOf(dateTime643.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - assertEquals(rs.getTimestamp("dateTime646").toString(), Timestamp.valueOf(dateTime646.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - assertEquals(rs.getTimestamp("dateTime649").toString(), Timestamp.valueOf(dateTime649.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - - assertEquals(rs.getTimestamp("dateTime", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime.toString()); - assertEquals(rs.getTimestamp("dateTime32", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime32.toString()); - assertEquals(rs.getTimestamp("dateTime643", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime643.toString()); - assertEquals(rs.getTimestamp("dateTime646", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime646.toString()); - assertEquals(rs.getTimestamp("dateTime649", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime649.toString()); - - assertFalse(rs.next()); - } - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_datetimes ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getObject("dateTime").toString(), "1970-01-01 00:00:00.0"); - assertEquals(rs.getObject("dateTime32").toString(), "1970-01-01 00:00:00.0"); - assertEquals(rs.getObject("dateTime643").toString(), "1970-01-01 00:00:00.0"); - assertEquals(rs.getObject("dateTime646").toString(), "1970-01-01 00:00:00.0"); - assertEquals(rs.getObject("dateTime649").toString(), "1970-01-01 00:00:00.0"); - - assertTrue(rs.next()); - - assertEquals(rs.getObject("dateTime").toString(), "2106-02-07 06:28:15.0"); - assertEquals(rs.getObject("dateTime32").toString(), "2106-02-07 06:28:15.0"); - assertEquals(rs.getObject("dateTime643").toString(), "2261-12-31 23:59:59.999"); - assertEquals(rs.getObject("dateTime646").toString(), "2261-12-31 23:59:59.999999"); - assertEquals(rs.getObject("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); - - assertTrue(rs.next()); - - assertEquals(rs.getObject("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - assertEquals(rs.getObject("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - assertEquals(rs.getObject("dateTime643").toString(), Timestamp.valueOf(dateTime643.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - assertEquals(rs.getObject("dateTime646").toString(), Timestamp.valueOf(dateTime646.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - assertEquals(rs.getObject("dateTime649").toString(), Timestamp.valueOf(dateTime649.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); - - assertFalse(rs.next()); - } - } - } - - try (Connection conn = getJdbcConnection(); - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM test_datetimes ORDER BY order")) - { - assertTrue(rs.next()); - - assertEquals(rs.getString("dateTime"), "1970-01-01 00:00:00"); - assertEquals(rs.getString("dateTime32"), "1970-01-01 00:00:00"); - assertEquals(rs.getString("dateTime643"), "1970-01-01 00:00:00"); - assertEquals(rs.getString("dateTime646"), "1970-01-01 00:00:00"); - assertEquals(rs.getString("dateTime649"), "1970-01-01 00:00:00"); - - assertTrue(rs.next()); - assertEquals(rs.getString("dateTime"), "2106-02-07 06:28:15"); - assertEquals(rs.getString("dateTime32"), "2106-02-07 06:28:15"); - assertEquals(rs.getString("dateTime643"), "2261-12-31 23:59:59.999"); - assertEquals(rs.getString("dateTime646"), "2261-12-31 23:59:59.999999"); - assertEquals(rs.getString("dateTime649"), "2261-12-31 23:59:59.999999999"); - - ZoneId tzServer = ZoneId.of(((ConnectionImpl) conn).getClient().getServerTimeZone()); - assertTrue(rs.next()); - assertEquals( - rs.getString("dateTime"), - DataTypeUtils.DATETIME_FORMATTER.format( - Instant.ofEpochMilli(dateTime.getTime()).atZone(tzServer))); - assertEquals( - rs.getString("dateTime32"), - DataTypeUtils.DATETIME_FORMATTER.format( - Instant.ofEpochMilli(dateTime32.getTime()).atZone(tzServer))); - assertEquals( - rs.getString("dateTime643"), - DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER.format(dateTime643.toInstant().atZone(tzServer))); - assertEquals( - rs.getString("dateTime646"), - DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER.format(dateTime646.toInstant().atZone(tzServer))); - assertEquals( - rs.getString("dateTime649"), - DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER.format(dateTime649.toInstant().atZone(tzServer))); - - assertFalse(rs.next()); - } - } - - @Test(groups = { "integration" }) - public void testDateTypes() throws SQLException { - runQuery("CREATE TABLE test_dates (order Int8, " - + "date Date, date32 Date32" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert minimum values - insertData("INSERT INTO test_dates VALUES ( 1, '1970-01-01', '1970-01-01')"); - - // Insert maximum values - insertData("INSERT INTO test_dates VALUES ( 2, '2149-06-06', '2299-12-31')"); - - // Insert random (valid) values - final ZoneId zoneId = ZoneId.of("America/Los_Angeles"); - final LocalDateTime now = LocalDateTime.now(zoneId); - final Date date = Date.valueOf(now.toLocalDate()); - final Date date32 = Date.valueOf(now.toLocalDate()); - - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_dates VALUES ( 3, ?, ?)")) { - stmt.setDate(1, date); - stmt.setDate(2, date32); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getDate("date"), Date.valueOf("1970-01-01")); - assertEquals(rs.getDate("date32"), Date.valueOf("1970-01-01")); - - assertTrue(rs.next()); - assertEquals(rs.getDate("date"), Date.valueOf("2149-06-06")); - assertEquals(rs.getDate("date32"), Date.valueOf("2299-12-31")); - - assertTrue(rs.next()); - assertEquals(rs.getDate("date").toString(), date.toString()); - assertEquals(rs.getDate("date32").toString(), date32.toString()); - - assertFalse(rs.next()); - } - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getObject("date"), Date.valueOf("1970-01-01")); - assertEquals(rs.getObject("date32"), Date.valueOf("1970-01-01")); - - assertTrue(rs.next()); - assertEquals(rs.getObject("date"), Date.valueOf("2149-06-06")); - assertEquals(rs.getObject("date32"), Date.valueOf("2299-12-31")); - - assertTrue(rs.next()); - assertEquals(rs.getObject("date").toString(), date.toString()); - assertEquals(rs.getObject("date32").toString(), date32.toString()); - - assertFalse(rs.next()); - } - } - } - - try (Connection conn = getJdbcConnection(); - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) - { - assertTrue(rs.next()); - assertEquals(rs.getString("date"), "1970-01-01"); - assertEquals(rs.getString("date32"), "1970-01-01"); - - assertTrue(rs.next()); - assertEquals(rs.getString("date"), "2149-06-06"); - assertEquals(rs.getString("date32"), "2299-12-31"); - - ZoneId tzServer = ZoneId.of(((ConnectionImpl) conn).getClient().getServerTimeZone()); - assertTrue(rs.next()); - assertEquals( - rs.getString("date"), - Instant.ofEpochMilli(date.getTime()).atZone(tzServer).toLocalDate().toString()); - assertEquals( - rs.getString("date32"), - Instant.ofEpochMilli(date32.getTime()).atZone(tzServer).toLocalDate().toString()); - - assertFalse(rs.next()); - } - } - - - @Test(groups = { "integration" }) - public void testTimeTypes() throws SQLException { - if (ClickHouseVersion.of(getServerVersion()).check("(,25.5]")) { - return; // Time64 introduced in 25.6 - } - Properties createProperties = new Properties(); - createProperties.put(ClientConfigProperties.serverSetting("allow_experimental_time_time64_type"), "1"); - runQuery("CREATE TABLE test_time64 (order Int8, " - + "time Time, time64 Time64(9) " - + ") ENGINE = MergeTree ORDER BY ()", - createProperties); - - runQuery("INSERT INTO test_time64 (order, time, time64) VALUES " + - " (1, '-999:59:59', '-999:59:59.999999999'), " + - " (2, '999:59:59', '999:59:59.999999999')"); - - // Check the results - try (Statement stmt = getJdbcConnection().createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_time64")) { - assertTrue(rs.next()); - assertEquals(rs.getInt("order"), 1); - - // Negative values - // Negative value cannot be returned as Time without being truncated - assertTrue(rs.getTime("time").getTime() < 0); - assertTrue(rs.getTime("time64").getTime() < 0); - LocalDateTime negativeTime = rs.getObject("time", LocalDateTime.class); - assertEquals(negativeTime.toEpochSecond(ZoneOffset.UTC), -(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); - LocalDateTime negativeTime64 = rs.getObject("time64", LocalDateTime.class); - assertEquals(negativeTime64.toEpochSecond(ZoneOffset.UTC), -(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59), "value " + negativeTime64); - assertEquals(negativeTime64.getNano(), 999_999_999); // nanoseconds are stored separately and only positive values accepted - - // Positive values - assertTrue(rs.next()); - assertEquals(rs.getInt("order"), 2); - LocalDateTime positiveTime = rs.getObject("time", LocalDateTime.class); - assertEquals(positiveTime.toEpochSecond(ZoneOffset.UTC), (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); - LocalDateTime positiveTime64 = rs.getObject("time64", LocalDateTime.class); - assertEquals(positiveTime64.toEpochSecond(ZoneOffset.UTC), (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); - assertEquals(positiveTime64.getNano(), 999_999_999); - - // Time is stored as UTC (server timezone) - assertEquals(rs.getTime("time", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime(), - (TimeUnit.HOURS.toMillis(999) + TimeUnit.MINUTES.toMillis(59) + TimeUnit.SECONDS.toMillis(59))); - - // java.sql.Time max resolution is milliseconds - assertEquals(rs.getTime("time64", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime(), - (TimeUnit.HOURS.toMillis(999) + TimeUnit.MINUTES.toMillis(59) + TimeUnit.SECONDS.toMillis(59) + 999)); - - assertEquals(rs.getTime("time"), rs.getObject("time", Time.class)); - assertEquals(rs.getTime("time64"), rs.getObject("time64", Time.class)); - - // time has no date part and cannot be converted to Date or Timestamp - for (String col : Arrays.asList("time", "time64")) { - assertThrows(SQLException.class, () -> rs.getDate(col)); - assertThrows(SQLException.class, () -> rs.getTimestamp(col)); - assertThrows(SQLException.class, () -> rs.getObject(col, Timestamp.class)); - assertThrows(SQLException.class, () -> rs.getObject(col, Date.class)); - } - assertFalse(rs.next()); - } - } - } - - @Test(groups = { "integration" }) - public void testStringTypes() throws SQLException { - runQuery("CREATE TABLE test_strings (order Int8, " - + "str String, fixed FixedString(6), " - + "enum Enum8('a' = 6, 'b' = 7, 'c' = 8), enum8 Enum8('a' = 1, 'b' = 2, 'c' = 3), enum16 Enum16('a' = 1, 'b' = 2, 'c' = 3), " - + "uuid UUID, escaped String " - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - - String str = "string" + rand.nextInt(1000); - String fixed = "fixed" + rand.nextInt(10); - String enum8 = "a"; - String enum16 = "b"; - String uuid = UUID.randomUUID().toString(); - String escaped = "\\xA3\\xA3\\x12\\xA0\\xDF\\x13\\x4E\\x8C\\x87\\x74\\xD4\\x53\\xDB\\xFC\\x34\\x95"; - - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_strings VALUES ( 1, ?, ?, ?, ?, ?, ?, ? )")) { - stmt.setString(1, str); - stmt.setString(2, fixed); - stmt.setString(3, enum8); - stmt.setString(4, enum8); - stmt.setString(5, enum16); - stmt.setString(6, uuid); - stmt.setString(7, escaped); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_strings ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getString("str"), str); - assertEquals(rs.getString("fixed"), fixed); - assertEquals(rs.getString("enum"), "a"); - assertEquals(rs.getInt("enum"), 6); - assertEquals(rs.getString("enum8"), "a"); - assertEquals(rs.getInt("enum8"), 1); - assertEquals(rs.getString("enum16"), "b"); - assertEquals(rs.getInt("enum16"), 2); - assertEquals(rs.getString("uuid"), uuid); - assertEquals(rs.getString("escaped"), escaped); - assertFalse(rs.next()); - } - } - } - - // Check the results with getObject - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_strings ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getObject("str"), str); - assertEquals(rs.getObject("fixed"), fixed); - assertEquals(rs.getObject("enum"), "a"); - assertEquals(rs.getObject("enum8"), "a"); - assertEquals(rs.getObject("enum16"), "b"); - assertEquals(rs.getObject("uuid"), UUID.fromString(uuid)); - assertEquals(rs.getObject("escaped"), escaped); - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testIpAddressTypes() throws SQLException, UnknownHostException { - runQuery("CREATE TABLE test_ips (order Int8, " - + "ipv4_ip IPv4, ipv4_name IPv4, ipv6 IPv6, ipv4_as_ipv6 IPv6" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - - InetAddress ipv4AddressByIp = Inet4Address.getByName("90.176.75.97"); - InetAddress ipv4AddressByName = Inet4Address.getByName("www.example.com"); - InetAddress ipv6Address = Inet6Address.getByName("2001:adb8:85a3:1:2:8a2e:370:7334"); - InetAddress ipv4AsIpv6 = Inet4Address.getByName("90.176.75.97"); - - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_ips VALUES ( 1, ?, ?, ?, ? )")) { - stmt.setObject(1, ipv4AddressByIp); - stmt.setObject(2, ipv4AddressByName); - stmt.setObject(3, ipv6Address); - stmt.setObject(4, ipv4AsIpv6); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_ips ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getObject("ipv4_ip"), ipv4AddressByIp); - assertEquals(rs.getObject("ipv4_ip", Inet6Address.class).getHostAddress(), "0:0:0:0:0:ffff:5ab0:4b61"); - assertEquals(rs.getString("ipv4_ip"), ipv4AddressByIp.getHostAddress()); - assertEquals(rs.getObject("ipv4_name"), ipv4AddressByName); - assertEquals(rs.getObject("ipv6"), ipv6Address); - assertEquals(rs.getString("ipv6"), ipv6Address.getHostAddress()); - assertEquals(rs.getObject("ipv4_as_ipv6"), ipv4AsIpv6); - assertEquals(rs.getObject("ipv4_as_ipv6", Inet4Address.class), ipv4AsIpv6); - assertEquals(rs.getBytes("ipv4_ip"), ipv4AddressByIp.getAddress()); - assertEquals(rs.getBytes("ipv6"), ipv6Address.getAddress()); - - assertFalse(rs.next()); - } - } - } - } - - - @Test(groups = {"integration"}) - public void testArrayOfIpAddress() throws Exception { - try (Connection connection = getJdbcConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT ['90.176.75.97'::IPv4] addrs1, ['2001:adb8:85a3:1:2:8a2e:370:7334'::IPv6] addrs2, ['2001:adb8:85a3:1:2:8a2e:370:7334'::IPv6, null] addrs3")) { - - InetAddress ipv4AddressByIp = Inet4Address.getByName("90.176.75.97"); - InetAddress ipv6Address = Inet6Address.getByName("2001:adb8:85a3:1:2:8a2e:370:7334"); - - Assert.assertTrue(rs.next()); - { - // IPv4 - Array addrs1 = rs.getArray(1); - Object[] arr = (Object[]) addrs1.getArray(); - Assert.assertEquals(ipv4AddressByIp, arr[0]); - - ResultSet arrRs = addrs1.getResultSet(); - arrRs.next(); - Assert.assertEquals(ipv4AddressByIp, arrRs.getObject(2)); - } - - { - // IPv6 - Array addrs2 = rs.getArray(2); - Object[] arr = (Object[]) addrs2.getArray(); - Assert.assertEquals(ipv6Address, arr[0]); - - ResultSet arrRs = addrs2.getResultSet(); - arrRs.next(); - Assert.assertEquals(ipv6Address, arrRs.getObject(2)); - } - - { - // IPv6 - Array addrs3 = rs.getArray(3); - Assert.assertEquals(addrs3.getBaseTypeName(), "Nullable(IPv6)"); - Object[] arr = (Object[]) addrs3.getArray(); - Assert.assertEquals(ipv6Address, arr[0]); - - ResultSet arrRs = addrs3.getResultSet(); - arrRs.next(); - Assert.assertEquals(ipv6Address, arrRs.getObject(2)); - arrRs.next(); - Assert.assertNull(arrRs.getObject(2)); - } - } - } - - - @Test(groups = { "integration" }) - public void testFloatTypes() throws SQLException { - runQuery("CREATE TABLE test_floats (order Int8, " - + "float32 Float32, float64 Float64" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert minimum values - insertData("INSERT INTO test_floats VALUES ( 1, -3.4028233E38, -1.7976931348623157E308 )"); - - // Insert maximum values - insertData("INSERT INTO test_floats VALUES ( 2, 3.4028233E38, 1.7976931348623157E308 )"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - Float float32 = rand.nextFloat(); - Double float64 = rand.nextDouble(); - - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_floats VALUES ( 3, ?, ? )")) { - stmt.setFloat(1, float32); - stmt.setDouble(2, float64); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_floats ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getFloat("float32"), -3.402823E38f); - assertEquals(rs.getDouble("float64"), Double.valueOf(-1.7976931348623157E308)); - - assertTrue(rs.next()); - assertEquals(rs.getFloat("float32"), Float.valueOf(3.402823E38f)); - assertEquals(rs.getDouble("float64"), Double.valueOf(1.7976931348623157E308)); - - assertTrue(rs.next()); - assertEquals(rs.getFloat("float32"), float32); - assertEquals(rs.getDouble("float64"), float64); - - assertFalse(rs.next()); - } - } - } - - // Check the results with getObject - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_floats ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getObject("float32"), -3.402823E38f); - assertEquals(rs.getObject("float64"), Double.valueOf(-1.7976931348623157E308)); - - assertTrue(rs.next()); - assertEquals(rs.getObject("float32"), 3.402823E38f); - assertEquals(rs.getObject("float64"), Double.valueOf(1.7976931348623157E308)); - - assertTrue(rs.next()); - - DecimalFormat df = new DecimalFormat("#.######"); - assertEquals(df.format(rs.getObject("float32")), df.format(float32)); - assertEquals(rs.getObject("float64"), float64); - - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testBooleanTypes() throws SQLException { - runQuery("CREATE TABLE test_booleans (order Int8, " - + "bool Boolean" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - boolean bool = rand.nextBoolean(); - - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_booleans VALUES ( 1, ? )")) { - stmt.setBoolean(1, bool); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_booleans ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getBoolean("bool"), bool); - - assertFalse(rs.next()); - } - } - } - - // Check the results with getObject - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_booleans ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getObject("bool"), bool); - - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testArrayTypes() throws SQLException { - runQuery("CREATE TABLE test_arrays (order Int8, " - + "array Array(Int8), arraystr Array(String), " - + "arraytuple Array(Tuple(Int8, String)), " - + "arraydate Array(Date)" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - Integer[] array = new Integer[10]; - for (int i = 0; i < array.length; i++) { - array[i] = rand.nextInt(256) - 128; - } - - String[] arraystr = new String[10]; - for (int i = 0; i < arraystr.length; i++) { - arraystr[i] = "string" + rand.nextInt(1000); - } - - Tuple[] arraytuple = new Tuple[10]; - for (int i = 0; i < arraytuple.length; i++) { - arraytuple[i] = new Tuple(rand.nextInt(256) - 128, "string" + rand.nextInt(1000)); - } - - Date[] arraydate = new Date[10]; - for (int i = 0; i < arraydate.length; i++) { - arraydate[i] = Date.valueOf(LocalDate.now().plusDays(rand.nextInt(100))); - } - - // Insert using `Connection#createArrayOf` - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_arrays VALUES ( 1, ?, ?, ?, ?)")) { - stmt.setArray(1, conn.createArrayOf("Int8", array)); - stmt.setArray(2, conn.createArrayOf("String", arraystr)); - stmt.setArray(3, conn.createArrayOf("Tuple(Int8, String)", arraytuple)); - stmt.setArray(3, conn.createArrayOf("Tuple(Int8, String)", arraytuple)); - stmt.setArray(4, conn.createArrayOf("Date", arraydate)); - stmt.executeUpdate(); - } - } - - // Insert using common java objects - final String INSERT_SQL = "INSERT INTO test_arrays VALUES ( 2, ?, ?, ?, ?)"; - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement(INSERT_SQL)) { - stmt.setObject(1, array); - stmt.setObject(2, arraystr); - stmt.setObject(3, arraytuple); - stmt.setObject(4, arraydate); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_arrays ORDER BY order")) { - assertTrue(rs.next()); - { - Object[] arrayResult = (Object[]) rs.getArray("array").getArray(); - assertEquals(arrayResult.length, array.length); - for (int i = 0; i < array.length; i++) { - assertEquals(String.valueOf(arrayResult[i]), String.valueOf(array[i])); - } - - Object[] arraystrResult = (Object[]) rs.getArray("arraystr").getArray(); - assertEquals(arraystrResult.length, arraystr.length); - for (int i = 0; i < arraystr.length; i++) { - assertEquals(arraystrResult[i], arraystr[i]); - } - Object[] arraytupleResult = (Object[]) rs.getArray("arraytuple").getArray(); - assertEquals(arraytupleResult.length, arraytuple.length); - for (int i = 0; i < arraytuple.length; i++) { - Tuple tuple = arraytuple[i]; - Tuple tupleResult = new Tuple(((Object[]) arraytupleResult[i])); - assertEquals(String.valueOf(tupleResult.getValue(0)), String.valueOf(tuple.getValue(0))); - assertEquals(String.valueOf(tupleResult.getValue(1)), String.valueOf(tuple.getValue(1))); - } - - Object[] arraydateResult = (Object[]) rs.getArray("arraydate").getArray(); - assertEquals(arraydateResult.length, arraydate.length); - for (int i = 0; i < arraydate.length; i++) { - assertEquals(String.valueOf(arraydateResult[i]), String.valueOf(arraydate[i])); - } - } - assertTrue(rs.next()); - { - Object[] arrayResult = (Object[]) ((Array) rs.getObject("array")).getArray(); - assertEquals(arrayResult.length, array.length); - for (int i = 0; i < array.length; i++) { - assertEquals(String.valueOf(arrayResult[i]), String.valueOf(array[i])); - } - - Object[] arraystrResult = (Object[]) ((Array) rs.getObject("arraystr")).getArray(); - assertEquals(arraystrResult.length, arraystr.length); - for (int i = 0; i < arraystr.length; i++) { - assertEquals(arraystrResult[i], arraystr[i]); - } - Object[] arraytupleResult = (Object[]) ((Array) rs.getObject("arraytuple")).getArray(); - assertEquals(arraytupleResult.length, arraytuple.length); - for (int i = 0; i < arraytuple.length; i++) { - Tuple tuple = arraytuple[i]; - Tuple tupleResult = new Tuple(((Object[]) arraytupleResult[i])); - assertEquals(String.valueOf(tupleResult.getValue(0)), String.valueOf(tuple.getValue(0))); - assertEquals(String.valueOf(tupleResult.getValue(1)), String.valueOf(tuple.getValue(1))); - } - - Object[] arraydateResult = (Object[]) ((Array) rs.getObject("arraydate")).getArray(); - assertEquals(arraydateResult.length, arraydate.length); - for (int i = 0; i < arraydate.length; i++) { - assertEquals(arraydateResult[i], arraydate[i]); - } - } - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testStringsUsedAsBytes() throws Exception { - runQuery("CREATE TABLE test_strings_as_bytes (order Int8, str String, fixed FixedString(10)) ENGINE = MergeTree ORDER BY ()"); - - String[][] testData = {{"Hello, World!", "FixedStr"}, {"Test String 123", "ABC"}}; - - try (Connection conn = getJdbcConnection(); - PreparedStatement insert = conn.prepareStatement("INSERT INTO test_strings_as_bytes VALUES (?, ?, ?)")) { - for (int i = 0; i < testData.length; i++) { - insert.setInt(1, i + 1); - insert.setBytes(2, testData[i][0].getBytes("UTF-8")); - insert.setBytes(3, testData[i][1].getBytes("UTF-8")); - insert.executeUpdate(); - } - } - - try (Connection conn = getJdbcConnection(); - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM test_strings_as_bytes ORDER BY order")) { - for (String[] expected : testData) { - assertTrue(rs.next()); - assertEquals(new String(rs.getBytes("str"), "UTF-8"), expected[0]); - assertEquals(new String(rs.getBytes("fixed"), "UTF-8").replace("\0", ""), expected[1]); - } - assertFalse(rs.next()); - } - } - - @Test(groups = { "integration" }) - public void testNestedArrays() throws Exception { - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Array(Int32)) as value")) { - Integer[][] srcArray = new Integer[][] { - {1, 2, 3}, - {4, 5, 6} - }; - Array array = conn.createArrayOf("Int32", srcArray); - stmt.setArray(1, array); - - try (ResultSet rs = stmt.executeQuery()) { - assertTrue(rs.next()); - Array arrayHolder = (Array) rs.getObject(1); - Object[] dbArray = (Object[]) arrayHolder.getArray(); - for (int i = 0; i < dbArray.length; i++) { - Object[] nestedArray = (Object[]) dbArray[i]; - for (int j = 0; j < nestedArray.length; j++) { - assertEquals((Integer) nestedArray[j], (Integer)srcArray[i][j]); - } - } - } - - Integer[] simpleArray = new Integer[] {1, 2, 3}; - Array array1 = conn.createArrayOf("Int32", simpleArray); - Array array2 = conn.createArrayOf("Int32", simpleArray); - - Array[] multiLevelArray = new Array[] {array1, array2}; - Array array3 = conn.createArrayOf("Int32", multiLevelArray); - stmt.setArray(1, array3); - try (ResultSet rs = stmt.executeQuery()) { - assertTrue(rs.next()); - Array arrayHolder = (Array) rs.getObject(1); - Object[] dbArray = (Object[]) arrayHolder.getArray(); - for (int i = 0; i < dbArray.length; i++) { - Object[] nestedArray = (Object[]) dbArray[i]; - for (int j = 0; j < nestedArray.length; j++) { - assertEquals((Integer) nestedArray[j], (Integer)simpleArray[j]); - } - } - } - } - } - } - - @Test(groups = { "integration" }) - public void testNestedArrayToString() throws SQLException { - // Test 1: Simple nested array - getString on Array(Array(Int32)) - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT [[1, 2, 3], [4, 5, 6]] as nested_array")) { - assertTrue(rs.next()); - // This was throwing NullPointerException before the fix - String result = rs.getString("nested_array"); - assertEquals(result, "[[1, 2, 3], [4, 5, 6]]"); - } - } - } - - // Test 2: Query similar to issue #2723 with splitByChar returning array - // The original issue was that getString() on an array column inside a CASE/WHEN - // would cause NPE. This test verifies that getString() works correctly on arrays. - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - String query = "SELECT " + - "splitByChar('_', 'field1_field2_field3') as split_result, " + - "CASE " + - " WHEN " + - " splitByChar('_', 'field1_field2_field3')[1] IN ('field1', 'field2') " + - " AND match( " + - " splitByChar('_', 'field1_field2_field3')[2], " + - " '(field1|field2|field3)' " + - " ) " + - " THEN 'Matched' " + - " ELSE 'NotMatched' " + - "END AS action_to_do"; - try (ResultSet rs = stmt.executeQuery(query)) { - assertTrue(rs.next()); - // The key test is that getString() doesn't throw NPE on array column - String splitResult = rs.getString("split_result"); - assertEquals(splitResult, "['field1', 'field2', 'field3']"); - String actionResult = rs.getString("action_to_do"); - assertEquals(actionResult, "Matched"); - } - } - } - - // Test 3: Deeply nested arrays - Array(Array(Array(String))) - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT [[['a', 'b'], ['c']], [['d', 'e', 'f']]] as deep_nested")) { - 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"}}})); - } - } - } - } - - @Test(groups = { "integration" }) - public void testMapTypes() throws SQLException { - runQuery("CREATE TABLE test_maps (order Int8, " - + "map Map(String, Int8), mapstr Map(String, String)" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - int mapSize = rand.nextInt(100); - Map integerMap = new java.util.HashMap<>(mapSize); - for (int i = 0; i < mapSize; i++) { - integerMap.put("key" + i, rand.nextInt(256) - 128); - } - - Map stringMap = new java.util.HashMap<>(mapSize); - for (int i = 0; i < mapSize; i++) { - stringMap.put("key" + i, "string" + rand.nextInt(1000)); - } - - // Insert random (valid) values - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_maps VALUES ( 1, ?, ? )")) { - stmt.setObject(1, integerMap); - stmt.setObject(2, stringMap); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_maps ORDER BY order")) { - assertTrue(rs.next()); - Map mapResult = (Map) rs.getObject("map"); - assertEquals(mapResult.size(), mapSize); - for (String key: integerMap.keySet()) { - assertEquals(String.valueOf(mapResult.get(key)), String.valueOf(integerMap.get(key))); - } - - Map mapstrResult = (Map) rs.getObject("mapstr"); - assertEquals(mapstrResult.size(), mapSize); - for (String key: stringMap.keySet()) { - assertEquals(String.valueOf(mapstrResult.get(key)), String.valueOf(stringMap.get(key))); - } - } - } - } - } - - - @Test(groups = { "integration" }) - public void testMapTypesWithArrayValues() throws SQLException { - runQuery("DROP TABLE test_maps;"); - runQuery("CREATE TABLE test_maps (order Int8, " - + "map Map(String, Array(Int32)), " - + "map2 Map(String, Array(Int32))" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - int mapSize = 3; - Map integerMap = new java.util.HashMap<>(mapSize); - Map integerMap2 = new java.util.HashMap<>(mapSize); - for (int i = 0; i < mapSize; i++) { - int[] array = new int[10]; - Integer[] array2 = new Integer[10]; - for (int j = 0; j < array.length; j++) { - array[j] = array2[j] = rand.nextInt(1000); - - } - integerMap.put("key" + i, array); - integerMap2.put("key" + i, array2); - } - - // Insert random (valid) values - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_maps VALUES ( 1, ?, ?)")) { - stmt.setObject(1, integerMap); - stmt.setObject(2, integerMap2); - stmt.executeUpdate(); - } - } - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_maps ORDER BY order")) { - assertTrue(rs.next()); - Map mapResult = (Map) rs.getObject("map"); - assertEquals(mapResult.size(), mapSize); - for (String key: integerMap.keySet()) { - Object[] arrayResult = ((List) mapResult.get(key)).toArray(); - int[] array = integerMap.get(key); - assertEquals(arrayResult.length, array.length); - for (int i = 0; i < array.length; i++) { - assertEquals(String.valueOf(arrayResult[i]), String.valueOf(array[i])); - } - } - } - } - } - } - - @Test(groups = {"integration"}) - public void testArrayOfMaps() throws Exception { - try (Connection connection = getJdbcConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT [map('a', 1, 'b', 2)::Map(String, Int32)] arr1")) { - - Assert.assertTrue(rs.next()); - { - // Array(Map(String, Int32)) - Array arrMap1 = rs.getArray(1); - Assert.assertEquals(arrMap1.getBaseTypeName(), "Map(String, Int32)"); - Object[] arr = (Object[]) arrMap1.getArray(); - @SuppressWarnings("unchecked") - Map map1 = (Map) arr[0]; - Assert.assertEquals(map1.get("a"), Integer.valueOf(1)); - Assert.assertEquals(map1.get("b"), Integer.valueOf(2)); - - ResultSet arrRs = arrMap1.getResultSet(); - arrRs.next(); - @SuppressWarnings("unchecked") - Map rsMap1 = (Map) arrRs.getObject(2); - Assert.assertEquals(rsMap1.get("a"), Integer.valueOf(1)); - Assert.assertEquals(rsMap1.get("b"), Integer.valueOf(2)); - } - } - } - - /** - * Verifies that Array(Map(LowCardinality(String), String)) with empty maps decodes correctly. - * Regression test for #2657 - */ - @Test(groups = {"integration"}) - public void testArrayOfMapsWithLowCardinalityAndEmptyMaps() throws Exception { - runQuery("CREATE TABLE test_array_map_lc_empty (" - + "StartedDateTime DateTime, " - + "traits Array(Map(LowCardinality(String), String))" - + ") ENGINE = MergeTree ORDER BY StartedDateTime"); - - try (Connection conn = getJdbcConnection(); - Statement stmt = conn.createStatement()) { - - stmt.executeUpdate("INSERT INTO test_array_map_lc_empty (StartedDateTime, traits) VALUES (" - + "'2025-11-11 00:00:01', " - + "[" - + " map(), " - + " map(" - + " 'RandomKey1','Value1'," - + " 'RandomKey2','Value2'," - + " 'RandomKey3','Value3'," - + " 'RandomKey4','Value4'," - + " 'RandomKey5','Value5'," - + " 'RandomKey6','Value6'," - + " 'RandomKey7','Value7'," - + " 'RandomKey8','Value8'" - + " ), " - + " map(), map(), map(), map(), map(), map()" - + "]" - + ")"); - - Map expectedNonEmptyMap = new HashMap<>(); - expectedNonEmptyMap.put("RandomKey1", "Value1"); - expectedNonEmptyMap.put("RandomKey2", "Value2"); - expectedNonEmptyMap.put("RandomKey3", "Value3"); - expectedNonEmptyMap.put("RandomKey4", "Value4"); - expectedNonEmptyMap.put("RandomKey5", "Value5"); - expectedNonEmptyMap.put("RandomKey6", "Value6"); - expectedNonEmptyMap.put("RandomKey7", "Value7"); - expectedNonEmptyMap.put("RandomKey8", "Value8"); - - // Run multiple iterations because the bug is intermittent - for (int attempt = 0; attempt < 10; attempt++) { - try (ResultSet rs = stmt.executeQuery("SELECT traits FROM test_array_map_lc_empty")) { - Assert.assertTrue(rs.next(), "Expected a row on attempt " + attempt); - - Array traitsArray = rs.getArray(1); - Assert.assertEquals(traitsArray.getBaseTypeName(), "Map(LowCardinality(String), String)"); - - Object[] maps = (Object[]) traitsArray.getArray(); - Assert.assertEquals(maps.length, 8, "Expected 8 maps in array on attempt " + attempt); - - @SuppressWarnings("unchecked") - Map firstMap = (Map) maps[0]; - Assert.assertTrue(firstMap.isEmpty(), "First map should be empty on attempt " + attempt); - - @SuppressWarnings("unchecked") - Map secondMap = (Map) maps[1]; - Assert.assertEquals(secondMap, expectedNonEmptyMap, "Second map mismatch on attempt " + attempt); - - for (int i = 2; i < 8; i++) { - @SuppressWarnings("unchecked") - Map emptyMap = (Map) maps[i]; - Assert.assertTrue(emptyMap.isEmpty(), - "Map at index " + i + " should be empty on attempt " + attempt); - } - - Assert.assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testNullableTypesSimpleStatement() throws SQLException { - runQuery("CREATE TABLE test_nullable (order Int8, " - + "int8 Nullable(Int8), int16 Nullable(Int16), int32 Nullable(Int32), int64 Nullable(Int64), int128 Nullable(Int128), int256 Nullable(Int256), " - + "uint8 Nullable(UInt8), uint16 Nullable(UInt16), uint32 Nullable(UInt32), uint64 Nullable(UInt64), uint128 Nullable(UInt128), uint256 Nullable(UInt256), " - + "dec Nullable(Decimal(9, 2)), dec32 Nullable(Decimal32(4)), dec64 Nullable(Decimal64(8)), dec128 Nullable(Decimal128(18)), dec256 Nullable(Decimal256(18)), " - + "date Nullable(Date), date32 Nullable(Date32), " - + "dateTime Nullable(DateTime), dateTime32 Nullable(DateTime32), " - + "dateTime643 Nullable(DateTime64(3)), dateTime646 Nullable(DateTime64(6)), dateTime649 Nullable(DateTime64(9)), " - + "str Nullable(String), fixed Nullable(FixedString(6)), " - + "enum Nullable(Enum8('a' = 6, 'b' = 7, 'c' = 8)), enum8 Nullable(Enum8('a' = 1, 'b' = 2, 'c' = 3)), enum16 Nullable(Enum16('a' = 1, 'b' = 2, 'c' = 3)), " - + "uuid Nullable(UUID), ipv4 Nullable(IPv4), ipv6 Nullable(IPv6), " - + "float32 Nullable(Float32), float64 Nullable(Float64), " - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert null values - insertData("INSERT INTO test_nullable VALUES ( 1, " - + "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, " - + "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, " - + "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, " - + "NULL, NULL, NULL, NULL)"); - - //Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_nullable ORDER BY order")) { - assertTrue(rs.next()); - for (int i = 2; i <= 34; i++) { - assertTrue(rs.getObject(i) == null); - } - - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testLowCardinalityTypeSimpleStatement() throws SQLException { - runQuery("CREATE TABLE test_low_cardinality (order Int8, " - + "lowcardinality LowCardinality(String)" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - String lowcardinality = "string" + rand.nextInt(1000); - - insertData(String.format("INSERT INTO test_low_cardinality VALUES ( 1, '%s' )", - lowcardinality)); - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_low_cardinality ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getString("lowcardinality"), lowcardinality); - assertEquals(rs.getObject("lowcardinality"), lowcardinality); - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testSimpleAggregateFunction() throws SQLException { - runQuery("CREATE TABLE test_aggregate (order Int8," + - " int8 Int8," + - " val SimpleAggregateFunction(any, Nullable(Int8))" + - ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - int int8 = rand.nextInt(256) - 128; - - insertData(String.format("INSERT INTO test_aggregate VALUES ( 1, %d, null )", int8)); - insertData(String.format("INSERT INTO test_aggregate VALUES ( 2, %d, null )", int8)); - insertData(String.format("INSERT INTO test_aggregate VALUES ( 3, %d, null )", int8)); - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT sum(int8) FROM test_aggregate")) { - assertTrue(rs.next()); - assertEquals(rs.getInt(1), int8 * 3); - assertEquals(rs.getObject(1), (long) (int8 * 3)); - } - try (ResultSet rs = stmt.executeQuery("SELECT any(val) FROM test_aggregate")) { - assertTrue(rs.next()); - assertNull(rs.getObject(1)); - assertTrue(rs.wasNull()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testNestedTypeSimpleStatement() throws SQLException { - runQuery("CREATE TABLE test_nested (order Int8, " - + "nested Nested (int8 Int8, int16 Int16, int32 Int32, int64 Int64, int128 Int128, int256 Int256)" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - int int8 = rand.nextInt(256) - 128; - int int16 = rand.nextInt(65536) - 32768; - int int32 = rand.nextInt(); - long int64 = rand.nextLong(); - BigInteger int128 = new BigInteger(127, rand); - BigInteger int256 = new BigInteger(255, rand); - - String sql = String.format("INSERT INTO test_nested VALUES ( 1, [%s], [%s], [%s], [%s], [%s], [%s])", - int8, int16, int32, int64, int128, int256); - log.info("SQL: {}", sql); - insertData(sql); - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_nested ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(String.valueOf(((Object[])rs.getArray("nested.int8").getArray())[0]), String.valueOf(int8)); - assertEquals(String.valueOf(((Object[])rs.getArray("nested.int16").getArray())[0]), String.valueOf(int16)); - assertEquals(String.valueOf(((Object[])rs.getArray("nested.int32").getArray())[0]), String.valueOf(int32)); - assertEquals(String.valueOf(((Object[])rs.getArray("nested.int64").getArray())[0]), String.valueOf(int64)); - assertEquals(String.valueOf(((Object[])rs.getArray("nested.int128").getArray())[0]), String.valueOf(int128)); - assertEquals(String.valueOf(((Object[])rs.getArray("nested.int256").getArray())[0]), String.valueOf(int256)); - - assertFalse(rs.next()); - } - } - } - - } - - @Test(groups = { "integration" }) - public void testNestedTypeNonFlatten() throws SQLException { - if (earlierThan(25,1)){ - return; - } - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - stmt.execute("SET flatten_nested = 0"); - stmt.execute("CREATE TABLE test_nested_not_flatten (order Int8, " - + "nested Nested (int8 Int8, int16 Int16, int32 Int32, int64 Int64, int128 Int128, int256 Int256)" - + ") ENGINE = MergeTree ORDER BY () SETTINGS flatten_nested = 0"); - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - int int8 = rand.nextInt(256) - 128; - int int16 = rand.nextInt(65536) - 32768; - int int32 = rand.nextInt(); - long int64 = rand.nextLong(); - BigInteger int128 = new BigInteger(127, rand); - BigInteger int256 = new BigInteger(255, rand); - - - String nsql = String.format("INSERT INTO test_nested_not_flatten VALUES ( 1, [(%s,%s,%s,%s,%s,%s)])", - int8, int16, int32, int64, int128, int256); - log.info("SQL: {}", nsql); - stmt.executeUpdate(nsql); - - // Check the results - - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_nested_not_flatten ORDER BY order")) { - assertTrue(rs.next()); - assertEquals((Object[])((Object[])((java.sql.Array) rs.getObject("nested")).getArray())[0], - new Object[] {(byte) int8, (short) int16, int32, int64, int128, int256}); - - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = {"integration"}) - public void testTupleType() throws Exception { - try (Connection conn = getJdbcConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Tuple(String, Int32, Date)")) { - Object[] arr = new Object[]{"test", 123, LocalDate.parse("2026-03-02")}; - Struct tuple = conn.createStruct("Tuple(String, Int32, Date)", arr); - Array tupleArr = conn.createArrayOf("Array(Tuple(String, Int32, Date))", arr); - stmt.setObject(1, tuple); - try (ResultSet rs = stmt.executeQuery()) { - rs.next(); - Array dbTuple = rs.getArray(1); - Assert.assertEquals(dbTuple, tupleArr); - Object tupleObjArr = rs.getObject(1); - Assert.assertEquals(tupleObjArr, arr); - } - } - } - } - - @Test(groups = { "integration" }) - public void testTupleTypeSimpleStatement() throws SQLException { - runQuery("CREATE TABLE test_tuple (order Int8, " - + "tuple Tuple(int8 Int8, int16 Int16, int32 Int32, int64 Int64, int128 Int128, int256 Int256)" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - int int8 = rand.nextInt(256) - 128; - int int16 = rand.nextInt(65536) - 32768; - int int32 = rand.nextInt(); - long int64 = rand.nextLong(); - BigInteger int128 = new BigInteger(127, rand); - BigInteger int256 = new BigInteger(255, rand); - - String sql = String.format("INSERT INTO test_tuple VALUES ( 1, (%s, %s, %s, %s, %s, %s))", - int8, int16, int32, int64, int128, int256); - insertData(sql); - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_tuple ORDER BY order")) { - assertTrue(rs.next()); - Object[] tuple = (Object[]) rs.getObject(2); - assertEquals(String.valueOf(tuple[0]), String.valueOf(int8)); - assertEquals(String.valueOf(tuple[1]), String.valueOf(int16)); - assertEquals(String.valueOf(tuple[2]), String.valueOf(int32)); - assertEquals(String.valueOf(tuple[3]), String.valueOf(int64)); - assertEquals(String.valueOf(tuple[4]), String.valueOf(int128)); - assertEquals(String.valueOf(tuple[5]), String.valueOf(int256)); - assertFalse(rs.next()); - } - } - } - } - - - - @Test(groups = { "integration" }) - public void testJSONWritingAsString() throws SQLException { - if (ClickHouseVersion.of(getServerVersion()).check("(,24.8]")) { - return; // JSON was introduced in 24.10 - } - - Properties createProperties = new Properties(); - createProperties.put(ClientConfigProperties.serverSetting("allow_experimental_json_type"), "1"); - runQuery("CREATE TABLE test_json (order Int8, " - + "json JSON" - + ") ENGINE = MergeTree ORDER BY ()", createProperties); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - double key1 = rand.nextDouble(); - int key2 = rand.nextInt(); - final String json = "{\"key1\": \"" + key1 + "\", \"key2\": " + key2 + ", \"key3\": [1000, \"value3\", 400000]}"; - final String serverJson = "{\"key1\":\"" + key1 + "\",\"key2\":\"" + key2 + "\",\"key3\":[\"1000\",\"value3\",\"400000\"]}"; - insertData(String.format("INSERT INTO test_json VALUES ( 1, '%s' )", json)); - - // Check the results - Properties props = new Properties(); - props.setProperty( - ClientConfigProperties.serverSetting(ServerSettings.OUTPUT_FORMAT_BINARY_WRITE_JSON_AS_STRING), - "1"); - props.setProperty(ClientConfigProperties.serverSetting("output_format_json_quote_64bit_integers"), "1"); - try (Connection conn = getJdbcConnection(props)) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_json ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getString("json"), serverJson); - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testReadingJSONBinary() throws SQLException { - if (ClickHouseVersion.of(getServerVersion()).check("(,24.8]")) { - return; // JSON was introduced in 24.10 - } - - Properties properties = new Properties(); - properties.put(ClientConfigProperties.serverSetting("allow_experimental_json_type"), "1"); - try (Connection conn = getJdbcConnection(properties); - Statement stmt = conn.createStatement()) { - - final String json = "{\"count\": 1000, \"event\": { \"name\": \"start\", \"value\": 0.10} }"; - String sql = String.format("SELECT %1$s::JSON(), %1$s::JSON(count Int16)", SQLUtils.enquoteLiteral(json)); - try (ResultSet rs = stmt.executeQuery(sql)) { - rs.next(); - - Map val1 = (Map) rs.getObject(1); - assertEquals(val1.get("count"), 1000L); - Map val2 = (Map) rs.getObject(2); - assertEquals(val2.get("count"), (short)1000); - } - } - } - - - @Test(groups = { "integration" }, enabled = false) - public void testGeometricTypesSimpleStatement() throws SQLException { - // TODO: add LineString and MultiLineString support - runQuery("CREATE TABLE test_geometric (order Int8, " - + "point Point, ring Ring, linestring LineString, multilinestring MultiLineString, polygon Polygon, multipolygon MultiPolygon" - + ") ENGINE = MergeTree ORDER BY ()"); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - String point = "(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")"; - String ring = "[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]"; - String linestring = "[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]"; - String multilinestring = "[[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")],[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]]"; - String polygon = "[[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")],[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]]"; - String multipolygon = "[[[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")],[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]]]"; - - insertData(String.format("INSERT INTO test_geometric VALUES ( 1, %s, %s, %s, %s, %s, %s )", - point, ring, linestring, multilinestring, polygon, multipolygon)); - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_geometric ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getString("point"), point); - assertEquals(rs.getString("linestring"), linestring); - assertEquals(rs.getString("polygon"), polygon); - assertEquals(rs.getString("multilinestring"), multilinestring); - assertEquals(rs.getString("multipolygon"), multipolygon); - assertEquals(rs.getString("ring"), ring); - - assertFalse(rs.next()); - } - } - } - } - - - @Test(groups = { "integration" }) - public void testDynamicTypesSimpleStatement() throws SQLException { - if (earlierThan(24, 8)) { - return; - } - - Properties properties = new Properties(); - properties.setProperty(ClientConfigProperties.serverSetting("allow_experimental_dynamic_type"), "1"); - runQuery("CREATE TABLE test_dynamic (order Int8, " - + "dynamic Dynamic" - + ") ENGINE = MergeTree ORDER BY ()", - properties); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - String dynamic = "string" + rand.nextInt(1000); - int dynamic2 = rand.nextInt(256) - 128; - double dynamic3 = rand.nextDouble(); - - String sql = String.format("INSERT INTO test_dynamic VALUES ( 1, '%s' )", dynamic); - insertData(sql); - - sql = String.format("INSERT INTO test_dynamic VALUES ( 2, %d )", dynamic2); - insertData(sql); - - sql = String.format("INSERT INTO test_dynamic VALUES ( 3, %s )", dynamic3); - insertData(sql); - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dynamic ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getString("dynamic"), dynamic); - - assertTrue(rs.next()); - assertEquals(rs.getInt("dynamic"), dynamic2); - - assertTrue(rs.next()); - assertEquals(rs.getDouble("dynamic"), dynamic3); - - assertFalse(rs.next()); - } - } - } - } - - - @Test(groups = { "integration" }) - public void testTypeConversions() throws Exception { - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT 1, 'true', '1.0', " + - "toDate('2024-12-01'), toDateTime('2024-12-01 12:34:56'), toDateTime64('2024-12-01 12:34:56.789', 3), toDateTime64('2024-12-01 12:34:56.789789', 6), toDateTime64('2024-12-01 12:34:56.789789789', 9)")) { - assertTrue(rs.next()); - assertEquals(rs.getInt(1), 1); - assertEquals(String.valueOf(rs.getObject(1)), "1"); - assertEquals(rs.getObject(1, Integer.class), 1); - assertEquals(rs.getObject(1, Long.class), 1L); - assertEquals(String.valueOf(rs.getObject(1, new HashMap>(){{put(JDBCType.INTEGER.getName(), Integer.class);}})), "1"); - - assertTrue(rs.getBoolean(2)); - assertEquals(String.valueOf(rs.getObject(2)), "true"); - assertEquals(rs.getObject(2, Boolean.class), true); - assertEquals(String.valueOf(rs.getObject(2, new HashMap>(){{put(JDBCType.BOOLEAN.getName(), Boolean.class);}})), "true"); - - assertEquals(rs.getFloat(3), 1.0f); - assertEquals(String.valueOf(rs.getObject(3)), "1.0"); - assertEquals(rs.getObject(3, Float.class), 1.0f); - assertEquals(rs.getObject(3, Double.class), 1.0); - assertEquals(String.valueOf(rs.getObject(3, new HashMap>(){{put(JDBCType.FLOAT.getName(), Float.class);}})), "1.0"); - - assertEquals(rs.getDate(4), Date.valueOf("2024-12-01")); - assertTrue(rs.getObject(4) instanceof Date); - assertEquals(rs.getObject(4), Date.valueOf("2024-12-01")); - assertEquals(rs.getString(4), "2024-12-01");//Underlying object is ZonedDateTime - assertEquals(rs.getObject(4, LocalDate.class), LocalDate.of(2024, 12, 1)); - assertThrows(SQLException.class, () -> rs.getObject(4, ZonedDateTime.class)); // Date cannot be presented as time - assertEquals(String.valueOf(rs.getObject(4, new HashMap>(){{put(JDBCType.DATE.getName(), LocalDate.class);}})), "2024-12-01"); - - assertEquals(rs.getTimestamp(5).toString(), "2024-12-01 12:34:56.0"); - assertTrue(rs.getObject(5) instanceof Timestamp); - assertEquals(rs.getObject(5), Timestamp.valueOf("2024-12-01 12:34:56")); - assertEquals(rs.getString(5), "2024-12-01 12:34:56"); - assertEquals(rs.getObject(5, LocalDateTime.class), LocalDateTime.of(2024, 12, 1, 12, 34, 56)); - assertEquals(rs.getObject(5, ZonedDateTime.class), ZonedDateTime.of(2024, 12, 1, 12, 34, 56, 0, ZoneId.of("UTC"))); - assertEquals(String.valueOf(rs.getObject(5, new HashMap>(){{put(JDBCType.TIMESTAMP.getName(), LocalDateTime.class);}})), "2024-12-01T12:34:56"); - - assertEquals(rs.getTimestamp(6).toString(), "2024-12-01 12:34:56.789"); - assertTrue(rs.getObject(6) instanceof Timestamp); - assertEquals(rs.getObject(6), Timestamp.valueOf("2024-12-01 12:34:56.789")); - assertEquals(rs.getString(6), "2024-12-01 12:34:56.789"); - assertEquals(rs.getObject(6, LocalDateTime.class), LocalDateTime.of(2024, 12, 1, 12, 34, 56, 789000000)); - assertEquals(String.valueOf(rs.getObject(6, new HashMap>(){{put(JDBCType.TIMESTAMP.getName(), LocalDateTime.class);}})), "2024-12-01T12:34:56.789"); - - assertEquals(rs.getTimestamp(7).toString(), "2024-12-01 12:34:56.789789"); - assertTrue(rs.getObject(7) instanceof Timestamp); - assertEquals(rs.getObject(7), Timestamp.valueOf("2024-12-01 12:34:56.789789")); - assertEquals(rs.getString(7), "2024-12-01 12:34:56.789789"); - assertEquals(rs.getObject(7, LocalDateTime.class), LocalDateTime.of(2024, 12, 1, 12, 34, 56, 789789000)); - assertEquals(String.valueOf(rs.getObject(7, new HashMap>(){{put(JDBCType.TIMESTAMP.getName(), OffsetDateTime.class);}})), "2024-12-01T12:34:56.789789Z"); - - assertEquals(rs.getTimestamp(8).toString(), "2024-12-01 12:34:56.789789789"); - assertTrue(rs.getObject(8) instanceof Timestamp); - assertEquals(rs.getObject(8), Timestamp.valueOf("2024-12-01 12:34:56.789789789")); - assertEquals(rs.getString(8), "2024-12-01 12:34:56.789789789"); - assertEquals(rs.getObject(8, LocalDateTime.class), LocalDateTime.of(2024, 12, 1, 12, 34, 56, 789789789)); - assertEquals(String.valueOf(rs.getObject(8, new HashMap>(){{put(JDBCType.TIMESTAMP.getName(), ZonedDateTime.class);}})), "2024-12-01T12:34:56.789789789Z[UTC]"); - } - } - } - } - - @Test(groups = { "integration" }) - public void testVariantTypesSimpleStatement() throws SQLException { - if (earlierThan(24, 8)) { - return; - } - - Properties properties = new Properties(); - properties.setProperty(ClientConfigProperties.serverSetting("allow_experimental_variant_type"), "1"); - runQuery("CREATE TABLE test_variant (order Int8, " - + "v Variant(String, Int32)" - + ") ENGINE = MergeTree ORDER BY ()", - properties); - - // Insert random (valid) values - long seed = System.currentTimeMillis(); - Random rand = new Random(seed); - log.info("Random seed was: {}", seed); - - String variant1 = "string" + rand.nextInt(1000); - int variant2 = rand.nextInt(256) - 128; - - String sql = String.format("INSERT INTO test_variant VALUES ( 1, '%s' )", variant1); - insertData(sql); - - sql = String.format("INSERT INTO test_variant VALUES ( 2, %d )", variant2); - insertData(sql); - - - // Check the results - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_variant ORDER BY order")) { - assertTrue(rs.next()); - assertEquals(rs.getString("v"), variant1); - assertTrue(rs.getObject("v") instanceof String); - - assertTrue(rs.next()); - assertEquals(rs.getInt("v"), variant2); - assertTrue(rs.getObject("v") instanceof Number); - - assertFalse(rs.next()); - } - } - } - } - - @Test(groups = { "integration" }) - public void testGeoPoint1() throws Exception { - final Double[][] spatialArrayData = new Double[][] { - {4.837388, 52.38795}, - {4.951513, 52.354582}, - {4.961987, 52.371763}, - {4.870017, 52.334932}, - {4.89813, 52.357238}, - {4.852437, 52.370315}, - {4.901712, 52.369567}, - {4.874112, 52.339823}, - {4.856942, 52.339122}, - {4.870253, 52.360353}, - }; - - StringBuilder sql = new StringBuilder(); - sql.append("SELECT \n"); - sql.append("\tcast(arrayJoin(["); - for (int i = 0; i < spatialArrayData.length; i++) { - sql.append("(" + spatialArrayData[i][0] + ", " + spatialArrayData[i][1] + ")").append(','); - } - sql.setLength(sql.length() - 1); - sql.append("])"); - sql.append("as Point) as Point"); - - - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery(sql.toString())) { - - ResultSetMetaData metaData = rs.getMetaData(); - assertEquals(metaData.getColumnCount(), 1); - assertEquals(metaData.getColumnTypeName(1), ClickHouseDataType.Point.name()); - assertEquals(metaData.getColumnType(1), Types.ARRAY); - - int rowCount = 0; - while (rs.next()) { - Object asObject = rs.getObject(1); - assertTrue(asObject instanceof double[]); - Array asArray = rs.getArray(1); - assertEquals(asArray.getArray(), spatialArrayData[rowCount]); - assertEquals(asObject, asArray.getArray()); - rowCount++; - } - assertTrue(rowCount > 0); - } - } - } - - @Test(groups = { "integration" }) - public void testGeoPoint() throws Exception { - final double[] row = new double[] { - 10.123456789, - 11.123456789 - }; - - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - String table = "test_geo_point"; - stmt.executeUpdate("DROP TABLE IF EXISTS " + table); - stmt.executeUpdate("CREATE TABLE " + table + " (geom Point) ENGINE = MergeTree ORDER BY ()"); - - try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { - Double[] rowObj = Arrays.stream(row).boxed().toArray(Double[]::new); - pstmt.setObject(1, conn.createStruct("Tuple(Float64, Float64)", rowObj)); - pstmt.executeUpdate(); - } - - try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { - int geomColumn = 1; - ResultSetMetaData rsMd = rs.getMetaData(); - assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.Point.name()); - assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); - - rs.next(); - assertTrue(rs.isLast()); - Object asObject = rs.getObject(geomColumn); - assertTrue(asObject instanceof double[]); - Array asArray = rs.getArray(geomColumn); - assertEquals(asArray.getArray(), row); - assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.Point.name()); - assertEquals(asArray.getBaseType(), Types.ARRAY); - } - } - } - - @Test(groups = { "integration" }) - public void testGeoRing() throws Exception { - final Double[][] row = new Double[][] { - {10.123456789, 11.123456789}, - {12.123456789, 13.123456789}, - {14.123456789, 15.123456789}, - {10.123456789, 11.123456789}, - }; - - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - final String table = "test_geo_ring"; - stmt.executeUpdate("DROP TABLE IF EXISTS " + table); - stmt.executeUpdate("CREATE TABLE " + table + " (geom Ring) ENGINE = MergeTree ORDER BY ()"); - - try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { - pstmt.setObject(1, conn.createArrayOf("Array(Point)", row)); - pstmt.executeUpdate(); - } - - try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { - int geomColumn = 1; - ResultSetMetaData rsMd = rs.getMetaData(); - assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.Ring.name()); - assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); - - rs.next(); - assertTrue(rs.isLast()); - Object asObject = rs.getObject(geomColumn); - assertTrue(asObject instanceof double[][]); - Array asArray = rs.getArray(geomColumn); - assertEquals(asArray.getArray(), row); - assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.Ring.name()); - assertEquals(asArray.getBaseType(), Types.ARRAY); - } - } - } - - @Test(groups = { "integration" }) - public void testGeoLineString() throws Exception { - final Double[][] row = new Double[][] { - {10.123456789, 11.123456789}, - {12.123456789, 13.123456789}, - {14.123456789, 15.123456789}, - {10.123456789, 11.123456789}, - }; - - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - final String table = "test_geo_line_string"; - stmt.executeUpdate("DROP TABLE IF EXISTS " + table); - stmt.executeUpdate("CREATE TABLE " + table +" (geom LineString) ENGINE = MergeTree ORDER BY ()"); - - try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { - pstmt.setObject(1, conn.createArrayOf("Array(Point)", row)); - pstmt.executeUpdate(); - } - - try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { - int geomColumn = 1; - ResultSetMetaData rsMd = rs.getMetaData(); - assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.LineString.name()); - assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); - - rs.next(); - assertTrue(rs.isLast()); - Object asObject = rs.getObject(geomColumn); - assertTrue(asObject instanceof double[][]); - Array asArray = rs.getArray(geomColumn); - assertEquals(asArray.getArray(), row); - assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.LineString.name()); - assertEquals(asArray.getBaseType(), Types.ARRAY); - } - } - } - - @Test(groups = { "integration" }) - public void testGeoMultiLineString() throws Exception { - final Double[][][] row = new Double[][][] { - { // LineString 1 - {10.123456789, 11.123456789}, - {12.123456789, 13.123456789}, - {14.123456789, 15.123456789}, - {10.123456789, 11.123456789}, - }, - { - {16.123456789, 17.123456789}, - {18.123456789, 19.123456789}, - {20.123456789, 21.123456789}, - {16.123456789, 17.123456789}, - } - }; - - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - final String table = "test_geo_multi_line_string"; - stmt.executeUpdate("DROP TABLE IF EXISTS " + table); - stmt.executeUpdate("CREATE TABLE " + table +" (geom MultiLineString) ENGINE = MergeTree ORDER BY ()"); - - try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { - pstmt.setObject(1, conn.createArrayOf("Array(Array(Point))", row)); - pstmt.executeUpdate(); - } - - try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { - int geomColumn = 1; - ResultSetMetaData rsMd = rs.getMetaData(); - assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.MultiLineString.name()); - assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); - - rs.next(); - assertTrue(rs.isLast()); - Object asObject = rs.getObject(geomColumn); - assertTrue(asObject instanceof double[][][]); - Array asArray = rs.getArray(geomColumn); - assertEquals(asArray.getArray(), row); - assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.MultiLineString.name()); - assertEquals(asArray.getBaseType(), Types.ARRAY); - } - } - } - - @Test(groups = { "integration" }) - public void testGeoPolygon() throws Exception { - final Double[][][] row = new Double[][][] { - { // Ring 1 - {10.123456789, 11.123456789}, - {12.123456789, 13.123456789}, - {14.123456789, 15.123456789}, - {10.123456789, 11.123456789}, - }, - { // Ring 2 - {16.123456789, 17.123456789}, - {18.123456789, 19.123456789}, - {20.123456789, 21.123456789}, - {16.123456789, 17.123456789}, - } - }; - - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - final String table = "test_geo_polygon"; - stmt.executeUpdate("DROP TABLE IF EXISTS " + table); - stmt.executeUpdate("CREATE TABLE " + table +" (geom Polygon) ENGINE = MergeTree ORDER BY ()"); - - try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { - pstmt.setObject(1, conn.createArrayOf("Array(Array(Point))", row)); - pstmt.executeUpdate(); - } - - try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { - int geomColumn = 1; - ResultSetMetaData rsMd = rs.getMetaData(); - assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.Polygon.name()); - assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); - - rs.next(); - assertTrue(rs.isLast()); - Object asObject = rs.getObject(geomColumn); - assertTrue(asObject instanceof double[][][]); - Array asArray = rs.getArray(geomColumn); - assertEquals(asArray.getArray(), row); - assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.Polygon.name()); - assertEquals(asArray.getBaseType(), Types.ARRAY); - } - } - } - - @Test(groups = { "integration" }) - public void testGeoMultiPolygon() throws Exception { - final Double[][][][] row = new Double[][][][] { - { // Polygon 1 - { // Ring 1 - {10.123456789, 11.123456789}, - {12.123456789, 13.123456789}, - {14.123456789, 15.123456789}, - {10.123456789, 11.123456789}, - }, - { // Ring 2 - {16.123456789, 17.123456789}, - {18.123456789, 19.123456789}, - {20.123456789, 21.123456789}, - {16.123456789, 17.123456789}, - } - }, - { // Polygon 2 - { // Ring 1 - {-10.123456789, -11.123456789}, - {-12.123456789, -13.123456789}, - {-14.123456789, -15.123456789}, - {-10.123456789, -11.123456789}, - }, - { // Ring 2 - {-16.123456789, -17.123456789}, - {-18.123456789, -19.123456789}, - {-20.123456789, -21.123456789}, - {-16.123456789, -17.123456789}, - } - } - }; - - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - final String table = "test_geo_muti_polygon"; - stmt.executeUpdate("DROP TABLE IF EXISTS " + table); - stmt.executeUpdate("CREATE TABLE " + table +" (geom MultiPolygon) ENGINE = MergeTree ORDER BY ()"); - - try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { - pstmt.setObject(1, conn.createArrayOf("Array(Array(Array(Point)))", row)); - pstmt.executeUpdate(); - } - - try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { - int geomColumn = 1; - ResultSetMetaData rsMd = rs.getMetaData(); - assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.MultiPolygon.name()); - assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); - - rs.next(); - assertTrue(rs.isLast()); - Object asObject = rs.getObject(geomColumn); - assertTrue(asObject instanceof double[][][][]); - Array asArray = rs.getArray(geomColumn); - assertEquals(asArray.getArray(), row); - assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.MultiPolygon.name()); - assertEquals(asArray.getBaseType(), Types.ARRAY); - } - } - } - - private static final HashMap EMPTY_JSON = new HashMap<>(); - - @Test(groups = { "integration" }, dataProvider = "testJSONReadDP") - public void testJSONRead(String json, Object expected) throws Exception { - if (ClickHouseVersion.of(getServerVersion()).check("(,24.8]")) { - return; // JSON was introduced in 24.10 - } - Properties createProperties = new Properties(); - createProperties.put(ClientConfigProperties.serverSetting("allow_experimental_json_type"), "1"); - runQuery("DROP TABLE IF EXISTS test_jdbc_json_read"); - runQuery("CREATE TABLE test_jdbc_json_read (data JSON) ENGINE = MergeTree ORDER BY ()", createProperties); - - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - final String sql = "INSERT INTO test_jdbc_json_read (data) VALUES ('%s'), ('{}')"; - stmt.executeUpdate(String.format(sql, json)); - - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_jdbc_json_read")) { - - assertTrue(rs.next()); - Object jsonObj = rs.getObject(1); - if (expected == null) { - expected = jsonToClientMap(json); - } - assertEquals(jsonObj, expected); - assertTrue(rs.next()); - Object emptyJsonObj = rs.getObject(1); - assertEquals(emptyJsonObj, EMPTY_JSON); - assertFalse(rs.next()); - } - } - } - - private final ObjectMapper objectMapper = new ObjectMapper() - .enable(DeserializationFeature.USE_LONG_FOR_INTS); - - private HashMap jsonToClientMap(String json) { - try { - return objectMapper.readValue(json, new TypeReference>() {}); - } catch (IOException e) { - throw new RuntimeException("Failed to read json to Map", e); - } - } - - @DataProvider(name = "testJSONReadDP") - public Object[][] testJSONReadDP() { - Map map1 = new HashMap<>(); - map1.put("nested.key", "value"); - Map map2 = new HashMap<>(); - map2.put("nested.numbers",new ArrayList() {{ add(1L); add(2L); add(3L); }}); - Map map3 = new HashMap<>(); - map3.put("nested.strings", new ArrayList() {{ add("one"); add("two"); add("three"); }}); - Map map4 = new HashMap<>(); - map4.put("array", new ArrayList>() {{ - add(new HashMap() {{ - put("nested.key", "value"); - }}); - add(new HashMap() {{ - put("nested.numbers", new ArrayList() {{ - add(1L); - add(2L); - add(3L); - }}); - }}); - }}); - Map map5 = new HashMap<>(); - map5.put("array", new ArrayList>() {{ - add(new HashMap() {{ - put("nested.strings", new ArrayList() {{ add("one"); add("two"); add("three"); }}); - - }}); - }}); - Map map6 = new HashMap<>(); - map6.put("level1.level2.level3", "value"); - - Map map7 = new HashMap<>(); - map7.put("level1.level2.level3.level4", "value"); - - return new Object[][] { - {"{\"key\": \"value\"}", null}, // Simple object - {"{\"numbers\":[1, 2, 3]}", null}, - {"{\"strings\":[\"one\", \"two\", \"three\"]}", null}, - {"{\"nested\":{\"key\": \"value\"}}", map1}, // nested objects - {"{\"nested\":{\"numbers\":[1, 2, 3]}}", map2}, // nested objects - {"{\"nested\":{\"strings\":[\"one\", \"two\", \"three\"]}}", map3}, // nested objects - {"{\"array\":[{\"key\": \"value\"},{\"key\": \"value\"}]}", null}, // array of objects - {"{\"array\":[{\"numbers\":[1, 2, 3]},{\"strings\":[\"one\", \"two\", \"three\"]}]}", null}, // array of objects - {"{\"array\":[{\"nested\":{\"key\": \"value\"}},{\"nested\":{\"numbers\":[1, 2, 3]}}]}", map4}, // array of objects - {"{\"array\":[{\"nested\":{\"strings\":[\"one\", \"two\", \"three\"]}}]}", map5}, // array of objects - {"{\"array\":[{\"nested\":[{\"key\": \"value\"}]}]}", null}, // simple array of objects - {"{\"level1\": {\"level2\": {\"level3\": \"value\"}}}", map6}, // deep nested objects - {"{\"level1\": {\"level2\": {\"level3\": {\"level4\": \"value\"}}}}", map7}, // deep nested objects - - }; - } - - /** - * Tests that both Time and DateTime columns are readable as JDBC TIME type. - * ClickHouse added Time and Time64 support in version 25.6. - * On older versions DateTime types were used to emulate TIME. - * This test ensures compatibility for reading time values from both column types. - */ - @Test(groups = { "integration" }) - public void testTimeAndDateTimeCompatibleWithJDBCTime() throws Exception { - boolean hasTimeType = !ClickHouseVersion.of(getServerVersion()).check("(,25.5]"); - - Properties createProperties = new Properties(); - if (hasTimeType) { - createProperties.put(ClientConfigProperties.serverSetting("allow_experimental_time_time64_type"), "1"); - } - - // Create table with DateTime columns (always supported) and Time columns (if available) - String tableDDL = hasTimeType - ? "CREATE TABLE test_time_compat (order Int8, " - + "time Time, time64 Time64(3), " - + "dateTime DateTime('UTC'), dateTime64 DateTime64(3, 'UTC') " - + ") ENGINE = MergeTree ORDER BY ()" - : "CREATE TABLE test_time_compat (order Int8, " - + "dateTime DateTime('UTC'), dateTime64 DateTime64(3, 'UTC') " - + ") ENGINE = MergeTree ORDER BY ()"; - - runQuery(tableDDL, createProperties); - - // Insert values representing times: 12:34:56 and 23:59:59.999 - String insertSQL = hasTimeType - ? "INSERT INTO test_time_compat (order, time, time64, dateTime, dateTime64) VALUES " - + "(1, '12:34:56', '12:34:56.789', '1970-01-01 12:34:56', '1970-01-01 12:34:56.789'), " - + "(2, '23:59:59', '23:59:59.999', '1970-01-01 23:59:59', '1970-01-01 23:59:59.999')" - : "INSERT INTO test_time_compat (order, dateTime, dateTime64) VALUES " - + "(1, '1970-01-01 12:34:56', '1970-01-01 12:34:56.789'), " - + "(2, '1970-01-01 23:59:59', '1970-01-01 23:59:59.999')"; - - runQuery(insertSQL, createProperties); - - // Expected values for each row: [order, year, month, day, hours, minutes, seconds, milliseconds] - // Note: month is 1-based (1 = January) - int[][] expectedValues = { - {1, 1970, 1, 1, 12, 34, 56, 789}, - {2, 1970, 1, 1, 23, 59, 59, 999} - }; - - Calendar utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - - // Check that all columns are readable as java.sql.Time and verify date components - try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_time_compat ORDER BY order")) { - for (int[] expected : expectedValues) { - assertTrue(rs.next()); - assertEquals(rs.getInt("order"), expected[0]); - - // Test DateTime columns as Time (always available) - verifyTimeValue(rs.getTime("dateTime", utcCalendar), expected[4], expected[5], expected[6], 0, utcCalendar); - verifyTimeValue(rs.getTime("dateTime64", utcCalendar), expected[4], expected[5], expected[6], expected[7], utcCalendar); - - // Verify date components for DateTime columns - verifyDateValue(rs.getDate("dateTime", utcCalendar), expected[1], expected[2], expected[3], utcCalendar); - verifyDateValue(rs.getDate("dateTime64", utcCalendar), expected[1], expected[2], expected[3], utcCalendar); - - if (hasTimeType) { - // Test Time columns as Time - verifyTimeValue(rs.getTime("time", utcCalendar), expected[4], expected[5], expected[6], 0, utcCalendar); - verifyTimeValue(rs.getTime("time64", utcCalendar), expected[4], expected[5], expected[6], expected[7], utcCalendar); - } - } - assertFalse(rs.next()); - } - } - } - } - - private static void assertNotNull(Object obj) { - Assert.assertNotNull(obj); - } - - private static void verifyTimeValue(Time time, int expectedHours, int expectedMinutes, - int expectedSeconds, int expectedMillis, Calendar calendar) { - assertNotNull(time); - calendar.setTime(time); - assertEquals(calendar.get(Calendar.HOUR_OF_DAY), expectedHours); - assertEquals(calendar.get(Calendar.MINUTE), expectedMinutes); - assertEquals(calendar.get(Calendar.SECOND), expectedSeconds); - if (expectedMillis > 0) { - assertEquals(time.getTime() % 1000, expectedMillis); - } - } - - private static void verifyDateValue(Date date, int expectedYear, int expectedMonth, - int expectedDay, Calendar calendar) { - assertNotNull(date); - calendar.setTime(date); - assertEquals(calendar.get(Calendar.YEAR), expectedYear); - assertEquals(calendar.get(Calendar.MONTH) + 1, expectedMonth); // Calendar.MONTH is 0-based, convert to 1-based - assertEquals(calendar.get(Calendar.DAY_OF_MONTH), expectedDay); - } -} +package com.clickhouse.jdbc; + +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.DataTypeUtils; +import com.clickhouse.client.api.internal.ServerSettings; +import com.clickhouse.client.api.sql.SQLUtils; +import com.clickhouse.data.ClickHouseDataType; +import com.clickhouse.data.ClickHouseVersion; +import com.clickhouse.data.Tuple; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.sql.Array; +import java.sql.Connection; +import java.sql.Date; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Struct; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.text.DecimalFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; + +@Test(groups = { "integration" }) +public class JdbcDataTypeTests extends JdbcIntegrationTest { + private static final Logger log = LoggerFactory.getLogger(JdbcDataTypeTests.class); + + @BeforeClass(groups = { "integration" }) + public static void setUp() throws SQLException { + Driver.load(); + } + + private int insertData(String sql) throws SQLException { + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + return stmt.executeUpdate(sql); + } + } + } + + @Test(groups = { "integration" }) + public void testIntegerTypes() throws SQLException { + runQuery("CREATE TABLE test_integers (order Int8, " + + "int8 Int8, int16 Int16, int32 Int32, int64 Int64, int128 Int128, int256 Int256, " + + "uint8 UInt8, uint16 UInt16, uint32 UInt32, uint64 UInt64, uint128 UInt128, uint256 UInt256" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert minimum values + insertData("INSERT INTO test_integers VALUES ( 1, " + + "-128, -32768, -2147483648, -9223372036854775808, -170141183460469231731687303715884105728, -57896044618658097711785492504343953926634992332820282019728792003956564819968, " + + "0, 0, 0, 0, 0, 0" + + ")"); + + // Insert maximum values + insertData("INSERT INTO test_integers VALUES ( 2, " + + "127, 32767, 2147483647, 9223372036854775807, 170141183460469231731687303715884105727, 57896044618658097711785492504343953926634992332820282019728792003956564819967, " + + "255, 65535, 4294967295, 18446744073709551615, 340282366920938463463374607431768211455, 115792089237316195423570985008687907853269984665640564039457584007913129639935" + + ")"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + int int8 = rand.nextInt(256) - 128; + int int16 = rand.nextInt(65536) - 32768; + int int32 = rand.nextInt(); + long int64 = rand.nextLong(); + BigInteger int128 = new BigInteger(127, rand); + BigInteger int256 = new BigInteger(255, rand); + Short uint8 = Integer.valueOf(rand.nextInt(256)).shortValue(); + int uint16 = rand.nextInt(65536); + long uint32 = rand.nextInt() & 0xFFFFFFFFL; + BigInteger uint64 = BigInteger.valueOf(rand.nextLong(Long.MAX_VALUE)); + BigInteger uint128 = new BigInteger(128, rand); + BigInteger uint256 = new BigInteger(256, rand); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_integers VALUES ( 3, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { + stmt.setInt(1, int8); + stmt.setInt(2, int16); + stmt.setInt(3, int32); + stmt.setLong(4, int64); + stmt.setBigDecimal(5, new BigDecimal(int128)); + stmt.setBigDecimal(6, new BigDecimal(int256)); + stmt.setInt(7, uint8); + stmt.setInt(8, uint16); + stmt.setLong(9, uint32); + stmt.setBigDecimal(10, new BigDecimal(uint64)); + stmt.setBigDecimal(11, new BigDecimal(uint128)); + stmt.setBigDecimal(12, new BigDecimal(uint256)); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_integers ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getByte("int8"), Byte.MIN_VALUE); + assertEquals(rs.getShort("int16"), Short.MIN_VALUE); + assertEquals(rs.getInt("int32"), Integer.MIN_VALUE); + assertEquals(rs.getLong("int64"), Long.MIN_VALUE); + assertEquals(rs.getBigDecimal("int128"), new BigDecimal("-170141183460469231731687303715884105728")); + assertEquals(rs.getBigDecimal("int256"), new BigDecimal("-57896044618658097711785492504343953926634992332820282019728792003956564819968")); + assertEquals(rs.getShort("uint8"), 0); + assertEquals(rs.getInt("uint16"), 0); + assertEquals(rs.getLong("uint32"), 0); + assertEquals(rs.getBigDecimal("uint64"), new BigDecimal("0")); + assertEquals(rs.getBigDecimal("uint128"), new BigDecimal("0")); + assertEquals(rs.getBigDecimal("uint256"), new BigDecimal("0")); + + assertTrue(rs.next()); + assertEquals(rs.getByte("int8"), Byte.MAX_VALUE); + assertEquals(rs.getShort("int16"), Short.MAX_VALUE); + assertEquals(rs.getInt("int32"), Integer.MAX_VALUE); + assertEquals(rs.getLong("int64"), Long.MAX_VALUE); + assertEquals(rs.getBigDecimal("int128"), new BigDecimal("170141183460469231731687303715884105727")); + assertEquals(rs.getBigDecimal("int256"), new BigDecimal("57896044618658097711785492504343953926634992332820282019728792003956564819967")); + assertEquals(rs.getShort("uint8"), 255); + assertEquals(rs.getInt("uint16"), 65535); + assertEquals(rs.getLong("uint32"), 4294967295L); + assertEquals(rs.getBigDecimal("uint64"), new BigDecimal("18446744073709551615")); + assertEquals(rs.getBigDecimal("uint128"), new BigDecimal("340282366920938463463374607431768211455")); + assertEquals(rs.getBigDecimal("uint256"), new BigDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935")); + + assertTrue(rs.next()); + assertEquals(rs.getByte("int8"), int8); + assertEquals(rs.getShort("int16"), int16); + assertEquals(rs.getInt("int32"), int32); + assertEquals(rs.getLong("int64"), int64); + assertEquals(rs.getBigDecimal("int128"), new BigDecimal(int128)); + assertEquals(rs.getBigDecimal("int256"), new BigDecimal(int256)); + assertEquals(rs.getShort("uint8"), uint8); + assertEquals(rs.getInt("uint16"), uint16); + assertEquals(rs.getLong("uint32"), uint32); + assertEquals(rs.getBigDecimal("uint64"), new BigDecimal(uint64)); + assertEquals(rs.getBigDecimal("uint128"), new BigDecimal(uint128)); + assertEquals(rs.getBigDecimal("uint256"), new BigDecimal(uint256)); + + assertFalse(rs.next()); + } + } + } + + // Check the with getObject + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_integers ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getObject("int8"), Byte.MIN_VALUE); + assertEquals(rs.getObject("int16"), Short.MIN_VALUE); + assertEquals(rs.getObject("int32"), Integer.MIN_VALUE); + assertEquals(rs.getObject("int64"), Long.MIN_VALUE); + assertEquals(rs.getObject("int128"), new BigInteger("-170141183460469231731687303715884105728")); + assertEquals(rs.getObject("int256"), new BigInteger("-57896044618658097711785492504343953926634992332820282019728792003956564819968")); + assertEquals(rs.getObject("uint8"), Short.valueOf("0")); + assertEquals(rs.getObject("uint16"), 0); + assertEquals(rs.getObject("uint32"), 0L); + assertEquals(rs.getObject("uint64"), new BigInteger("0")); + assertEquals(rs.getObject("uint128"), new BigInteger("0")); + assertEquals(rs.getObject("uint256"), new BigInteger("0")); + + assertTrue(rs.next()); + assertEquals(rs.getObject("int8"), Byte.MAX_VALUE); + assertEquals(rs.getObject("int16"), Short.MAX_VALUE); + assertEquals(rs.getObject("int32"), Integer.MAX_VALUE); + assertEquals(rs.getObject("int64"), Long.MAX_VALUE); + assertEquals(rs.getObject("int128"), new BigInteger("170141183460469231731687303715884105727")); + assertEquals(rs.getObject("int256"), new BigInteger("57896044618658097711785492504343953926634992332820282019728792003956564819967")); + assertEquals(rs.getObject("uint8"), Short.valueOf("255")); + assertEquals(rs.getObject("uint16"), 65535); + assertEquals(rs.getObject("uint32"), 4294967295L); + assertEquals(rs.getObject("uint64"), new BigInteger("18446744073709551615")); + assertEquals(rs.getObject("uint128"), new BigInteger("340282366920938463463374607431768211455")); + assertEquals(rs.getObject("uint256"), new BigInteger("115792089237316195423570985008687907853269984665640564039457584007913129639935")); + + assertTrue(rs.next()); + assertEquals(rs.getObject("int8"), (byte)int8); + assertEquals(rs.getObject("int16"), (short)int16); + assertEquals(rs.getObject("int32"), int32); + assertEquals(rs.getObject("int64"), int64); + assertEquals(rs.getObject("int128"), int128); + assertEquals(rs.getObject("int256"), int256); + assertEquals(rs.getObject("uint8"), uint8); + assertEquals(rs.getObject("uint16"), uint16); + assertEquals(rs.getObject("uint32"), uint32); + assertEquals(rs.getObject("uint64"), uint64); + assertEquals(rs.getObject("uint128"), uint128); + assertEquals(rs.getObject("uint256"), uint256); + + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testBigIntegerTypesMapping() throws SQLException { + String tableName = "test_biginteger_mapping"; + + // Create table with all large integer types + runQuery("CREATE TABLE " + tableName + " (" + + "id Int32, " + + "int128_col Int128, " + + "int256_col Int256, " + + "uint64_col UInt64, " + + "uint128_col UInt128, " + + "uint256_col UInt256, " + + "int128_null Nullable(Int128), " + + "int256_null Nullable(Int256), " + + "uint64_null Nullable(UInt64), " + + "uint128_null Nullable(UInt128), " + + "uint256_null Nullable(UInt256)" + + ") ENGINE = MergeTree ORDER BY id"); + + // Test values + BigInteger int128Min = new BigInteger("-170141183460469231731687303715884105728"); // -2^127 + BigInteger int128Max = new BigInteger("170141183460469231731687303715884105727"); // 2^127 - 1 + BigInteger int256Min = new BigInteger("-57896044618658097711785492504343953926634992332820282019728792003956564819968"); // -2^255 + BigInteger int256Max = new BigInteger("57896044618658097711785492504343953926634992332820282019728792003956564819967"); // 2^255 - 1 + BigInteger uint64Max = new BigInteger("18446744073709551615"); // 2^64 - 1 + BigInteger uint128Max = new BigInteger("340282366920938463463374607431768211455"); // 2^128 - 1 + BigInteger uint256Max = new BigInteger("115792089237316195423570985008687907853269984665640564039457584007913129639935"); // 2^256 - 1 + + // Insert minimum values + insertData("INSERT INTO " + tableName + " VALUES (" + + "1, " + + int128Min + ", " + int256Min + ", 0, 0, 0, " + + "NULL, NULL, NULL, NULL, NULL" + + ")"); + + // Insert maximum values + insertData("INSERT INTO " + tableName + " VALUES (" + + "2, " + + int128Max + ", " + int256Max + ", " + uint64Max + ", " + uint128Max + ", " + uint256Max + ", " + + "NULL, NULL, NULL, NULL, NULL" + + ")"); + + // Insert random values with PreparedStatement + Random rand = new Random(System.currentTimeMillis()); + BigInteger int128Random = new BigInteger(127, rand); + BigInteger int256Random = new BigInteger(255, rand); + BigInteger uint64Random = BigInteger.valueOf(rand.nextLong(Long.MAX_VALUE)); + BigInteger uint128Random = new BigInteger(128, rand); + BigInteger uint256Random = new BigInteger(256, rand); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO " + tableName + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { + pstmt.setInt(1, 3); + pstmt.setObject(2, int128Random); + pstmt.setObject(3, int256Random); + pstmt.setObject(4, uint64Random); + pstmt.setObject(5, uint128Random); + pstmt.setObject(6, uint256Random); + pstmt.setObject(7, int128Random); + pstmt.setObject(8, int256Random); + pstmt.setObject(9, uint64Random); + pstmt.setObject(10, uint128Random); + pstmt.setObject(11, uint256Random); + pstmt.executeUpdate(); + } + } + + // Verify results and metadata + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName + " ORDER BY id")) { + ResultSetMetaData meta = rs.getMetaData(); + + // Verify metadata for each large integer column + // Int128 + assertEquals(meta.getColumnType(2), Types.NUMERIC, "Int128 should map to Types.NUMERIC"); + assertEquals(meta.getColumnTypeName(2), "Int128", "Int128 column type name"); + assertEquals(meta.getColumnClassName(2), BigInteger.class.getName(), "Int128 should map to BigInteger class"); + + // Int256 + assertEquals(meta.getColumnType(3), Types.NUMERIC, "Int256 should map to Types.NUMERIC"); + assertEquals(meta.getColumnTypeName(3), "Int256", "Int256 column type name"); + assertEquals(meta.getColumnClassName(3), BigInteger.class.getName(), "Int256 should map to BigInteger class"); + + // UInt64 + assertEquals(meta.getColumnType(4), Types.NUMERIC, "UInt64 should map to Types.NUMERIC"); + assertEquals(meta.getColumnTypeName(4), "UInt64", "UInt64 column type name"); + assertEquals(meta.getColumnClassName(4), BigInteger.class.getName(), "UInt64 should map to BigInteger class"); + + // UInt128 + assertEquals(meta.getColumnType(5), Types.NUMERIC, "UInt128 should map to Types.NUMERIC"); + assertEquals(meta.getColumnTypeName(5), "UInt128", "UInt128 column type name"); + assertEquals(meta.getColumnClassName(5), BigInteger.class.getName(), "UInt128 should map to BigInteger class"); + + // UInt256 + assertEquals(meta.getColumnType(6), Types.NUMERIC, "UInt256 should map to Types.NUMERIC"); + assertEquals(meta.getColumnTypeName(6), "UInt256", "UInt256 column type name"); + assertEquals(meta.getColumnClassName(6), BigInteger.class.getName(), "UInt256 should map to BigInteger class"); + + // Verify first row (minimum values) + assertTrue(rs.next(), "Should have first row"); + assertEquals(rs.getInt("id"), 1); + + // Verify that getObject() returns BigInteger instances + Object int128Obj = rs.getObject("int128_col"); + assertTrue(int128Obj instanceof BigInteger, "Int128 getObject() should return BigInteger, got: " + int128Obj.getClass().getName()); + assertEquals(int128Obj, int128Min, "Int128 min value"); + + Object int256Obj = rs.getObject("int256_col"); + assertTrue(int256Obj instanceof BigInteger, "Int256 getObject() should return BigInteger, got: " + int256Obj.getClass().getName()); + assertEquals(int256Obj, int256Min, "Int256 min value"); + + Object uint64Obj = rs.getObject("uint64_col"); + assertTrue(uint64Obj instanceof BigInteger, "UInt64 getObject() should return BigInteger, got: " + uint64Obj.getClass().getName()); + assertEquals(uint64Obj, BigInteger.ZERO, "UInt64 zero value"); + + Object uint128Obj = rs.getObject("uint128_col"); + assertTrue(uint128Obj instanceof BigInteger, "UInt128 getObject() should return BigInteger, got: " + uint128Obj.getClass().getName()); + assertEquals(uint128Obj, BigInteger.ZERO, "UInt128 zero value"); + + Object uint256Obj = rs.getObject("uint256_col"); + assertTrue(uint256Obj instanceof BigInteger, "UInt256 getObject() should return BigInteger, got: " + uint256Obj.getClass().getName()); + assertEquals(uint256Obj, BigInteger.ZERO, "UInt256 zero value"); + + // Verify nullable columns + assertNull(rs.getObject("int128_null"), "Nullable Int128 should be null"); + assertNull(rs.getObject("int256_null"), "Nullable Int256 should be null"); + assertNull(rs.getObject("uint64_null"), "Nullable UInt64 should be null"); + assertNull(rs.getObject("uint128_null"), "Nullable UInt128 should be null"); + assertNull(rs.getObject("uint256_null"), "Nullable UInt256 should be null"); + + // Verify second row (maximum values) + assertTrue(rs.next(), "Should have second row"); + assertEquals(rs.getInt("id"), 2); + assertEquals(rs.getObject("int128_col"), int128Max, "Int128 max value"); + assertEquals(rs.getObject("int256_col"), int256Max, "Int256 max value"); + assertEquals(rs.getObject("uint64_col"), uint64Max, "UInt64 max value"); + assertEquals(rs.getObject("uint128_col"), uint128Max, "UInt128 max value"); + assertEquals(rs.getObject("uint256_col"), uint256Max, "UInt256 max value"); + + // Verify third row (random values) + assertTrue(rs.next(), "Should have third row"); + assertEquals(rs.getInt("id"), 3); + assertEquals(rs.getObject("int128_col"), int128Random, "Int128 random value"); + assertEquals(rs.getObject("int256_col"), int256Random, "Int256 random value"); + assertEquals(rs.getObject("uint64_col"), uint64Random, "UInt64 random value"); + assertEquals(rs.getObject("uint128_col"), uint128Random, "UInt128 random value"); + assertEquals(rs.getObject("uint256_col"), uint256Random, "UInt256 random value"); + + // Verify that nullable columns contain the inserted values + assertEquals(rs.getObject("int128_null"), int128Random, "Nullable Int128 with value"); + assertEquals(rs.getObject("int256_null"), int256Random, "Nullable Int256 with value"); + assertEquals(rs.getObject("uint64_null"), uint64Random, "Nullable UInt64 with value"); + assertEquals(rs.getObject("uint128_null"), uint128Random, "Nullable UInt128 with value"); + assertEquals(rs.getObject("uint256_null"), uint256Random, "Nullable UInt256 with value"); + + assertFalse(rs.next(), "Should have no more rows"); + } + } + } + + // Additional test: verify getObject(index, BigInteger.class) works correctly + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT int128_col, int256_col, uint64_col, uint128_col, uint256_col FROM " + tableName + " WHERE id = 2")) { + assertTrue(rs.next()); + + // Verify that getObject with specific class works + assertEquals(rs.getObject(1, BigInteger.class), int128Max, "getObject(index, BigInteger.class) for Int128"); + assertEquals(rs.getObject(2, BigInteger.class), int256Max, "getObject(index, BigInteger.class) for Int256"); + assertEquals(rs.getObject(3, BigInteger.class), uint64Max, "getObject(index, BigInteger.class) for UInt64"); + assertEquals(rs.getObject(4, BigInteger.class), uint128Max, "getObject(index, BigInteger.class) for UInt128"); + assertEquals(rs.getObject(5, BigInteger.class), uint256Max, "getObject(index, BigInteger.class) for UInt256"); + + assertFalse(rs.next()); + } + } + } + + log.info("BigInteger types mapping test completed successfully"); + } + + @Test(groups = { "integration" }) + public void testUnsignedIntegerTypes() throws Exception { + Random rand = new Random(); + runQuery("CREATE TABLE test_unsigned_integers (order Int8, " + + "uint8 Nullable(UInt8), " + + "uint16 Nullable(UInt16), " + + "uint32 Nullable(UInt32), " + + "uint64 Nullable(UInt64), " + + "uint128 Nullable(UInt128), " + + "uint256 Nullable(UInt256)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert null values + insertData("INSERT INTO test_unsigned_integers VALUES ( 1, " + + "NULL, NULL, NULL, NULL, NULL, NULL)"); + + // Insert minimum values + insertData("INSERT INTO test_unsigned_integers VALUES ( 2, " + + "0, 0, 0, 0, 0, 0)"); + + // Insert random values + int uint8 = rand.nextInt(256); + int uint16 = rand.nextInt(65536); + long uint32 = rand.nextLong() & 0xFFFFFFFFL; + long uint64 = rand.nextLong() & 0xFFFFFFFFFFFFL; + BigInteger uint128 = new BigInteger(38, rand); + BigInteger uint256 = new BigInteger(77, rand); + insertData("INSERT INTO test_unsigned_integers VALUES ( 3, " + + uint8 + ", " + uint16 + ", " + uint32 + ", " + uint64 + ", " + uint128 + ", " + uint256 + ")"); + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT uint8, uint16, uint32, uint64, uint128, uint256 FROM test_unsigned_integers ORDER BY order")) { + + List> expectedTypes = Arrays.asList( + Short.class, Integer.class, Long.class, BigInteger.class, BigInteger.class, BigInteger.class); + List> actualTypes = new ArrayList<>(); + ResultSetMetaData rsmd = rs.getMetaData(); + for (int i = 0; i < rsmd.getColumnCount(); i++) { + actualTypes.add(Class.forName(rsmd.getColumnClassName(i + 1))); + } + assertEquals(actualTypes, expectedTypes); + + + assertTrue(rs.next()); + assertEquals(rs.getObject("uint8"), null); + assertEquals(rs.getObject("uint16"), null); + assertEquals(rs.getObject("uint32"), null); + assertEquals(rs.getObject("uint64"), null); + assertEquals(rs.getObject("uint128"), null); + assertEquals(rs.getObject("uint256"), null); + + assertTrue(rs.next()); + assertEquals((Short) rs.getObject("uint8"), (byte) 0); + assertEquals((Integer) rs.getObject("uint16"), (short) 0); + assertEquals((Long) rs.getObject("uint32"), 0); + assertEquals(rs.getObject("uint64"), BigInteger.ZERO); + assertEquals(rs.getObject("uint128"), BigInteger.ZERO); + assertEquals(rs.getObject("uint256"), BigInteger.ZERO); + + assertTrue(rs.next()); + assertEquals(((Short) rs.getObject("uint8")).intValue(), uint8); + assertEquals((Integer) rs.getObject("uint16"), uint16); + assertEquals((Long) rs.getObject("uint32"), uint32); + assertEquals(rs.getObject("uint64"), BigInteger.valueOf(uint64)); + assertEquals(rs.getObject("uint128"), uint128); + assertEquals(rs.getObject("uint256"), uint256); + + assertFalse(rs.next()); + } + } + + + @Test(groups = { "integration" }) + public void testUUIDTypes() throws Exception { + Random rand = new Random(); + runQuery("CREATE TABLE test_uuids (order Int8, " + + "uuid Nullable(UUID) " + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert null values + insertData("INSERT INTO test_uuids VALUES ( 1, NULL)"); + + // Insert random values + UUID uuid = UUID.randomUUID(); + insertData("INSERT INTO test_uuids VALUES ( 2, " + + "'" + uuid + "')"); + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT uuid FROM test_uuids ORDER BY order")) { + + assertTrue(rs.next()); + assertNull(rs.getObject("uuid")); + + + assertTrue(rs.next()); + assertEquals(rs.getObject("uuid"), uuid); + + assertFalse(rs.next()); + } + } + + + @Test(groups = {"integration"}) + public void testArrayOfUUID() throws Exception { + try (Connection connection = getJdbcConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT '2d1f626d-eb07-4c81-be3d-ac1173f0d018'::UUID f_elem, ['2d1f626d-eb07-4c81-be3d-ac1173f0d018']::Array(UUID) arr")) { + + Assert.assertTrue(rs.next()); + UUID fElem = (UUID) rs.getObject(1); + Array colValue = rs.getArray(2); + Object[] arr = (Object[]) colValue.getArray(); + Assert.assertEquals(fElem, arr[0]); + + ResultSet arrRs = colValue.getResultSet(); + arrRs.next(); + Assert.assertEquals(fElem, arrRs.getObject(2)); + } + } + + @Test(groups = { "integration" }) + public void testDecimalTypes() throws SQLException { + runQuery("CREATE TABLE test_decimals (order Int8, " + + "dec Decimal(9, 2), dec32 Decimal32(4), dec64 Decimal64(8), dec128 Decimal128(18), dec256 Decimal256(18)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert minimum values + insertData("INSERT INTO test_decimals VALUES ( 1, -9999999.99, -99999.9999, -9999999999.99999999, -99999999999999999999.999999999999999999, " + + "-9999999999999999999999999999999999999999999999999999999999.999999999999999999)"); + + // Insert maximum values + insertData("INSERT INTO test_decimals VALUES ( 2, 9999999.99, 99999.9999, 9999999999.99999999, 99999999999999999999.999999999999999999, " + + "9999999999999999999999999999999999999999999999999999999999.999999999999999999)"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + BigDecimal dec = new BigDecimal(new BigInteger(7, rand) + "." + rand.nextInt(10,100));//P - S; 9 - 2 + BigDecimal dec32 = new BigDecimal(new BigInteger(5, rand) + "." + rand.nextInt(1000, 10000)); + BigDecimal dec64 = new BigDecimal(new BigInteger(18, rand) + "." + rand.nextInt(10000000, 100000000)); + BigDecimal dec128 = new BigDecimal(new BigInteger(20, rand) + "." + rand.nextLong(100000000000000000L, 1000000000000000000L)); + BigDecimal dec256 = new BigDecimal(new BigInteger(58, rand) + "." + rand.nextLong(100000000000000000L, 1000000000000000000L)); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_decimals VALUES ( 3, ?, ?, ?, ?, ?)")) { + stmt.setBigDecimal(1, dec); + stmt.setBigDecimal(2, dec32); + stmt.setBigDecimal(3, dec64); + stmt.setBigDecimal(4, dec128); + stmt.setBigDecimal(5, dec256); + stmt.executeUpdate(); + } + } + + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_decimals ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getBigDecimal("dec"), new BigDecimal("-9999999.99")); + assertEquals(rs.getBigDecimal("dec32"), new BigDecimal("-99999.9999")); + assertEquals(rs.getBigDecimal("dec64"), new BigDecimal("-9999999999.99999999")); + assertEquals(rs.getBigDecimal("dec128"), new BigDecimal("-99999999999999999999.999999999999999999")); + assertEquals(rs.getBigDecimal("dec256"), new BigDecimal("-9999999999999999999999999999999999999999999999999999999999.999999999999999999")); + + assertTrue(rs.next()); + assertEquals(rs.getBigDecimal("dec"), new BigDecimal("9999999.99")); + assertEquals(rs.getBigDecimal("dec32"), new BigDecimal("99999.9999")); + assertEquals(rs.getBigDecimal("dec64"), new BigDecimal("9999999999.99999999")); + assertEquals(rs.getBigDecimal("dec128"), new BigDecimal("99999999999999999999.999999999999999999")); + assertEquals(rs.getBigDecimal("dec256"), new BigDecimal("9999999999999999999999999999999999999999999999999999999999.999999999999999999")); + + assertTrue(rs.next()); + assertEquals(rs.getBigDecimal("dec"), dec); + assertEquals(rs.getBigDecimal("dec32"), dec32); + assertEquals(rs.getBigDecimal("dec64"), dec64); + assertEquals(rs.getBigDecimal("dec128"), dec128); + assertEquals(rs.getBigDecimal("dec256"), dec256); + + assertFalse(rs.next()); + } + } + } + + // Check the results with getObject + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_decimals ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getObject("dec"), new BigDecimal("-9999999.99")); + assertEquals(rs.getObject("dec32"), new BigDecimal("-99999.9999")); + assertEquals(rs.getObject("dec64"), new BigDecimal("-9999999999.99999999")); + assertEquals(rs.getObject("dec128"), new BigDecimal("-99999999999999999999.999999999999999999")); + assertEquals(rs.getObject("dec256"), new BigDecimal("-9999999999999999999999999999999999999999999999999999999999.999999999999999999")); + + assertTrue(rs.next()); + assertEquals(rs.getObject("dec"), new BigDecimal("9999999.99")); + assertEquals(rs.getObject("dec32"), new BigDecimal("99999.9999")); + assertEquals(rs.getObject("dec64"), new BigDecimal("9999999999.99999999")); + assertEquals(rs.getObject("dec128"), new BigDecimal("99999999999999999999.999999999999999999")); + assertEquals(rs.getObject("dec256"), new BigDecimal("9999999999999999999999999999999999999999999999999999999999.999999999999999999")); + + assertTrue(rs.next()); + assertEquals(rs.getObject("dec"), dec); + assertEquals(rs.getObject("dec32"), dec32); + assertEquals(rs.getObject("dec64"), dec64); + assertEquals(rs.getObject("dec128"), dec128); + assertEquals(rs.getObject("dec256"), dec256); + + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testDateTimeTypes() throws SQLException { + runQuery("CREATE TABLE test_datetimes (order Int8, " + + "dateTime DateTime, dateTime32 DateTime32, " + + "dateTime643 DateTime64(3), dateTime646 DateTime64(6), dateTime649 DateTime64(9)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert minimum values + insertData("INSERT INTO test_datetimes VALUES ( 1, " + + "'1970-01-01 00:00:00', '1970-01-01 00:00:00', " + + "'1970-01-01 00:00:00.000', '1970-01-01 00:00:00.000000', '1970-01-01 00:00:00.000000000' )"); + + // Insert maximum values + insertData("INSERT INTO test_datetimes VALUES ( 2," + + "'2106-02-07 06:28:15', '2106-02-07 06:28:15', " + + "'2261-12-31 23:59:59.999', '2261-12-31 23:59:59.999999', '2261-12-31 23:59:59.999999999' )"); + + // Insert random (valid) values + final ZoneId zoneId = ZoneId.of("America/Los_Angeles"); + final LocalDateTime now = LocalDateTime.now(zoneId); + final java.sql.Timestamp dateTime = Timestamp.valueOf(now); + dateTime.setNanos(0); + final java.sql.Timestamp dateTime32 = Timestamp.valueOf(now); + dateTime32.setNanos(0); + final java.sql.Timestamp dateTime643 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + dateTime643.setNanos(333000000); + final java.sql.Timestamp dateTime646 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + dateTime646.setNanos(333333000); + final java.sql.Timestamp dateTime649 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + dateTime649.setNanos(333333333); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_datetimes VALUES ( 4, ?, ?, ?, ?, ?)")) { + stmt.setTimestamp(1, dateTime); + stmt.setTimestamp(2, dateTime32); + stmt.setTimestamp(3, dateTime643); + stmt.setTimestamp(4, dateTime646); + stmt.setTimestamp(5, dateTime649); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_datetimes ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getTimestamp("dateTime").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime32").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime643").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime646").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime649").toString(), "1970-01-01 00:00:00.0"); + + assertTrue(rs.next()); + assertEquals(rs.getTimestamp("dateTime").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getTimestamp("dateTime32").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getTimestamp("dateTime643").toString(), "2261-12-31 23:59:59.999"); + assertEquals(rs.getTimestamp("dateTime646").toString(), "2261-12-31 23:59:59.999999"); + assertEquals(rs.getTimestamp("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); + + assertTrue(rs.next()); + assertEquals(rs.getTimestamp("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime643").toString(), Timestamp.valueOf(dateTime643.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime646").toString(), Timestamp.valueOf(dateTime646.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime649").toString(), Timestamp.valueOf(dateTime649.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + + assertEquals(rs.getTimestamp("dateTime", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime.toString()); + assertEquals(rs.getTimestamp("dateTime32", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime32.toString()); + assertEquals(rs.getTimestamp("dateTime643", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime643.toString()); + assertEquals(rs.getTimestamp("dateTime646", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime646.toString()); + assertEquals(rs.getTimestamp("dateTime649", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime649.toString()); + + assertFalse(rs.next()); + } + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_datetimes ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getObject("dateTime").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime32").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime643").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime646").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime649").toString(), "1970-01-01 00:00:00.0"); + + assertTrue(rs.next()); + + assertEquals(rs.getObject("dateTime").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getObject("dateTime32").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getObject("dateTime643").toString(), "2261-12-31 23:59:59.999"); + assertEquals(rs.getObject("dateTime646").toString(), "2261-12-31 23:59:59.999999"); + assertEquals(rs.getObject("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); + + assertTrue(rs.next()); + + assertEquals(rs.getObject("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime643").toString(), Timestamp.valueOf(dateTime643.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime646").toString(), Timestamp.valueOf(dateTime646.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime649").toString(), Timestamp.valueOf(dateTime649.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + + assertFalse(rs.next()); + } + } + } + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM test_datetimes ORDER BY order")) + { + assertTrue(rs.next()); + + assertEquals(rs.getString("dateTime"), "1970-01-01 00:00:00"); + assertEquals(rs.getString("dateTime32"), "1970-01-01 00:00:00"); + assertEquals(rs.getString("dateTime643"), "1970-01-01 00:00:00"); + assertEquals(rs.getString("dateTime646"), "1970-01-01 00:00:00"); + assertEquals(rs.getString("dateTime649"), "1970-01-01 00:00:00"); + + assertTrue(rs.next()); + assertEquals(rs.getString("dateTime"), "2106-02-07 06:28:15"); + assertEquals(rs.getString("dateTime32"), "2106-02-07 06:28:15"); + assertEquals(rs.getString("dateTime643"), "2261-12-31 23:59:59.999"); + assertEquals(rs.getString("dateTime646"), "2261-12-31 23:59:59.999999"); + assertEquals(rs.getString("dateTime649"), "2261-12-31 23:59:59.999999999"); + + ZoneId tzServer = ZoneId.of(((ConnectionImpl) conn).getClient().getServerTimeZone()); + assertTrue(rs.next()); + assertEquals( + rs.getString("dateTime"), + DataTypeUtils.DATETIME_FORMATTER.format( + Instant.ofEpochMilli(dateTime.getTime()).atZone(tzServer))); + assertEquals( + rs.getString("dateTime32"), + DataTypeUtils.DATETIME_FORMATTER.format( + Instant.ofEpochMilli(dateTime32.getTime()).atZone(tzServer))); + assertEquals( + rs.getString("dateTime643"), + DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER.format(dateTime643.toInstant().atZone(tzServer))); + assertEquals( + rs.getString("dateTime646"), + DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER.format(dateTime646.toInstant().atZone(tzServer))); + assertEquals( + rs.getString("dateTime649"), + DataTypeUtils.DATETIME_WITH_NANOS_FORMATTER.format(dateTime649.toInstant().atZone(tzServer))); + + assertFalse(rs.next()); + } + } + + @Test(groups = { "integration" }) + public void testDateTypes() throws SQLException { + runQuery("CREATE TABLE test_dates (order Int8, " + + "date Date, date32 Date32" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert minimum values + insertData("INSERT INTO test_dates VALUES ( 1, '1970-01-01', '1970-01-01')"); + + // Insert maximum values + insertData("INSERT INTO test_dates VALUES ( 2, '2149-06-06', '2299-12-31')"); + + // Insert random (valid) values + final ZoneId zoneId = ZoneId.of("America/Los_Angeles"); + final LocalDateTime now = LocalDateTime.now(zoneId); + final Date date = Date.valueOf(now.toLocalDate()); + final Date date32 = Date.valueOf(now.toLocalDate()); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_dates VALUES ( 3, ?, ?)")) { + stmt.setDate(1, date); + stmt.setDate(2, date32); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getDate("date"), Date.valueOf("1970-01-01")); + assertEquals(rs.getDate("date32"), Date.valueOf("1970-01-01")); + + assertTrue(rs.next()); + assertEquals(rs.getDate("date"), Date.valueOf("2149-06-06")); + assertEquals(rs.getDate("date32"), Date.valueOf("2299-12-31")); + + assertTrue(rs.next()); + assertEquals(rs.getDate("date").toString(), date.toString()); + assertEquals(rs.getDate("date32").toString(), date32.toString()); + + assertFalse(rs.next()); + } + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getObject("date"), Date.valueOf("1970-01-01")); + assertEquals(rs.getObject("date32"), Date.valueOf("1970-01-01")); + + assertTrue(rs.next()); + assertEquals(rs.getObject("date"), Date.valueOf("2149-06-06")); + assertEquals(rs.getObject("date32"), Date.valueOf("2299-12-31")); + + assertTrue(rs.next()); + assertEquals(rs.getObject("date").toString(), date.toString()); + assertEquals(rs.getObject("date32").toString(), date32.toString()); + + assertFalse(rs.next()); + } + } + } + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM test_dates ORDER BY order")) + { + assertTrue(rs.next()); + assertEquals(rs.getString("date"), "1970-01-01"); + assertEquals(rs.getString("date32"), "1970-01-01"); + + assertTrue(rs.next()); + assertEquals(rs.getString("date"), "2149-06-06"); + assertEquals(rs.getString("date32"), "2299-12-31"); + + ZoneId tzServer = ZoneId.of(((ConnectionImpl) conn).getClient().getServerTimeZone()); + assertTrue(rs.next()); + assertEquals( + rs.getString("date"), + Instant.ofEpochMilli(date.getTime()).atZone(tzServer).toLocalDate().toString()); + assertEquals( + rs.getString("date32"), + Instant.ofEpochMilli(date32.getTime()).atZone(tzServer).toLocalDate().toString()); + + assertFalse(rs.next()); + } + } + + + @Test(groups = { "integration" }) + public void testTimeTypes() throws SQLException { + if (ClickHouseVersion.of(getServerVersion()).check("(,25.5]")) { + return; // Time64 introduced in 25.6 + } + Properties createProperties = new Properties(); + createProperties.put(ClientConfigProperties.serverSetting("allow_experimental_time_time64_type"), "1"); + runQuery("CREATE TABLE test_time64 (order Int8, " + + "time Time, time64 Time64(9) " + + ") ENGINE = MergeTree ORDER BY ()", + createProperties); + + runQuery("INSERT INTO test_time64 (order, time, time64) VALUES " + + " (1, '-999:59:59', '-999:59:59.999999999'), " + + " (2, '999:59:59', '999:59:59.999999999')"); + + // Check the results + try (Statement stmt = getJdbcConnection().createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_time64")) { + assertTrue(rs.next()); + assertEquals(rs.getInt("order"), 1); + + // Negative values + // Negative value cannot be returned as Time without being truncated + assertTrue(rs.getTime("time").getTime() < 0); + assertTrue(rs.getTime("time64").getTime() < 0); + LocalDateTime negativeTime = rs.getObject("time", LocalDateTime.class); + assertEquals(negativeTime.toEpochSecond(ZoneOffset.UTC), -(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); + LocalDateTime negativeTime64 = rs.getObject("time64", LocalDateTime.class); + assertEquals(negativeTime64.toEpochSecond(ZoneOffset.UTC), -(TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59), "value " + negativeTime64); + assertEquals(negativeTime64.getNano(), 999_999_999); // nanoseconds are stored separately and only positive values accepted + + // Positive values + assertTrue(rs.next()); + assertEquals(rs.getInt("order"), 2); + LocalDateTime positiveTime = rs.getObject("time", LocalDateTime.class); + assertEquals(positiveTime.toEpochSecond(ZoneOffset.UTC), (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); + LocalDateTime positiveTime64 = rs.getObject("time64", LocalDateTime.class); + assertEquals(positiveTime64.toEpochSecond(ZoneOffset.UTC), (TimeUnit.HOURS.toSeconds(999) + TimeUnit.MINUTES.toSeconds(59) + 59)); + assertEquals(positiveTime64.getNano(), 999_999_999); + + // Time is stored as UTC (server timezone) + assertEquals(rs.getTime("time", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime(), + (TimeUnit.HOURS.toMillis(999) + TimeUnit.MINUTES.toMillis(59) + TimeUnit.SECONDS.toMillis(59))); + + // java.sql.Time max resolution is milliseconds + assertEquals(rs.getTime("time64", Calendar.getInstance(TimeZone.getTimeZone("UTC"))).getTime(), + (TimeUnit.HOURS.toMillis(999) + TimeUnit.MINUTES.toMillis(59) + TimeUnit.SECONDS.toMillis(59) + 999)); + + assertEquals(rs.getTime("time"), rs.getObject("time", Time.class)); + assertEquals(rs.getTime("time64"), rs.getObject("time64", Time.class)); + + // time has no date part and cannot be converted to Date or Timestamp + for (String col : Arrays.asList("time", "time64")) { + assertThrows(SQLException.class, () -> rs.getDate(col)); + assertThrows(SQLException.class, () -> rs.getTimestamp(col)); + assertThrows(SQLException.class, () -> rs.getObject(col, Timestamp.class)); + assertThrows(SQLException.class, () -> rs.getObject(col, Date.class)); + } + assertFalse(rs.next()); + } + } + } + + @Test(groups = { "integration" }) + public void testStringTypes() throws SQLException { + runQuery("CREATE TABLE test_strings (order Int8, " + + "str String, fixed FixedString(6), " + + "enum Enum8('a' = 6, 'b' = 7, 'c' = 8), enum8 Enum8('a' = 1, 'b' = 2, 'c' = 3), enum16 Enum16('a' = 1, 'b' = 2, 'c' = 3), " + + "uuid UUID, escaped String " + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + + String str = "string" + rand.nextInt(1000); + String fixed = "fixed" + rand.nextInt(10); + String enum8 = "a"; + String enum16 = "b"; + String uuid = UUID.randomUUID().toString(); + String escaped = "\\xA3\\xA3\\x12\\xA0\\xDF\\x13\\x4E\\x8C\\x87\\x74\\xD4\\x53\\xDB\\xFC\\x34\\x95"; + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_strings VALUES ( 1, ?, ?, ?, ?, ?, ?, ? )")) { + stmt.setString(1, str); + stmt.setString(2, fixed); + stmt.setString(3, enum8); + stmt.setString(4, enum8); + stmt.setString(5, enum16); + stmt.setString(6, uuid); + stmt.setString(7, escaped); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_strings ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getString("str"), str); + assertEquals(rs.getString("fixed"), fixed); + assertEquals(rs.getString("enum"), "a"); + assertEquals(rs.getInt("enum"), 6); + assertEquals(rs.getString("enum8"), "a"); + assertEquals(rs.getInt("enum8"), 1); + assertEquals(rs.getString("enum16"), "b"); + assertEquals(rs.getInt("enum16"), 2); + assertEquals(rs.getString("uuid"), uuid); + assertEquals(rs.getString("escaped"), escaped); + assertFalse(rs.next()); + } + } + } + + // Check the results with getObject + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_strings ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getObject("str"), str); + assertEquals(rs.getObject("fixed"), fixed); + assertEquals(rs.getObject("enum"), "a"); + assertEquals(rs.getObject("enum8"), "a"); + assertEquals(rs.getObject("enum16"), "b"); + assertEquals(rs.getObject("uuid"), UUID.fromString(uuid)); + assertEquals(rs.getObject("escaped"), escaped); + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testIpAddressTypes() throws SQLException, UnknownHostException { + runQuery("CREATE TABLE test_ips (order Int8, " + + "ipv4_ip IPv4, ipv4_name IPv4, ipv6 IPv6, ipv4_as_ipv6 IPv6" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + + InetAddress ipv4AddressByIp = Inet4Address.getByName("90.176.75.97"); + InetAddress ipv4AddressByName = Inet4Address.getByName("www.example.com"); + InetAddress ipv6Address = Inet6Address.getByName("2001:adb8:85a3:1:2:8a2e:370:7334"); + InetAddress ipv4AsIpv6 = Inet4Address.getByName("90.176.75.97"); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_ips VALUES ( 1, ?, ?, ?, ? )")) { + stmt.setObject(1, ipv4AddressByIp); + stmt.setObject(2, ipv4AddressByName); + stmt.setObject(3, ipv6Address); + stmt.setObject(4, ipv4AsIpv6); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_ips ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getObject("ipv4_ip"), ipv4AddressByIp); + assertEquals(rs.getObject("ipv4_ip", Inet6Address.class).getHostAddress(), "0:0:0:0:0:ffff:5ab0:4b61"); + assertEquals(rs.getString("ipv4_ip"), ipv4AddressByIp.getHostAddress()); + assertEquals(rs.getObject("ipv4_name"), ipv4AddressByName); + assertEquals(rs.getObject("ipv6"), ipv6Address); + assertEquals(rs.getString("ipv6"), ipv6Address.getHostAddress()); + assertEquals(rs.getObject("ipv4_as_ipv6"), ipv4AsIpv6); + assertEquals(rs.getObject("ipv4_as_ipv6", Inet4Address.class), ipv4AsIpv6); + assertEquals(rs.getBytes("ipv4_ip"), ipv4AddressByIp.getAddress()); + assertEquals(rs.getBytes("ipv6"), ipv6Address.getAddress()); + + assertFalse(rs.next()); + } + } + } + } + + + @Test(groups = {"integration"}) + public void testArrayOfIpAddress() throws Exception { + try (Connection connection = getJdbcConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT ['90.176.75.97'::IPv4] addrs1, ['2001:adb8:85a3:1:2:8a2e:370:7334'::IPv6] addrs2, ['2001:adb8:85a3:1:2:8a2e:370:7334'::IPv6, null] addrs3")) { + + InetAddress ipv4AddressByIp = Inet4Address.getByName("90.176.75.97"); + InetAddress ipv6Address = Inet6Address.getByName("2001:adb8:85a3:1:2:8a2e:370:7334"); + + Assert.assertTrue(rs.next()); + { + // IPv4 + Array addrs1 = rs.getArray(1); + Object[] arr = (Object[]) addrs1.getArray(); + Assert.assertEquals(ipv4AddressByIp, arr[0]); + + ResultSet arrRs = addrs1.getResultSet(); + arrRs.next(); + Assert.assertEquals(ipv4AddressByIp, arrRs.getObject(2)); + } + + { + // IPv6 + Array addrs2 = rs.getArray(2); + Object[] arr = (Object[]) addrs2.getArray(); + Assert.assertEquals(ipv6Address, arr[0]); + + ResultSet arrRs = addrs2.getResultSet(); + arrRs.next(); + Assert.assertEquals(ipv6Address, arrRs.getObject(2)); + } + + { + // IPv6 + Array addrs3 = rs.getArray(3); + Assert.assertEquals(addrs3.getBaseTypeName(), "Nullable(IPv6)"); + Object[] arr = (Object[]) addrs3.getArray(); + Assert.assertEquals(ipv6Address, arr[0]); + + ResultSet arrRs = addrs3.getResultSet(); + arrRs.next(); + Assert.assertEquals(ipv6Address, arrRs.getObject(2)); + arrRs.next(); + Assert.assertNull(arrRs.getObject(2)); + } + } + } + + + @Test(groups = { "integration" }) + public void testFloatTypes() throws SQLException { + runQuery("CREATE TABLE test_floats (order Int8, " + + "float32 Float32, float64 Float64" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert minimum values + insertData("INSERT INTO test_floats VALUES ( 1, -3.4028233E38, -1.7976931348623157E308 )"); + + // Insert maximum values + insertData("INSERT INTO test_floats VALUES ( 2, 3.4028233E38, 1.7976931348623157E308 )"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + Float float32 = rand.nextFloat(); + Double float64 = rand.nextDouble(); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_floats VALUES ( 3, ?, ? )")) { + stmt.setFloat(1, float32); + stmt.setDouble(2, float64); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_floats ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getFloat("float32"), -3.402823E38f); + assertEquals(rs.getDouble("float64"), Double.valueOf(-1.7976931348623157E308)); + + assertTrue(rs.next()); + assertEquals(rs.getFloat("float32"), Float.valueOf(3.402823E38f)); + assertEquals(rs.getDouble("float64"), Double.valueOf(1.7976931348623157E308)); + + assertTrue(rs.next()); + assertEquals(rs.getFloat("float32"), float32); + assertEquals(rs.getDouble("float64"), float64); + + assertFalse(rs.next()); + } + } + } + + // Check the results with getObject + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_floats ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getObject("float32"), -3.402823E38f); + assertEquals(rs.getObject("float64"), Double.valueOf(-1.7976931348623157E308)); + + assertTrue(rs.next()); + assertEquals(rs.getObject("float32"), 3.402823E38f); + assertEquals(rs.getObject("float64"), Double.valueOf(1.7976931348623157E308)); + + assertTrue(rs.next()); + + DecimalFormat df = new DecimalFormat("#.######"); + assertEquals(df.format(rs.getObject("float32")), df.format(float32)); + assertEquals(rs.getObject("float64"), float64); + + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testBooleanTypes() throws SQLException { + runQuery("CREATE TABLE test_booleans (order Int8, " + + "bool Boolean" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + boolean bool = rand.nextBoolean(); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_booleans VALUES ( 1, ? )")) { + stmt.setBoolean(1, bool); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_booleans ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getBoolean("bool"), bool); + + assertFalse(rs.next()); + } + } + } + + // Check the results with getObject + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_booleans ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getObject("bool"), bool); + + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testArrayTypes() throws SQLException { + runQuery("CREATE TABLE test_arrays (order Int8, " + + "array Array(Int8), arraystr Array(String), " + + "arraytuple Array(Tuple(Int8, String)), " + + "arraydate Array(Date)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + Integer[] array = new Integer[10]; + for (int i = 0; i < array.length; i++) { + array[i] = rand.nextInt(256) - 128; + } + + String[] arraystr = new String[10]; + for (int i = 0; i < arraystr.length; i++) { + arraystr[i] = "string" + rand.nextInt(1000); + } + + Tuple[] arraytuple = new Tuple[10]; + for (int i = 0; i < arraytuple.length; i++) { + arraytuple[i] = new Tuple(rand.nextInt(256) - 128, "string" + rand.nextInt(1000)); + } + + Date[] arraydate = new Date[10]; + for (int i = 0; i < arraydate.length; i++) { + arraydate[i] = Date.valueOf(LocalDate.now().plusDays(rand.nextInt(100))); + } + + // Insert using `Connection#createArrayOf` + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_arrays VALUES ( 1, ?, ?, ?, ?)")) { + stmt.setArray(1, conn.createArrayOf("Int8", array)); + stmt.setArray(2, conn.createArrayOf("String", arraystr)); + stmt.setArray(3, conn.createArrayOf("Tuple(Int8, String)", arraytuple)); + stmt.setArray(3, conn.createArrayOf("Tuple(Int8, String)", arraytuple)); + stmt.setArray(4, conn.createArrayOf("Date", arraydate)); + stmt.executeUpdate(); + } + } + + // Insert using common java objects + final String INSERT_SQL = "INSERT INTO test_arrays VALUES ( 2, ?, ?, ?, ?)"; + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement(INSERT_SQL)) { + stmt.setObject(1, array); + stmt.setObject(2, arraystr); + stmt.setObject(3, arraytuple); + stmt.setObject(4, arraydate); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_arrays ORDER BY order")) { + assertTrue(rs.next()); + { + Object[] arrayResult = (Object[]) rs.getArray("array").getArray(); + assertEquals(arrayResult.length, array.length); + for (int i = 0; i < array.length; i++) { + assertEquals(String.valueOf(arrayResult[i]), String.valueOf(array[i])); + } + + Object[] arraystrResult = (Object[]) rs.getArray("arraystr").getArray(); + assertEquals(arraystrResult.length, arraystr.length); + for (int i = 0; i < arraystr.length; i++) { + assertEquals(arraystrResult[i], arraystr[i]); + } + Object[] arraytupleResult = (Object[]) rs.getArray("arraytuple").getArray(); + assertEquals(arraytupleResult.length, arraytuple.length); + for (int i = 0; i < arraytuple.length; i++) { + Tuple tuple = arraytuple[i]; + Tuple tupleResult = new Tuple(((Object[]) arraytupleResult[i])); + assertEquals(String.valueOf(tupleResult.getValue(0)), String.valueOf(tuple.getValue(0))); + assertEquals(String.valueOf(tupleResult.getValue(1)), String.valueOf(tuple.getValue(1))); + } + + Object[] arraydateResult = (Object[]) rs.getArray("arraydate").getArray(); + assertEquals(arraydateResult.length, arraydate.length); + for (int i = 0; i < arraydate.length; i++) { + assertEquals(String.valueOf(arraydateResult[i]), String.valueOf(arraydate[i])); + } + } + assertTrue(rs.next()); + { + Object[] arrayResult = (Object[]) ((Array) rs.getObject("array")).getArray(); + assertEquals(arrayResult.length, array.length); + for (int i = 0; i < array.length; i++) { + assertEquals(String.valueOf(arrayResult[i]), String.valueOf(array[i])); + } + + Object[] arraystrResult = (Object[]) ((Array) rs.getObject("arraystr")).getArray(); + assertEquals(arraystrResult.length, arraystr.length); + for (int i = 0; i < arraystr.length; i++) { + assertEquals(arraystrResult[i], arraystr[i]); + } + Object[] arraytupleResult = (Object[]) ((Array) rs.getObject("arraytuple")).getArray(); + assertEquals(arraytupleResult.length, arraytuple.length); + for (int i = 0; i < arraytuple.length; i++) { + Tuple tuple = arraytuple[i]; + Tuple tupleResult = new Tuple(((Object[]) arraytupleResult[i])); + assertEquals(String.valueOf(tupleResult.getValue(0)), String.valueOf(tuple.getValue(0))); + assertEquals(String.valueOf(tupleResult.getValue(1)), String.valueOf(tuple.getValue(1))); + } + + Object[] arraydateResult = (Object[]) ((Array) rs.getObject("arraydate")).getArray(); + assertEquals(arraydateResult.length, arraydate.length); + for (int i = 0; i < arraydate.length; i++) { + assertEquals(arraydateResult[i], arraydate[i]); + } + } + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testStringsUsedAsBytes() throws Exception { + runQuery("CREATE TABLE test_strings_as_bytes (order Int8, str String, fixed FixedString(10)) ENGINE = MergeTree ORDER BY ()"); + + String[][] testData = {{"Hello, World!", "FixedStr"}, {"Test String 123", "ABC"}}; + + try (Connection conn = getJdbcConnection(); + PreparedStatement insert = conn.prepareStatement("INSERT INTO test_strings_as_bytes VALUES (?, ?, ?)")) { + for (int i = 0; i < testData.length; i++) { + insert.setInt(1, i + 1); + insert.setBytes(2, testData[i][0].getBytes("UTF-8")); + insert.setBytes(3, testData[i][1].getBytes("UTF-8")); + insert.executeUpdate(); + } + } + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM test_strings_as_bytes ORDER BY order")) { + for (String[] expected : testData) { + assertTrue(rs.next()); + assertEquals(new String(rs.getBytes("str"), "UTF-8"), expected[0]); + assertEquals(new String(rs.getBytes("fixed"), "UTF-8").replace("\0", ""), expected[1]); + } + assertFalse(rs.next()); + } + } + + @Test(groups = { "integration" }) + public void testNestedArrays() throws Exception { + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Array(Array(Int32)) as value")) { + Integer[][] srcArray = new Integer[][] { + {1, 2, 3}, + {4, 5, 6} + }; + Array array = conn.createArrayOf("Int32", srcArray); + stmt.setArray(1, array); + + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + Array arrayHolder = (Array) rs.getObject(1); + Object[] dbArray = (Object[]) arrayHolder.getArray(); + for (int i = 0; i < dbArray.length; i++) { + Object[] nestedArray = (Object[]) dbArray[i]; + for (int j = 0; j < nestedArray.length; j++) { + assertEquals((Integer) nestedArray[j], (Integer)srcArray[i][j]); + } + } + } + + Integer[] simpleArray = new Integer[] {1, 2, 3}; + Array array1 = conn.createArrayOf("Int32", simpleArray); + Array array2 = conn.createArrayOf("Int32", simpleArray); + + Array[] multiLevelArray = new Array[] {array1, array2}; + Array array3 = conn.createArrayOf("Int32", multiLevelArray); + stmt.setArray(1, array3); + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + Array arrayHolder = (Array) rs.getObject(1); + Object[] dbArray = (Object[]) arrayHolder.getArray(); + for (int i = 0; i < dbArray.length; i++) { + Object[] nestedArray = (Object[]) dbArray[i]; + for (int j = 0; j < nestedArray.length; j++) { + assertEquals((Integer) nestedArray[j], (Integer)simpleArray[j]); + } + } + } + } + } + } + + @Test(groups = { "integration" }) + public void testNestedArrayToString() throws SQLException { + // Test 1: Simple nested array - getString on Array(Array(Int32)) + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT [[1, 2, 3], [4, 5, 6]] as nested_array")) { + assertTrue(rs.next()); + // This was throwing NullPointerException before the fix + String result = rs.getString("nested_array"); + assertEquals(result, "[[1, 2, 3], [4, 5, 6]]"); + } + } + } + + // Test 2: Query similar to issue #2723 with splitByChar returning array + // The original issue was that getString() on an array column inside a CASE/WHEN + // would cause NPE. This test verifies that getString() works correctly on arrays. + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + String query = "SELECT " + + "splitByChar('_', 'field1_field2_field3') as split_result, " + + "CASE " + + " WHEN " + + " splitByChar('_', 'field1_field2_field3')[1] IN ('field1', 'field2') " + + " AND match( " + + " splitByChar('_', 'field1_field2_field3')[2], " + + " '(field1|field2|field3)' " + + " ) " + + " THEN 'Matched' " + + " ELSE 'NotMatched' " + + "END AS action_to_do"; + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); + // The key test is that getString() doesn't throw NPE on array column + String splitResult = rs.getString("split_result"); + assertEquals(splitResult, "['field1', 'field2', 'field3']"); + String actionResult = rs.getString("action_to_do"); + assertEquals(actionResult, "Matched"); + } + } + } + + // Test 3: Deeply nested arrays - Array(Array(Array(String))) + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT [[['a', 'b'], ['c']], [['d', 'e', 'f']]] as deep_nested")) { + 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"}}})); + } + } + } + } + + @Test(groups = { "integration" }) + public void testMapTypes() throws SQLException { + runQuery("CREATE TABLE test_maps (order Int8, " + + "map Map(String, Int8), mapstr Map(String, String)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + int mapSize = rand.nextInt(100); + Map integerMap = new java.util.HashMap<>(mapSize); + for (int i = 0; i < mapSize; i++) { + integerMap.put("key" + i, rand.nextInt(256) - 128); + } + + Map stringMap = new java.util.HashMap<>(mapSize); + for (int i = 0; i < mapSize; i++) { + stringMap.put("key" + i, "string" + rand.nextInt(1000)); + } + + // Insert random (valid) values + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_maps VALUES ( 1, ?, ? )")) { + stmt.setObject(1, integerMap); + stmt.setObject(2, stringMap); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_maps ORDER BY order")) { + assertTrue(rs.next()); + Map mapResult = (Map) rs.getObject("map"); + assertEquals(mapResult.size(), mapSize); + for (String key: integerMap.keySet()) { + assertEquals(String.valueOf(mapResult.get(key)), String.valueOf(integerMap.get(key))); + } + + Map mapstrResult = (Map) rs.getObject("mapstr"); + assertEquals(mapstrResult.size(), mapSize); + for (String key: stringMap.keySet()) { + assertEquals(String.valueOf(mapstrResult.get(key)), String.valueOf(stringMap.get(key))); + } + } + } + } + } + + + @Test(groups = { "integration" }) + public void testMapTypesWithArrayValues() throws SQLException { + runQuery("DROP TABLE test_maps;"); + runQuery("CREATE TABLE test_maps (order Int8, " + + "map Map(String, Array(Int32)), " + + "map2 Map(String, Array(Int32))" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + int mapSize = 3; + Map integerMap = new java.util.HashMap<>(mapSize); + Map integerMap2 = new java.util.HashMap<>(mapSize); + for (int i = 0; i < mapSize; i++) { + int[] array = new int[10]; + Integer[] array2 = new Integer[10]; + for (int j = 0; j < array.length; j++) { + array[j] = array2[j] = rand.nextInt(1000); + + } + integerMap.put("key" + i, array); + integerMap2.put("key" + i, array2); + } + + // Insert random (valid) values + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test_maps VALUES ( 1, ?, ?)")) { + stmt.setObject(1, integerMap); + stmt.setObject(2, integerMap2); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_maps ORDER BY order")) { + assertTrue(rs.next()); + Map mapResult = (Map) rs.getObject("map"); + assertEquals(mapResult.size(), mapSize); + for (String key: integerMap.keySet()) { + Object[] arrayResult = ((List) mapResult.get(key)).toArray(); + int[] array = integerMap.get(key); + assertEquals(arrayResult.length, array.length); + for (int i = 0; i < array.length; i++) { + assertEquals(String.valueOf(arrayResult[i]), String.valueOf(array[i])); + } + } + } + } + } + } + + @Test(groups = {"integration"}) + public void testArrayOfMaps() throws Exception { + try (Connection connection = getJdbcConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT [map('a', 1, 'b', 2)::Map(String, Int32)] arr1")) { + + Assert.assertTrue(rs.next()); + { + // Array(Map(String, Int32)) + Array arrMap1 = rs.getArray(1); + Assert.assertEquals(arrMap1.getBaseTypeName(), "Map(String, Int32)"); + Object[] arr = (Object[]) arrMap1.getArray(); + @SuppressWarnings("unchecked") + Map map1 = (Map) arr[0]; + Assert.assertEquals(map1.get("a"), Integer.valueOf(1)); + Assert.assertEquals(map1.get("b"), Integer.valueOf(2)); + + ResultSet arrRs = arrMap1.getResultSet(); + arrRs.next(); + @SuppressWarnings("unchecked") + Map rsMap1 = (Map) arrRs.getObject(2); + Assert.assertEquals(rsMap1.get("a"), Integer.valueOf(1)); + Assert.assertEquals(rsMap1.get("b"), Integer.valueOf(2)); + } + } + } + + /** + * Verifies that Array(Map(LowCardinality(String), String)) with empty maps decodes correctly. + * Regression test for #2657 + */ + @Test(groups = {"integration"}) + public void testArrayOfMapsWithLowCardinalityAndEmptyMaps() throws Exception { + runQuery("CREATE TABLE test_array_map_lc_empty (" + + "StartedDateTime DateTime, " + + "traits Array(Map(LowCardinality(String), String))" + + ") ENGINE = MergeTree ORDER BY StartedDateTime"); + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("INSERT INTO test_array_map_lc_empty (StartedDateTime, traits) VALUES (" + + "'2025-11-11 00:00:01', " + + "[" + + " map(), " + + " map(" + + " 'RandomKey1','Value1'," + + " 'RandomKey2','Value2'," + + " 'RandomKey3','Value3'," + + " 'RandomKey4','Value4'," + + " 'RandomKey5','Value5'," + + " 'RandomKey6','Value6'," + + " 'RandomKey7','Value7'," + + " 'RandomKey8','Value8'" + + " ), " + + " map(), map(), map(), map(), map(), map()" + + "]" + + ")"); + + Map expectedNonEmptyMap = new HashMap<>(); + expectedNonEmptyMap.put("RandomKey1", "Value1"); + expectedNonEmptyMap.put("RandomKey2", "Value2"); + expectedNonEmptyMap.put("RandomKey3", "Value3"); + expectedNonEmptyMap.put("RandomKey4", "Value4"); + expectedNonEmptyMap.put("RandomKey5", "Value5"); + expectedNonEmptyMap.put("RandomKey6", "Value6"); + expectedNonEmptyMap.put("RandomKey7", "Value7"); + expectedNonEmptyMap.put("RandomKey8", "Value8"); + + // Run multiple iterations because the bug is intermittent + for (int attempt = 0; attempt < 10; attempt++) { + try (ResultSet rs = stmt.executeQuery("SELECT traits FROM test_array_map_lc_empty")) { + Assert.assertTrue(rs.next(), "Expected a row on attempt " + attempt); + + Array traitsArray = rs.getArray(1); + Assert.assertEquals(traitsArray.getBaseTypeName(), "Map(LowCardinality(String), String)"); + + Object[] maps = (Object[]) traitsArray.getArray(); + Assert.assertEquals(maps.length, 8, "Expected 8 maps in array on attempt " + attempt); + + @SuppressWarnings("unchecked") + Map firstMap = (Map) maps[0]; + Assert.assertTrue(firstMap.isEmpty(), "First map should be empty on attempt " + attempt); + + @SuppressWarnings("unchecked") + Map secondMap = (Map) maps[1]; + Assert.assertEquals(secondMap, expectedNonEmptyMap, "Second map mismatch on attempt " + attempt); + + for (int i = 2; i < 8; i++) { + @SuppressWarnings("unchecked") + Map emptyMap = (Map) maps[i]; + Assert.assertTrue(emptyMap.isEmpty(), + "Map at index " + i + " should be empty on attempt " + attempt); + } + + Assert.assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testNullableTypesSimpleStatement() throws SQLException { + runQuery("CREATE TABLE test_nullable (order Int8, " + + "int8 Nullable(Int8), int16 Nullable(Int16), int32 Nullable(Int32), int64 Nullable(Int64), int128 Nullable(Int128), int256 Nullable(Int256), " + + "uint8 Nullable(UInt8), uint16 Nullable(UInt16), uint32 Nullable(UInt32), uint64 Nullable(UInt64), uint128 Nullable(UInt128), uint256 Nullable(UInt256), " + + "dec Nullable(Decimal(9, 2)), dec32 Nullable(Decimal32(4)), dec64 Nullable(Decimal64(8)), dec128 Nullable(Decimal128(18)), dec256 Nullable(Decimal256(18)), " + + "date Nullable(Date), date32 Nullable(Date32), " + + "dateTime Nullable(DateTime), dateTime32 Nullable(DateTime32), " + + "dateTime643 Nullable(DateTime64(3)), dateTime646 Nullable(DateTime64(6)), dateTime649 Nullable(DateTime64(9)), " + + "str Nullable(String), fixed Nullable(FixedString(6)), " + + "enum Nullable(Enum8('a' = 6, 'b' = 7, 'c' = 8)), enum8 Nullable(Enum8('a' = 1, 'b' = 2, 'c' = 3)), enum16 Nullable(Enum16('a' = 1, 'b' = 2, 'c' = 3)), " + + "uuid Nullable(UUID), ipv4 Nullable(IPv4), ipv6 Nullable(IPv6), " + + "float32 Nullable(Float32), float64 Nullable(Float64), " + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert null values + insertData("INSERT INTO test_nullable VALUES ( 1, " + + "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, " + + "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, " + + "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, " + + "NULL, NULL, NULL, NULL)"); + + //Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_nullable ORDER BY order")) { + assertTrue(rs.next()); + for (int i = 2; i <= 34; i++) { + assertTrue(rs.getObject(i) == null); + } + + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testLowCardinalityTypeSimpleStatement() throws SQLException { + runQuery("CREATE TABLE test_low_cardinality (order Int8, " + + "lowcardinality LowCardinality(String)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + String lowcardinality = "string" + rand.nextInt(1000); + + insertData(String.format("INSERT INTO test_low_cardinality VALUES ( 1, '%s' )", + lowcardinality)); + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_low_cardinality ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getString("lowcardinality"), lowcardinality); + assertEquals(rs.getObject("lowcardinality"), lowcardinality); + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testSimpleAggregateFunction() throws SQLException { + runQuery("CREATE TABLE test_aggregate (order Int8," + + " int8 Int8," + + " val SimpleAggregateFunction(any, Nullable(Int8))" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + int int8 = rand.nextInt(256) - 128; + + insertData(String.format("INSERT INTO test_aggregate VALUES ( 1, %d, null )", int8)); + insertData(String.format("INSERT INTO test_aggregate VALUES ( 2, %d, null )", int8)); + insertData(String.format("INSERT INTO test_aggregate VALUES ( 3, %d, null )", int8)); + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT sum(int8) FROM test_aggregate")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), int8 * 3); + assertEquals(rs.getObject(1), (long) (int8 * 3)); + } + try (ResultSet rs = stmt.executeQuery("SELECT any(val) FROM test_aggregate")) { + assertTrue(rs.next()); + assertNull(rs.getObject(1)); + assertTrue(rs.wasNull()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testNestedTypeSimpleStatement() throws SQLException { + runQuery("CREATE TABLE test_nested (order Int8, " + + "nested Nested (int8 Int8, int16 Int16, int32 Int32, int64 Int64, int128 Int128, int256 Int256)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + int int8 = rand.nextInt(256) - 128; + int int16 = rand.nextInt(65536) - 32768; + int int32 = rand.nextInt(); + long int64 = rand.nextLong(); + BigInteger int128 = new BigInteger(127, rand); + BigInteger int256 = new BigInteger(255, rand); + + String sql = String.format("INSERT INTO test_nested VALUES ( 1, [%s], [%s], [%s], [%s], [%s], [%s])", + int8, int16, int32, int64, int128, int256); + log.info("SQL: {}", sql); + insertData(sql); + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_nested ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(String.valueOf(((Object[])rs.getArray("nested.int8").getArray())[0]), String.valueOf(int8)); + assertEquals(String.valueOf(((Object[])rs.getArray("nested.int16").getArray())[0]), String.valueOf(int16)); + assertEquals(String.valueOf(((Object[])rs.getArray("nested.int32").getArray())[0]), String.valueOf(int32)); + assertEquals(String.valueOf(((Object[])rs.getArray("nested.int64").getArray())[0]), String.valueOf(int64)); + assertEquals(String.valueOf(((Object[])rs.getArray("nested.int128").getArray())[0]), String.valueOf(int128)); + assertEquals(String.valueOf(((Object[])rs.getArray("nested.int256").getArray())[0]), String.valueOf(int256)); + + assertFalse(rs.next()); + } + } + } + + } + + @Test(groups = { "integration" }) + public void testNestedTypeNonFlatten() throws SQLException { + if (earlierThan(25,1)){ + return; + } + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + stmt.execute("SET flatten_nested = 0"); + stmt.execute("CREATE TABLE test_nested_not_flatten (order Int8, " + + "nested Nested (int8 Int8, int16 Int16, int32 Int32, int64 Int64, int128 Int128, int256 Int256)" + + ") ENGINE = MergeTree ORDER BY () SETTINGS flatten_nested = 0"); + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + int int8 = rand.nextInt(256) - 128; + int int16 = rand.nextInt(65536) - 32768; + int int32 = rand.nextInt(); + long int64 = rand.nextLong(); + BigInteger int128 = new BigInteger(127, rand); + BigInteger int256 = new BigInteger(255, rand); + + + String nsql = String.format("INSERT INTO test_nested_not_flatten VALUES ( 1, [(%s,%s,%s,%s,%s,%s)])", + int8, int16, int32, int64, int128, int256); + log.info("SQL: {}", nsql); + stmt.executeUpdate(nsql); + + // Check the results + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_nested_not_flatten ORDER BY order")) { + assertTrue(rs.next()); + assertEquals((Object[])((Object[])((java.sql.Array) rs.getObject("nested")).getArray())[0], + new Object[] {(byte) int8, (short) int16, int32, int64, int128, int256}); + + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = {"integration"}) + public void testTupleType() throws Exception { + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::Tuple(String, Int32, Date)")) { + Object[] arr = new Object[]{"test", 123, LocalDate.parse("2026-03-02")}; + Struct tuple = conn.createStruct("Tuple(String, Int32, Date)", arr); + Array tupleArr = conn.createArrayOf("Array(Tuple(String, Int32, Date))", arr); + stmt.setObject(1, tuple); + try (ResultSet rs = stmt.executeQuery()) { + rs.next(); + Array dbTuple = rs.getArray(1); + Assert.assertEquals(dbTuple, tupleArr); + Object tupleObjArr = rs.getObject(1); + Assert.assertEquals(tupleObjArr, arr); + } + } + } + } + + @Test(groups = { "integration" }) + public void testTupleTypeSimpleStatement() throws SQLException { + runQuery("CREATE TABLE test_tuple (order Int8, " + + "tuple Tuple(int8 Int8, int16 Int16, int32 Int32, int64 Int64, int128 Int128, int256 Int256)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + int int8 = rand.nextInt(256) - 128; + int int16 = rand.nextInt(65536) - 32768; + int int32 = rand.nextInt(); + long int64 = rand.nextLong(); + BigInteger int128 = new BigInteger(127, rand); + BigInteger int256 = new BigInteger(255, rand); + + String sql = String.format("INSERT INTO test_tuple VALUES ( 1, (%s, %s, %s, %s, %s, %s))", + int8, int16, int32, int64, int128, int256); + insertData(sql); + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_tuple ORDER BY order")) { + assertTrue(rs.next()); + Object[] tuple = (Object[]) rs.getObject(2); + assertEquals(String.valueOf(tuple[0]), String.valueOf(int8)); + assertEquals(String.valueOf(tuple[1]), String.valueOf(int16)); + assertEquals(String.valueOf(tuple[2]), String.valueOf(int32)); + assertEquals(String.valueOf(tuple[3]), String.valueOf(int64)); + assertEquals(String.valueOf(tuple[4]), String.valueOf(int128)); + assertEquals(String.valueOf(tuple[5]), String.valueOf(int256)); + assertFalse(rs.next()); + } + } + } + } + + + + @Test(groups = { "integration" }) + public void testJSONWritingAsString() throws SQLException { + if (ClickHouseVersion.of(getServerVersion()).check("(,24.8]")) { + return; // JSON was introduced in 24.10 + } + + Properties createProperties = new Properties(); + createProperties.put(ClientConfigProperties.serverSetting("allow_experimental_json_type"), "1"); + runQuery("CREATE TABLE test_json (order Int8, " + + "json JSON" + + ") ENGINE = MergeTree ORDER BY ()", createProperties); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + double key1 = rand.nextDouble(); + int key2 = rand.nextInt(); + final String json = "{\"key1\": \"" + key1 + "\", \"key2\": " + key2 + ", \"key3\": [1000, \"value3\", 400000]}"; + final String serverJson = "{\"key1\":\"" + key1 + "\",\"key2\":\"" + key2 + "\",\"key3\":[\"1000\",\"value3\",\"400000\"]}"; + insertData(String.format("INSERT INTO test_json VALUES ( 1, '%s' )", json)); + + // Check the results + Properties props = new Properties(); + props.setProperty( + ClientConfigProperties.serverSetting(ServerSettings.OUTPUT_FORMAT_BINARY_WRITE_JSON_AS_STRING), + "1"); + props.setProperty(ClientConfigProperties.serverSetting("output_format_json_quote_64bit_integers"), "1"); + try (Connection conn = getJdbcConnection(props)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_json ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getString("json"), serverJson); + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testReadingJSONBinary() throws SQLException { + if (ClickHouseVersion.of(getServerVersion()).check("(,24.8]")) { + return; // JSON was introduced in 24.10 + } + + Properties properties = new Properties(); + properties.put(ClientConfigProperties.serverSetting("allow_experimental_json_type"), "1"); + try (Connection conn = getJdbcConnection(properties); + Statement stmt = conn.createStatement()) { + + final String json = "{\"count\": 1000, \"event\": { \"name\": \"start\", \"value\": 0.10} }"; + String sql = String.format("SELECT %1$s::JSON(), %1$s::JSON(count Int16)", SQLUtils.enquoteLiteral(json)); + try (ResultSet rs = stmt.executeQuery(sql)) { + rs.next(); + + Map val1 = (Map) rs.getObject(1); + assertEquals(val1.get("count"), 1000L); + Map val2 = (Map) rs.getObject(2); + assertEquals(val2.get("count"), (short)1000); + } + } + } + + + @Test(groups = { "integration" }, enabled = false) + public void testGeometricTypesSimpleStatement() throws SQLException { + // TODO: add LineString and MultiLineString support + runQuery("CREATE TABLE test_geometric (order Int8, " + + "point Point, ring Ring, linestring LineString, multilinestring MultiLineString, polygon Polygon, multipolygon MultiPolygon" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + String point = "(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")"; + String ring = "[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]"; + String linestring = "[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]"; + String multilinestring = "[[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")],[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]]"; + String polygon = "[[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")],[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]]"; + String multipolygon = "[[[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")],[(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + "),(" + rand.nextInt(1000) + "," + rand.nextInt(1000) + ")]]]"; + + insertData(String.format("INSERT INTO test_geometric VALUES ( 1, %s, %s, %s, %s, %s, %s )", + point, ring, linestring, multilinestring, polygon, multipolygon)); + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_geometric ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getString("point"), point); + assertEquals(rs.getString("linestring"), linestring); + assertEquals(rs.getString("polygon"), polygon); + assertEquals(rs.getString("multilinestring"), multilinestring); + assertEquals(rs.getString("multipolygon"), multipolygon); + assertEquals(rs.getString("ring"), ring); + + assertFalse(rs.next()); + } + } + } + } + + + @Test(groups = { "integration" }) + public void testDynamicTypesSimpleStatement() throws SQLException { + if (earlierThan(24, 8)) { + return; + } + + Properties properties = new Properties(); + properties.setProperty(ClientConfigProperties.serverSetting("allow_experimental_dynamic_type"), "1"); + runQuery("CREATE TABLE test_dynamic (order Int8, " + + "dynamic Dynamic" + + ") ENGINE = MergeTree ORDER BY ()", + properties); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + String dynamic = "string" + rand.nextInt(1000); + int dynamic2 = rand.nextInt(256) - 128; + double dynamic3 = rand.nextDouble(); + + String sql = String.format("INSERT INTO test_dynamic VALUES ( 1, '%s' )", dynamic); + insertData(sql); + + sql = String.format("INSERT INTO test_dynamic VALUES ( 2, %d )", dynamic2); + insertData(sql); + + sql = String.format("INSERT INTO test_dynamic VALUES ( 3, %s )", dynamic3); + insertData(sql); + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_dynamic ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getString("dynamic"), dynamic); + + assertTrue(rs.next()); + assertEquals(rs.getInt("dynamic"), dynamic2); + + assertTrue(rs.next()); + assertEquals(rs.getDouble("dynamic"), dynamic3); + + assertFalse(rs.next()); + } + } + } + } + + + @Test(groups = { "integration" }) + public void testTypeConversions() throws Exception { + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT 1, 'true', '1.0', " + + "toDate('2024-12-01'), toDateTime('2024-12-01 12:34:56'), toDateTime64('2024-12-01 12:34:56.789', 3), toDateTime64('2024-12-01 12:34:56.789789', 6), toDateTime64('2024-12-01 12:34:56.789789789', 9)")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 1); + assertEquals(String.valueOf(rs.getObject(1)), "1"); + assertEquals(rs.getObject(1, Integer.class), 1); + assertEquals(rs.getObject(1, Long.class), 1L); + assertEquals(String.valueOf(rs.getObject(1, new HashMap>(){{put(JDBCType.INTEGER.getName(), Integer.class);}})), "1"); + + assertTrue(rs.getBoolean(2)); + assertEquals(String.valueOf(rs.getObject(2)), "true"); + assertEquals(rs.getObject(2, Boolean.class), true); + assertEquals(String.valueOf(rs.getObject(2, new HashMap>(){{put(JDBCType.BOOLEAN.getName(), Boolean.class);}})), "true"); + + assertEquals(rs.getFloat(3), 1.0f); + assertEquals(String.valueOf(rs.getObject(3)), "1.0"); + assertEquals(rs.getObject(3, Float.class), 1.0f); + assertEquals(rs.getObject(3, Double.class), 1.0); + assertEquals(String.valueOf(rs.getObject(3, new HashMap>(){{put(JDBCType.FLOAT.getName(), Float.class);}})), "1.0"); + + assertEquals(rs.getDate(4), Date.valueOf("2024-12-01")); + assertTrue(rs.getObject(4) instanceof Date); + assertEquals(rs.getObject(4), Date.valueOf("2024-12-01")); + assertEquals(rs.getString(4), "2024-12-01");//Underlying object is ZonedDateTime + assertEquals(rs.getObject(4, LocalDate.class), LocalDate.of(2024, 12, 1)); + assertThrows(SQLException.class, () -> rs.getObject(4, ZonedDateTime.class)); // Date cannot be presented as time + assertEquals(String.valueOf(rs.getObject(4, new HashMap>(){{put(JDBCType.DATE.getName(), LocalDate.class);}})), "2024-12-01"); + + assertEquals(rs.getTimestamp(5).toString(), "2024-12-01 12:34:56.0"); + assertTrue(rs.getObject(5) instanceof Timestamp); + assertEquals(rs.getObject(5), Timestamp.valueOf("2024-12-01 12:34:56")); + assertEquals(rs.getString(5), "2024-12-01 12:34:56"); + assertEquals(rs.getObject(5, LocalDateTime.class), LocalDateTime.of(2024, 12, 1, 12, 34, 56)); + assertEquals(rs.getObject(5, ZonedDateTime.class), ZonedDateTime.of(2024, 12, 1, 12, 34, 56, 0, ZoneId.of("UTC"))); + assertEquals(String.valueOf(rs.getObject(5, new HashMap>(){{put(JDBCType.TIMESTAMP.getName(), LocalDateTime.class);}})), "2024-12-01T12:34:56"); + + assertEquals(rs.getTimestamp(6).toString(), "2024-12-01 12:34:56.789"); + assertTrue(rs.getObject(6) instanceof Timestamp); + assertEquals(rs.getObject(6), Timestamp.valueOf("2024-12-01 12:34:56.789")); + assertEquals(rs.getString(6), "2024-12-01 12:34:56.789"); + assertEquals(rs.getObject(6, LocalDateTime.class), LocalDateTime.of(2024, 12, 1, 12, 34, 56, 789000000)); + assertEquals(String.valueOf(rs.getObject(6, new HashMap>(){{put(JDBCType.TIMESTAMP.getName(), LocalDateTime.class);}})), "2024-12-01T12:34:56.789"); + + assertEquals(rs.getTimestamp(7).toString(), "2024-12-01 12:34:56.789789"); + assertTrue(rs.getObject(7) instanceof Timestamp); + assertEquals(rs.getObject(7), Timestamp.valueOf("2024-12-01 12:34:56.789789")); + assertEquals(rs.getString(7), "2024-12-01 12:34:56.789789"); + assertEquals(rs.getObject(7, LocalDateTime.class), LocalDateTime.of(2024, 12, 1, 12, 34, 56, 789789000)); + assertEquals(String.valueOf(rs.getObject(7, new HashMap>(){{put(JDBCType.TIMESTAMP.getName(), OffsetDateTime.class);}})), "2024-12-01T12:34:56.789789Z"); + + assertEquals(rs.getTimestamp(8).toString(), "2024-12-01 12:34:56.789789789"); + assertTrue(rs.getObject(8) instanceof Timestamp); + assertEquals(rs.getObject(8), Timestamp.valueOf("2024-12-01 12:34:56.789789789")); + assertEquals(rs.getString(8), "2024-12-01 12:34:56.789789789"); + assertEquals(rs.getObject(8, LocalDateTime.class), LocalDateTime.of(2024, 12, 1, 12, 34, 56, 789789789)); + assertEquals(String.valueOf(rs.getObject(8, new HashMap>(){{put(JDBCType.TIMESTAMP.getName(), ZonedDateTime.class);}})), "2024-12-01T12:34:56.789789789Z[UTC]"); + } + } + } + } + + @Test(groups = { "integration" }) + public void testVariantTypesSimpleStatement() throws SQLException { + if (earlierThan(24, 8)) { + return; + } + + Properties properties = new Properties(); + properties.setProperty(ClientConfigProperties.serverSetting("allow_experimental_variant_type"), "1"); + runQuery("CREATE TABLE test_variant (order Int8, " + + "v Variant(String, Int32)" + + ") ENGINE = MergeTree ORDER BY ()", + properties); + + // Insert random (valid) values + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + log.info("Random seed was: {}", seed); + + String variant1 = "string" + rand.nextInt(1000); + int variant2 = rand.nextInt(256) - 128; + + String sql = String.format("INSERT INTO test_variant VALUES ( 1, '%s' )", variant1); + insertData(sql); + + sql = String.format("INSERT INTO test_variant VALUES ( 2, %d )", variant2); + insertData(sql); + + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_variant ORDER BY order")) { + assertTrue(rs.next()); + assertEquals(rs.getString("v"), variant1); + assertTrue(rs.getObject("v") instanceof String); + + assertTrue(rs.next()); + assertEquals(rs.getInt("v"), variant2); + assertTrue(rs.getObject("v") instanceof Number); + + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = { "integration" }) + public void testGeoPoint1() throws Exception { + final Double[][] spatialArrayData = new Double[][] { + {4.837388, 52.38795}, + {4.951513, 52.354582}, + {4.961987, 52.371763}, + {4.870017, 52.334932}, + {4.89813, 52.357238}, + {4.852437, 52.370315}, + {4.901712, 52.369567}, + {4.874112, 52.339823}, + {4.856942, 52.339122}, + {4.870253, 52.360353}, + }; + + StringBuilder sql = new StringBuilder(); + sql.append("SELECT \n"); + sql.append("\tcast(arrayJoin(["); + for (int i = 0; i < spatialArrayData.length; i++) { + sql.append("(" + spatialArrayData[i][0] + ", " + spatialArrayData[i][1] + ")").append(','); + } + sql.setLength(sql.length() - 1); + sql.append("])"); + sql.append("as Point) as Point"); + + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery(sql.toString())) { + + ResultSetMetaData metaData = rs.getMetaData(); + assertEquals(metaData.getColumnCount(), 1); + assertEquals(metaData.getColumnTypeName(1), ClickHouseDataType.Point.name()); + assertEquals(metaData.getColumnType(1), Types.ARRAY); + + int rowCount = 0; + while (rs.next()) { + Object asObject = rs.getObject(1); + assertTrue(asObject instanceof double[]); + Array asArray = rs.getArray(1); + assertEquals(asArray.getArray(), spatialArrayData[rowCount]); + assertEquals(asObject, asArray.getArray()); + rowCount++; + } + assertTrue(rowCount > 0); + } + } + } + + @Test(groups = { "integration" }) + public void testGeoPoint() throws Exception { + final double[] row = new double[] { + 10.123456789, + 11.123456789 + }; + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + String table = "test_geo_point"; + stmt.executeUpdate("DROP TABLE IF EXISTS " + table); + stmt.executeUpdate("CREATE TABLE " + table + " (geom Point) ENGINE = MergeTree ORDER BY ()"); + + try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { + Double[] rowObj = Arrays.stream(row).boxed().toArray(Double[]::new); + pstmt.setObject(1, conn.createStruct("Tuple(Float64, Float64)", rowObj)); + pstmt.executeUpdate(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { + int geomColumn = 1; + ResultSetMetaData rsMd = rs.getMetaData(); + assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.Point.name()); + assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); + + rs.next(); + assertTrue(rs.isLast()); + Object asObject = rs.getObject(geomColumn); + assertTrue(asObject instanceof double[]); + Array asArray = rs.getArray(geomColumn); + assertEquals(asArray.getArray(), row); + assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.Point.name()); + assertEquals(asArray.getBaseType(), Types.ARRAY); + } + } + } + + @Test(groups = { "integration" }) + public void testGeoRing() throws Exception { + final Double[][] row = new Double[][] { + {10.123456789, 11.123456789}, + {12.123456789, 13.123456789}, + {14.123456789, 15.123456789}, + {10.123456789, 11.123456789}, + }; + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + final String table = "test_geo_ring"; + stmt.executeUpdate("DROP TABLE IF EXISTS " + table); + stmt.executeUpdate("CREATE TABLE " + table + " (geom Ring) ENGINE = MergeTree ORDER BY ()"); + + try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { + pstmt.setObject(1, conn.createArrayOf("Array(Point)", row)); + pstmt.executeUpdate(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { + int geomColumn = 1; + ResultSetMetaData rsMd = rs.getMetaData(); + assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.Ring.name()); + assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); + + rs.next(); + assertTrue(rs.isLast()); + Object asObject = rs.getObject(geomColumn); + assertTrue(asObject instanceof double[][]); + Array asArray = rs.getArray(geomColumn); + assertEquals(asArray.getArray(), row); + assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.Ring.name()); + assertEquals(asArray.getBaseType(), Types.ARRAY); + } + } + } + + @Test(groups = { "integration" }) + public void testGeoLineString() throws Exception { + final Double[][] row = new Double[][] { + {10.123456789, 11.123456789}, + {12.123456789, 13.123456789}, + {14.123456789, 15.123456789}, + {10.123456789, 11.123456789}, + }; + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + final String table = "test_geo_line_string"; + stmt.executeUpdate("DROP TABLE IF EXISTS " + table); + stmt.executeUpdate("CREATE TABLE " + table +" (geom LineString) ENGINE = MergeTree ORDER BY ()"); + + try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { + pstmt.setObject(1, conn.createArrayOf("Array(Point)", row)); + pstmt.executeUpdate(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { + int geomColumn = 1; + ResultSetMetaData rsMd = rs.getMetaData(); + assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.LineString.name()); + assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); + + rs.next(); + assertTrue(rs.isLast()); + Object asObject = rs.getObject(geomColumn); + assertTrue(asObject instanceof double[][]); + Array asArray = rs.getArray(geomColumn); + assertEquals(asArray.getArray(), row); + assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.LineString.name()); + assertEquals(asArray.getBaseType(), Types.ARRAY); + } + } + } + + @Test(groups = { "integration" }) + public void testGeoMultiLineString() throws Exception { + final Double[][][] row = new Double[][][] { + { // LineString 1 + {10.123456789, 11.123456789}, + {12.123456789, 13.123456789}, + {14.123456789, 15.123456789}, + {10.123456789, 11.123456789}, + }, + { + {16.123456789, 17.123456789}, + {18.123456789, 19.123456789}, + {20.123456789, 21.123456789}, + {16.123456789, 17.123456789}, + } + }; + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + final String table = "test_geo_multi_line_string"; + stmt.executeUpdate("DROP TABLE IF EXISTS " + table); + stmt.executeUpdate("CREATE TABLE " + table +" (geom MultiLineString) ENGINE = MergeTree ORDER BY ()"); + + try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { + pstmt.setObject(1, conn.createArrayOf("Array(Array(Point))", row)); + pstmt.executeUpdate(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { + int geomColumn = 1; + ResultSetMetaData rsMd = rs.getMetaData(); + assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.MultiLineString.name()); + assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); + + rs.next(); + assertTrue(rs.isLast()); + Object asObject = rs.getObject(geomColumn); + assertTrue(asObject instanceof double[][][]); + Array asArray = rs.getArray(geomColumn); + assertEquals(asArray.getArray(), row); + assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.MultiLineString.name()); + assertEquals(asArray.getBaseType(), Types.ARRAY); + } + } + } + + @Test(groups = { "integration" }) + public void testGeoPolygon() throws Exception { + final Double[][][] row = new Double[][][] { + { // Ring 1 + {10.123456789, 11.123456789}, + {12.123456789, 13.123456789}, + {14.123456789, 15.123456789}, + {10.123456789, 11.123456789}, + }, + { // Ring 2 + {16.123456789, 17.123456789}, + {18.123456789, 19.123456789}, + {20.123456789, 21.123456789}, + {16.123456789, 17.123456789}, + } + }; + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + final String table = "test_geo_polygon"; + stmt.executeUpdate("DROP TABLE IF EXISTS " + table); + stmt.executeUpdate("CREATE TABLE " + table +" (geom Polygon) ENGINE = MergeTree ORDER BY ()"); + + try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { + pstmt.setObject(1, conn.createArrayOf("Array(Array(Point))", row)); + pstmt.executeUpdate(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { + int geomColumn = 1; + ResultSetMetaData rsMd = rs.getMetaData(); + assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.Polygon.name()); + assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); + + rs.next(); + assertTrue(rs.isLast()); + Object asObject = rs.getObject(geomColumn); + assertTrue(asObject instanceof double[][][]); + Array asArray = rs.getArray(geomColumn); + assertEquals(asArray.getArray(), row); + assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.Polygon.name()); + assertEquals(asArray.getBaseType(), Types.ARRAY); + } + } + } + + @Test(groups = { "integration" }) + public void testGeoMultiPolygon() throws Exception { + final Double[][][][] row = new Double[][][][] { + { // Polygon 1 + { // Ring 1 + {10.123456789, 11.123456789}, + {12.123456789, 13.123456789}, + {14.123456789, 15.123456789}, + {10.123456789, 11.123456789}, + }, + { // Ring 2 + {16.123456789, 17.123456789}, + {18.123456789, 19.123456789}, + {20.123456789, 21.123456789}, + {16.123456789, 17.123456789}, + } + }, + { // Polygon 2 + { // Ring 1 + {-10.123456789, -11.123456789}, + {-12.123456789, -13.123456789}, + {-14.123456789, -15.123456789}, + {-10.123456789, -11.123456789}, + }, + { // Ring 2 + {-16.123456789, -17.123456789}, + {-18.123456789, -19.123456789}, + {-20.123456789, -21.123456789}, + {-16.123456789, -17.123456789}, + } + } + }; + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + final String table = "test_geo_muti_polygon"; + stmt.executeUpdate("DROP TABLE IF EXISTS " + table); + stmt.executeUpdate("CREATE TABLE " + table +" (geom MultiPolygon) ENGINE = MergeTree ORDER BY ()"); + + try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?)")) { + pstmt.setObject(1, conn.createArrayOf("Array(Array(Array(Point)))", row)); + pstmt.executeUpdate(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { + int geomColumn = 1; + ResultSetMetaData rsMd = rs.getMetaData(); + assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.MultiPolygon.name()); + assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); + + rs.next(); + assertTrue(rs.isLast()); + Object asObject = rs.getObject(geomColumn); + assertTrue(asObject instanceof double[][][][]); + Array asArray = rs.getArray(geomColumn); + assertEquals(asArray.getArray(), row); + assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.MultiPolygon.name()); + assertEquals(asArray.getBaseType(), Types.ARRAY); + } + } + } + + @Test(groups = { "integration" }) + public void testGeoGeometry() throws Exception { + if (earlierThan(25, 11)) { + return; + } + + final Object[] expectedValues = new Object[] { + new double[] {1D, 2D}, + new double[][] {{1D, 2D}, {3D, 4D}, {1D, 2D}}, + new double[][][] {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + new double[][][][] { + {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + {{{5D, 6D}, {7D, 8D}, {5D, 6D}}} + } + }; + final String table = "test_geo_geometry"; + Properties properties = new Properties(); + properties.setProperty(ClientConfigProperties.serverSetting("allow_suspicious_variant_types"), "1"); + + try (Connection conn = getJdbcConnection(properties); Statement stmt = conn.createStatement()) { + stmt.executeUpdate("DROP TABLE IF EXISTS " + table); + stmt.executeUpdate("CREATE TABLE " + table + " (rowId Int32, geom Geometry) ENGINE = MergeTree ORDER BY ()"); + stmt.executeUpdate("INSERT INTO " + table + " VALUES " + + "(0, (1, 2)), " + + "(1, [(1, 2), (3, 4), (1, 2)]), " + + "(2, [[(1, 2), (3, 4), (1, 2)]]), " + + "(3, [[[(1, 2), (3, 4), (1, 2)]], [[(5, 6), (7, 8), (5, 6)]]])"); + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table + " ORDER BY rowId")) { + final int geomColumn = 2; + ResultSetMetaData rsMd = rs.getMetaData(); + assertEquals(rsMd.getColumnTypeName(geomColumn), ClickHouseDataType.Geometry.name()); + assertEquals(rsMd.getColumnType(geomColumn), Types.ARRAY); + + int rowId = 0; + while (rs.next()) { + assertEquals(rs.getInt("rowId"), rowId); + assertFalse(rs.getString("geom").isEmpty()); + + Object asObject = rs.getObject(geomColumn); + assertGeometryValue(asObject, expectedValues[rowId]); + + Array asArray = rs.getArray(geomColumn); + assertGeometryValue(asArray.getArray(), expectedValues[rowId]); + assertEquals(asArray.getBaseTypeName(), ClickHouseDataType.Geometry.name()); + assertEquals(asArray.getBaseType(), Types.ARRAY); + rowId++; + } + + assertEquals(rowId, expectedValues.length); + } + } + } + + @Test(groups = { "integration" }) + public void testGeoGeometryPreparedStatement() throws Exception { + if (earlierThan(25, 11)) { + return; + } + + final Object[] expectedValues = new Object[] { + new double[] {1D, 2D}, + new double[][] {{1D, 2D}, {3D, 4D}, {1D, 2D}}, + new double[][][] {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + new double[][][][] { + {{{1D, 2D}, {3D, 4D}, {1D, 2D}}}, + {{{5D, 6D}, {7D, 8D}, {5D, 6D}}} + } + }; + final String table = "test_geo_geometry_ps"; + Properties properties = new Properties(); + properties.setProperty(ClientConfigProperties.serverSetting("allow_suspicious_variant_types"), "1"); + + try (Connection conn = getJdbcConnection(properties); Statement stmt = conn.createStatement()) { + stmt.executeUpdate("DROP TABLE IF EXISTS " + table); + stmt.executeUpdate("CREATE TABLE " + table + " (rowId Int32, geom Geometry) ENGINE = MergeTree ORDER BY ()"); + + try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?, ?)")) { + for (int i = 0; i < expectedValues.length; i++) { + pstmt.setInt(1, i); + pstmt.setObject(2, createGeometryParameter(conn, expectedValues[i])); + pstmt.addBatch(); + } + assertEquals(pstmt.executeBatch().length, expectedValues.length); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + table + " ORDER BY rowId")) { + int rowId = 0; + while (rs.next()) { + assertEquals(rs.getInt("rowId"), rowId); + assertFalse(rs.getString("geom").isEmpty()); + assertGeometryValue(rs.getObject("geom"), expectedValues[rowId]); + assertGeometryValue(rs.getArray("geom").getArray(), expectedValues[rowId]); + rowId++; + } + + assertEquals(rowId, expectedValues.length); + } + } + } + + private static Object createGeometryParameter(Connection conn, Object value) throws SQLException { + if (value instanceof double[]) { + return conn.createStruct("Tuple(Float32, Float32)", Arrays.stream((double[]) value).boxed().toArray(Double[]::new)); + } else if (value instanceof double[][]) { + return conn.createArrayOf("Array(Point)", box2d((double[][]) value)); + } else if (value instanceof double[][][]) { + return conn.createArrayOf("Array(Array(Point))", box3d((double[][][]) value)); + } else if (value instanceof double[][][][]) { + return conn.createArrayOf("Array(Array(Array(Point)))", box4d((double[][][][]) value)); + } + + throw new SQLException("Unsupported geometry parameter type: " + value.getClass()); + } + + private static Double[][] box2d(double[][] value) { + Double[][] result = new Double[value.length][]; + for (int i = 0; i < value.length; i++) { + result[i] = Arrays.stream(value[i]).boxed().toArray(Double[]::new); + } + return result; + } + + private static Double[][][] box3d(double[][][] value) { + Double[][][] result = new Double[value.length][][]; + for (int i = 0; i < value.length; i++) { + result[i] = box2d(value[i]); + } + return result; + } + + private static Double[][][][] box4d(double[][][][] value) { + Double[][][][] result = new Double[value.length][][][]; + for (int i = 0; i < value.length; i++) { + result[i] = box3d(value[i]); + } + return result; + } + + private static void assertGeometryValue(Object actual, Object expected) { + Assert.assertNotNull(actual); + Assert.assertTrue(actual.getClass().isArray(), "Geometry value should be returned as an array"); + assertGeometryArrayEquals(actual, expected); + } + + private static void assertGeometryArrayEquals(Object actual, Object expected) { + int actualLength = java.lang.reflect.Array.getLength(actual); + int expectedLength = java.lang.reflect.Array.getLength(expected); + assertEquals(actualLength, expectedLength); + + for (int i = 0; i < expectedLength; i++) { + Object actualItem = java.lang.reflect.Array.get(actual, i); + Object expectedItem = java.lang.reflect.Array.get(expected, i); + + if (expectedItem != null && expectedItem.getClass().isArray()) { + Assert.assertNotNull(actualItem); + Assert.assertTrue(actualItem.getClass().isArray(), + "Expected nested array but found: " + actualItem.getClass()); + assertGeometryArrayEquals(actualItem, expectedItem); + } else { + assertEquals(((Number) actualItem).doubleValue(), ((Number) expectedItem).doubleValue()); + } + } + } + + private static final HashMap EMPTY_JSON = new HashMap<>(); + + @Test(groups = { "integration" }, dataProvider = "testJSONReadDP") + public void testJSONRead(String json, Object expected) throws Exception { + if (ClickHouseVersion.of(getServerVersion()).check("(,24.8]")) { + return; // JSON was introduced in 24.10 + } + Properties createProperties = new Properties(); + createProperties.put(ClientConfigProperties.serverSetting("allow_experimental_json_type"), "1"); + runQuery("DROP TABLE IF EXISTS test_jdbc_json_read"); + runQuery("CREATE TABLE test_jdbc_json_read (data JSON) ENGINE = MergeTree ORDER BY ()", createProperties); + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + final String sql = "INSERT INTO test_jdbc_json_read (data) VALUES ('%s'), ('{}')"; + stmt.executeUpdate(String.format(sql, json)); + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_jdbc_json_read")) { + + assertTrue(rs.next()); + Object jsonObj = rs.getObject(1); + if (expected == null) { + expected = jsonToClientMap(json); + } + assertEquals(jsonObj, expected); + assertTrue(rs.next()); + Object emptyJsonObj = rs.getObject(1); + assertEquals(emptyJsonObj, EMPTY_JSON); + assertFalse(rs.next()); + } + } + } + + private final ObjectMapper objectMapper = new ObjectMapper() + .enable(DeserializationFeature.USE_LONG_FOR_INTS); + + private HashMap jsonToClientMap(String json) { + try { + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (IOException e) { + throw new RuntimeException("Failed to read json to Map", e); + } + } + + @DataProvider(name = "testJSONReadDP") + public Object[][] testJSONReadDP() { + Map map1 = new HashMap<>(); + map1.put("nested.key", "value"); + Map map2 = new HashMap<>(); + map2.put("nested.numbers",new ArrayList() {{ add(1L); add(2L); add(3L); }}); + Map map3 = new HashMap<>(); + map3.put("nested.strings", new ArrayList() {{ add("one"); add("two"); add("three"); }}); + Map map4 = new HashMap<>(); + map4.put("array", new ArrayList>() {{ + add(new HashMap() {{ + put("nested.key", "value"); + }}); + add(new HashMap() {{ + put("nested.numbers", new ArrayList() {{ + add(1L); + add(2L); + add(3L); + }}); + }}); + }}); + Map map5 = new HashMap<>(); + map5.put("array", new ArrayList>() {{ + add(new HashMap() {{ + put("nested.strings", new ArrayList() {{ add("one"); add("two"); add("three"); }}); + + }}); + }}); + Map map6 = new HashMap<>(); + map6.put("level1.level2.level3", "value"); + + Map map7 = new HashMap<>(); + map7.put("level1.level2.level3.level4", "value"); + + return new Object[][] { + {"{\"key\": \"value\"}", null}, // Simple object + {"{\"numbers\":[1, 2, 3]}", null}, + {"{\"strings\":[\"one\", \"two\", \"three\"]}", null}, + {"{\"nested\":{\"key\": \"value\"}}", map1}, // nested objects + {"{\"nested\":{\"numbers\":[1, 2, 3]}}", map2}, // nested objects + {"{\"nested\":{\"strings\":[\"one\", \"two\", \"three\"]}}", map3}, // nested objects + {"{\"array\":[{\"key\": \"value\"},{\"key\": \"value\"}]}", null}, // array of objects + {"{\"array\":[{\"numbers\":[1, 2, 3]},{\"strings\":[\"one\", \"two\", \"three\"]}]}", null}, // array of objects + {"{\"array\":[{\"nested\":{\"key\": \"value\"}},{\"nested\":{\"numbers\":[1, 2, 3]}}]}", map4}, // array of objects + {"{\"array\":[{\"nested\":{\"strings\":[\"one\", \"two\", \"three\"]}}]}", map5}, // array of objects + {"{\"array\":[{\"nested\":[{\"key\": \"value\"}]}]}", null}, // simple array of objects + {"{\"level1\": {\"level2\": {\"level3\": \"value\"}}}", map6}, // deep nested objects + {"{\"level1\": {\"level2\": {\"level3\": {\"level4\": \"value\"}}}}", map7}, // deep nested objects + + }; + } + + /** + * Tests that both Time and DateTime columns are readable as JDBC TIME type. + * ClickHouse added Time and Time64 support in version 25.6. + * On older versions DateTime types were used to emulate TIME. + * This test ensures compatibility for reading time values from both column types. + */ + @Test(groups = { "integration" }) + public void testTimeAndDateTimeCompatibleWithJDBCTime() throws Exception { + boolean hasTimeType = !ClickHouseVersion.of(getServerVersion()).check("(,25.5]"); + + Properties createProperties = new Properties(); + if (hasTimeType) { + createProperties.put(ClientConfigProperties.serverSetting("allow_experimental_time_time64_type"), "1"); + } + + // Create table with DateTime columns (always supported) and Time columns (if available) + String tableDDL = hasTimeType + ? "CREATE TABLE test_time_compat (order Int8, " + + "time Time, time64 Time64(3), " + + "dateTime DateTime('UTC'), dateTime64 DateTime64(3, 'UTC') " + + ") ENGINE = MergeTree ORDER BY ()" + : "CREATE TABLE test_time_compat (order Int8, " + + "dateTime DateTime('UTC'), dateTime64 DateTime64(3, 'UTC') " + + ") ENGINE = MergeTree ORDER BY ()"; + + runQuery(tableDDL, createProperties); + + // Insert values representing times: 12:34:56 and 23:59:59.999 + String insertSQL = hasTimeType + ? "INSERT INTO test_time_compat (order, time, time64, dateTime, dateTime64) VALUES " + + "(1, '12:34:56', '12:34:56.789', '1970-01-01 12:34:56', '1970-01-01 12:34:56.789'), " + + "(2, '23:59:59', '23:59:59.999', '1970-01-01 23:59:59', '1970-01-01 23:59:59.999')" + : "INSERT INTO test_time_compat (order, dateTime, dateTime64) VALUES " + + "(1, '1970-01-01 12:34:56', '1970-01-01 12:34:56.789'), " + + "(2, '1970-01-01 23:59:59', '1970-01-01 23:59:59.999')"; + + runQuery(insertSQL, createProperties); + + // Expected values for each row: [order, year, month, day, hours, minutes, seconds, milliseconds] + // Note: month is 1-based (1 = January) + int[][] expectedValues = { + {1, 1970, 1, 1, 12, 34, 56, 789}, + {2, 1970, 1, 1, 23, 59, 59, 999} + }; + + Calendar utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + + // Check that all columns are readable as java.sql.Time and verify date components + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_time_compat ORDER BY order")) { + for (int[] expected : expectedValues) { + assertTrue(rs.next()); + assertEquals(rs.getInt("order"), expected[0]); + + // Test DateTime columns as Time (always available) + verifyTimeValue(rs.getTime("dateTime", utcCalendar), expected[4], expected[5], expected[6], 0, utcCalendar); + verifyTimeValue(rs.getTime("dateTime64", utcCalendar), expected[4], expected[5], expected[6], expected[7], utcCalendar); + + // Verify date components for DateTime columns + verifyDateValue(rs.getDate("dateTime", utcCalendar), expected[1], expected[2], expected[3], utcCalendar); + verifyDateValue(rs.getDate("dateTime64", utcCalendar), expected[1], expected[2], expected[3], utcCalendar); + + if (hasTimeType) { + // Test Time columns as Time + verifyTimeValue(rs.getTime("time", utcCalendar), expected[4], expected[5], expected[6], 0, utcCalendar); + verifyTimeValue(rs.getTime("time64", utcCalendar), expected[4], expected[5], expected[6], expected[7], utcCalendar); + } + } + assertFalse(rs.next()); + } + } + } + } + + private static void assertNotNull(Object obj) { + Assert.assertNotNull(obj); + } + + private static void verifyTimeValue(Time time, int expectedHours, int expectedMinutes, + int expectedSeconds, int expectedMillis, Calendar calendar) { + assertNotNull(time); + calendar.setTime(time); + assertEquals(calendar.get(Calendar.HOUR_OF_DAY), expectedHours); + assertEquals(calendar.get(Calendar.MINUTE), expectedMinutes); + assertEquals(calendar.get(Calendar.SECOND), expectedSeconds); + if (expectedMillis > 0) { + assertEquals(time.getTime() % 1000, expectedMillis); + } + } + + private static void verifyDateValue(Date date, int expectedYear, int expectedMonth, + int expectedDay, Calendar calendar) { + assertNotNull(date); + calendar.setTime(date); + assertEquals(calendar.get(Calendar.YEAR), expectedYear); + assertEquals(calendar.get(Calendar.MONTH) + 1, expectedMonth); // Calendar.MONTH is 0-based, convert to 1-based + assertEquals(calendar.get(Calendar.DAY_OF_MONTH), expectedDay); + } +}