diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseStatementTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseStatementTest.java index ec676d368..6a1e1d832 100644 --- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseStatementTest.java +++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseStatementTest.java @@ -52,6 +52,7 @@ import java.util.List; import java.util.Locale; import java.util.Properties; +import java.util.ServiceLoader; import java.util.TimeZone; import java.util.UUID; import java.util.concurrent.CountDownLatch; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java index 3e1d1a7e6..aaa37f678 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java @@ -929,9 +929,9 @@ public String encodeArray(Object[] elements, int levels, ClickHouseDataType elem } else if (cursor.arrayAsTuple) { arraySb.append(encodeTuple((Object[]) element)).append(','); cursor.pos++; - } else if (cursor.level == 1 && elementType == ClickHouseDataType.Tuple && element instanceof Array ) { + } else if (cursor.level == 1 && isTupleType(elementType) && element instanceof Array ) { cursor.arrayObjAsTuple = true; - } else if (cursor.level == 1 && elementType == ClickHouseDataType.Tuple && element instanceof Object[] ) { + } else if (cursor.level == 1 && isTupleType(elementType) && element instanceof Object[] ) { cursor.arrayAsTuple = true; } else if (cursor.level == 1) { arraySb.append(encodeObject(element)).append(','); @@ -947,6 +947,10 @@ public String encodeArray(Object[] elements, int levels, ClickHouseDataType elem return arraySb.toString(); } + private static boolean isTupleType(ClickHouseDataType type ) { + return type == ClickHouseDataType.Tuple || type == ClickHouseDataType.Point; + } + private static final class ArrayProcessingCursor { Object[] array; // current array int pos; // processing position 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 2f8444897..8437d36d2 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -3,7 +3,7 @@ import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.metadata.TableSchema; import com.clickhouse.client.api.query.QueryResponse; -import com.clickhouse.data.ClickHouseDataType; +import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.FeatureManager; import com.clickhouse.jdbc.internal.JdbcUtils; @@ -35,6 +35,7 @@ import java.sql.Timestamp; import java.time.ZonedDateTime; import java.util.Calendar; +import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -481,16 +482,6 @@ protected void setMetaData(ResultSetMetaDataImpl metaData) { this.metaData = metaData; } - @Override - public Object getObject(int columnIndex) throws SQLException { - return getObject(columnIndex, JdbcUtils.convertToJavaClass(getSchema().getColumnByIndex(columnIndex).getDataType())); - } - - @Override - public Object getObject(String columnLabel) throws SQLException { - return getObject(columnLabel, JdbcUtils.convertToJavaClass(getSchema().getColumnByName(columnLabel).getDataType())); - } - @Override public int findColumn(String columnLabel) throws SQLException { checkClosed(); @@ -962,12 +953,6 @@ public Statement getStatement() throws SQLException { return this.parentStatement; } - @Override - public Object getObject(int columnIndex, Map> map) throws SQLException { - ClickHouseDataType type = getSchema().getColumnByIndex(columnIndex).getDataType(); - return getObject(columnIndex, map.get(JdbcUtils.convertToSqlType(type).getName())); - } - @Override public Ref getRef(int columnIndex) throws SQLException { return getRef(columnIndexToName(columnIndex)); @@ -988,12 +973,6 @@ public java.sql.Array getArray(int columnIndex) throws SQLException { return getObject(columnIndex, java.sql.Array.class); } - @Override - public Object getObject(String columnLabel, Map> map) throws SQLException { - checkClosed(); - return getObject(columnLabel, map.get(JdbcUtils.convertToSqlType(getSchema().getColumnByName(columnLabel).getDataType()).getName())); - } - @Override public Ref getRef(String columnLabel) throws SQLException { checkClosed(); @@ -1437,38 +1416,72 @@ public void updateNClob(String columnLabel, Reader reader) throws SQLException { } @Override - public T getObject(int columnIndex, Class type) throws SQLException { + public Object getObject(int columnIndex) throws SQLException { + return getObject(columnIndexToName(columnIndex)); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + return getObjectImpl(columnLabel, null, Collections.emptyMap()); + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + return getObject(columnIndexToName(columnIndex), map); + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { checkClosed(); - try { - if (reader.hasValue(columnIndex)) { - wasNull = false; - if (type == null) {//As a fallback, try to get the value as is - return reader.readValue(columnIndex); - } + return getObjectImpl(columnLabel, null, map); + } - return (T) JdbcUtils.convert(reader.readValue(columnIndex), type, type == java.sql.Array.class ? getSchema().getColumnByIndex(columnIndex) : null); - } else { - wasNull = true; - return null; - } - } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getObject(\"%s\", %s) encountered an exception.", - reader.getSchema().columnIndexToName(columnIndex), type), - String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); - } + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + checkClosed(); + return getObject(columnIndexToName(columnIndex), type); } @Override public T getObject(String columnLabel, Class type) throws SQLException { checkClosed(); + return getObjectImpl(columnLabel, type, Collections.emptyMap()); + } + + @SuppressWarnings("unchecked") + public T getObjectImpl(String columnLabel, Class type, Map> typeMap) throws SQLException { try { + ClickHouseColumn column = getSchema().getColumnByName(columnLabel); + if (column == null) { + throw new SQLException("Column \"" + columnLabel + "\" does not exist."); + } + if (reader.hasValue(columnLabel)) { wasNull = false; + + if (type == null) { + switch (column.getDataType()) { + case Point: + case Ring: + case LineString: + case Polygon: + case MultiPolygon: + case MultiLineString: + break; // read as is + default: + if (typeMap == null || typeMap.isEmpty()) { + type = JdbcUtils.convertToJavaClass(column.getDataType()); + } else { + type = typeMap.get(JdbcUtils.convertToSqlType(column.getDataType()).getName()); + } + } + } + if (type == null) {//As a fallback, try to get the value as is return reader.readValue(columnLabel); } - return (T) JdbcUtils.convert(reader.readValue(columnLabel), type, type == java.sql.Array.class ? getSchema().getColumnByName(columnLabel) : null); + return (T) JdbcUtils.convert(reader.readValue(columnLabel), type, column); } else { wasNull = true; return null; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java index 2562b5529..f555fe8de 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java @@ -24,6 +24,7 @@ import java.time.chrono.ChronoZonedDateTime; import java.time.temporal.TemporalAccessor; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; @@ -78,12 +79,12 @@ private static Map generateTypeMap() { map.put(ClickHouseDataType.Array, JDBCType.ARRAY); map.put(ClickHouseDataType.Nested, JDBCType.ARRAY); map.put(ClickHouseDataType.Map, JDBCType.JAVA_OBJECT); - map.put(ClickHouseDataType.Point, JDBCType.OTHER); - map.put(ClickHouseDataType.Ring, JDBCType.OTHER); - map.put(ClickHouseDataType.Polygon, JDBCType.OTHER); - map.put(ClickHouseDataType.LineString, JDBCType.OTHER); - map.put(ClickHouseDataType.MultiPolygon, JDBCType.OTHER); - map.put(ClickHouseDataType.MultiLineString, JDBCType.OTHER); + map.put(ClickHouseDataType.Point, JDBCType.ARRAY); + map.put(ClickHouseDataType.Ring, JDBCType.ARRAY); + map.put(ClickHouseDataType.Polygon, JDBCType.ARRAY); + map.put(ClickHouseDataType.LineString, JDBCType.ARRAY); + map.put(ClickHouseDataType.MultiPolygon, JDBCType.ARRAY); + map.put(ClickHouseDataType.MultiLineString, JDBCType.ARRAY); return ImmutableMap.copyOf(map); } @@ -281,6 +282,8 @@ public static Object convert(Object value, Class type, ClickHouseColumn colum } // base type is unknown. all objects should be converted return new Array(column, ((List) value).toArray()); + } else if (type == java.sql.Array.class && value.getClass().isArray()) { + return new Array(column, arrayToObjectArray(value)); } else if (type == Inet4Address.class && value instanceof Inet6Address) { // Convert Inet6Address to Inet4Address return InetAddressConverter.convertToIpv4((InetAddress) value); @@ -322,4 +325,75 @@ public static Object[] convertArray(Object[] values, Class type) throws SQLEx } return convertedValues; } + + private static Object[] arrayToObjectArray(Object array) { + if (array == null) { + return null; + } + if (array instanceof Object[]) { + return (Object[]) array; + } + if (!array.getClass().isArray()) { + throw new IllegalArgumentException("Not an array: " + array.getClass().getName()); + } + + if (array instanceof byte[]) { + byte[] src = (byte[]) array; + Object[] dst = new Object[src.length]; + for (int i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + return dst; + } else if (array instanceof short[]) { + short[] src = (short[]) array; + Object[] dst = new Object[src.length]; + for (int i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + return dst; + } else if (array instanceof int[]) { + int[] src = (int[]) array; + Object[] dst = new Object[src.length]; + for (int i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + return dst; + } else if (array instanceof long[]) { + long[] src = (long[]) array; + Object[] dst = new Object[src.length]; + for (int i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + return dst; + } else if (array instanceof float[]) { + float[] src = (float[]) array; + Object[] dst = new Object[src.length]; + for (int i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + return dst; + } else if (array instanceof double[]) { + double[] src = (double[]) array; + Object[] dst = new Object[src.length]; + for (int i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + return dst; + } else if (array instanceof char[]) { + char[] src = (char[]) array; + Object[] dst = new Object[src.length]; + for (int i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + return dst; + } else if (array instanceof boolean[]) { + boolean[] src = (boolean[]) array; + Object[] dst = new Object[src.length]; + for (int i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + return dst; + } + throw new IllegalArgumentException("Cannot convert " + array.getClass().getName() + " to an Object[]"); + } } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java index 610501b69..fc61feebd 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java @@ -118,4 +118,16 @@ private void ensureValid() throws SQLException { throw ExceptionUtils.toSqlState(new SQLFeatureNotSupportedException("Array is not valid. Possible free() was called.")); } } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Array other = (Array) obj; + return type == other.type && java.util.Arrays.equals(array, other.array); + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java index bb958adb4..3ade33d73 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java @@ -2,14 +2,13 @@ import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.client.api.DataTypeUtils; -import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -29,6 +28,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; +import java.sql.Types; import java.text.DecimalFormat; import java.time.Instant; import java.time.LocalDate; @@ -50,7 +50,6 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; @@ -1651,4 +1650,314 @@ public void testVariantTypesSimpleStatement() throws SQLException { } } } + + @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); + } + } + } + }