From 84f9e7eccb9b67c92dc276bad9972ad6b192697b Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 10 Sep 2025 13:17:22 -0700 Subject: [PATCH] Fixed spatial data returning correct type and object --- .../jdbc/ClickHouseStatementTest.java | 33 +++++++ .../com/clickhouse/jdbc/ResultSetImpl.java | 95 +++++++++++-------- .../clickhouse/jdbc/internal/JdbcUtils.java | 86 +++++++++++++++-- .../java/com/clickhouse/jdbc/types/Array.java | 12 +++ .../com/clickhouse/jdbc/DataTypeTests.java | 50 ++++++++++ 5 files changed, 228 insertions(+), 48 deletions(-) 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..4d19e9c06 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; @@ -1554,4 +1555,36 @@ public void testVariantDataType() throws SQLException { } } } + + @Test(groups = "integration") + public void testSpatialData() { + final String spatialQuery = "select \n" + + "\tcast(arrayJoin([(4.837388, 52.38795),\n" + + "\t\t\t(4.951513, 52.354582),\n" + + "\t\t\t(4.961987, 52.371763),\n" + + "\t\t\t(4.870017, 52.334932),\n" + + "\t\t\t(4.89813, 52.357238),\n" + + "\t\t\t(4.852437, 52.370315),\n" + + "\t\t\t(4.901712, 52.369567),\n" + + "\t\t\t(4.874112, 52.339823),\n" + + "\t\t\t(4.856942, 52.339122),\n" + + "\t\t\t(4.870253, 52.360353)]\n" + + "\t\t\t)\n" + + "\t\tas Point) as Point"; + try (ClickHouseConnection conn = newConnection(); + ClickHouseStatement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery(spatialQuery); + rs.next(); + ResultSetMetaData metaData = rs.getMetaData(); + String columnTypeName = metaData.getColumnTypeName(1); + int columnType = metaData.getColumnType(1); + Array asArray = rs.getArray(1); + Object asObject = rs.getObject(1); + String asString = rs.getString(1); + Assert.assertEquals(metaData.getColumnCount(), 7); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("Failed to create connection", e); + } + } } 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 d7bf05525..74a8cde7f 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; @@ -16,7 +16,6 @@ import java.io.Reader; import java.io.StringReader; import java.math.BigDecimal; -import java.net.SocketTimeoutException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.sql.Blob; @@ -36,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; @@ -465,16 +465,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(); @@ -945,12 +935,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)); @@ -971,12 +955,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(); @@ -1420,38 +1398,71 @@ 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 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..574270d22 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java @@ -29,6 +29,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; @@ -1651,4 +1652,53 @@ public void testVariantTypesSimpleStatement() throws SQLException { } } } + + @Test(groups = { "integration" }) + public void testSpatialData() 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), "Point"); + 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); + } + } + } }