From aa84f98570cca635f253bea8d216def1db89e7ac Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:39:32 +0000 Subject: [PATCH] Fix jdbc-v2: report the IPv4/IPv6 Java class in getColumnClassName ResultSetMetaData#getColumnClassName returned java.lang.Object for IPv4 and IPv6 columns because JdbcUtils.DATA_TYPE_CLASS_MAP mapped both types to Object. IPv4 now maps to java.net.Inet4Address and IPv6 to java.net.InetAddress: the reader returns an Inet4Address for an IPv4-mapped IPv6 value, so InetAddress is the class that getObject actually returns. getObject returns the same values as before. ArrayResultSet.convertValue used an exact class match, so an array element could not be returned for a superclass target such as InetAddress. It now uses isInstance, the same as JdbcUtils.convert. Fixes: https://github.com/ClickHouse/clickhouse-java/issues/3189 Co-Authored-By: Claude Opus 5.5 --- docs/features.md | 1 + history/latest/3189.md | 9 ++++ .../clickhouse/jdbc/internal/JdbcUtils.java | 5 +++ .../clickhouse/jdbc/types/ArrayResultSet.java | 2 +- .../metadata/ResultSetMetaDataImplTest.java | 43 +++++++++++++++++++ .../jdbc/types/ArrayResultSetTest.java | 20 +++++++++ 6 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 history/latest/3189.md diff --git a/docs/features.md b/docs/features.md index b8b2b24a8..d34bd4cf5 100644 --- a/docs/features.md +++ b/docs/features.md @@ -97,6 +97,7 @@ Compatibility-sensitive traits: - Nested columns: Un-flattened `Nested(f1 T1, ..., fN TN)` columns (tables created with `flatten_nested = 0`) are exposed as JDBC `ARRAY` whose element type is `Tuple(f1 T1, ..., fN TN)`. They can be inserted through `Connection#createArrayOf`/`setArray` or `setObject` (a Java array of tuples) and read back through `getArray`/`getObject`; `java.sql.Array#getResultSet()` iterates the nested rows as `(INDEX, VALUE)` pairs where each `VALUE` is the tuple. - Geometry type mapping: For ClickHouse `25.11+`, where `Geometry` changed from a string alias to `Variant(Point, Ring, LineString, MultiLineString, Polygon, MultiPolygon)`, JDBC exposes `Geometry` as `ARRAY`, returns nested Java arrays from `getObject()`/`getArray()`, and accepts `Struct` or nested `Array` inputs for prepared-statement inserts depending on the geometry shape. For ClickHouse `26.8+` the variant also carries `MultiPoint`. - MultiPoint type mapping: For ClickHouse `26.8+`, JDBC exposes `MultiPoint` as `ARRAY` with type name `MultiPoint`, returns `double[][]` from `getObject()` and a `java.sql.Array` from `getArray()`, and accepts a nested `Array` (`createArrayOf("Array(Point)", ...)`) for prepared-statement inserts, the same as `Ring` and `LineString`. +- IP address type mapping: JDBC exposes `IPv4` and `IPv6` as `OTHER`. `getObject()` returns a `java.net.Inet4Address` for `IPv4` and a `java.net.InetAddress` for `IPv6` (an `Inet6Address`, or an `Inet4Address` for an IPv4-mapped value such as `::ffff:10.0.0.1`). `ResultSetMetaData#getColumnClassName` reports `java.net.Inet4Address` and `java.net.InetAddress` respectively, also for the `VALUE` column of `java.sql.Array#getResultSet()`, and `java.sql.Array#getArray()` returns an `Inet4Address[]` or an `InetAddress[]`. - Client info propagation: Supports JDBC client info such as `ApplicationName` and forwards it to the underlying client name. - Wrapper support: Implements standard JDBC `Wrapper` and `unwrap` behavior on major JDBC objects. - Packaging and runtime compatibility: Ships as a JDBC 4.2 driver, depends on `client-v2`, and includes native-image metadata for GraalVM users. diff --git a/history/latest/3189.md b/history/latest/3189.md new file mode 100644 index 000000000..671009c37 --- /dev/null +++ b/history/latest/3189.md @@ -0,0 +1,9 @@ +- **[jdbc-v2]** Fixed `ResultSetMetaData#getColumnClassName` for `IPv4` and `IPv6` columns, which returned + `java.lang.Object`. Now `IPv4` reports `java.net.Inet4Address` and `IPv6` reports `java.net.InetAddress`, the + classes of the values that `ResultSet#getObject` returns. `IPv6` reports `InetAddress` (not `Inet6Address`) because + an IPv4-mapped value (`::ffff:a.b.c.d`) is returned as an `Inet4Address`. `getColumnType` stays `Types.OTHER`, and + `ResultSet#getObject` returns the same values as before. Array elements use the same classes: the `VALUE` column of + `java.sql.Array#getResultSet()` reports the same class name, and `java.sql.Array#getArray()` now returns an + `Inet4Address[]` / `InetAddress[]` instead of an `Object[]`. `getObject(int, Class)` on the result set of a + `java.sql.Array` now also accepts a superclass or an interface of the element value (for example, + `InetAddress.class`). (https://github.com/ClickHouse/clickhouse-java/issues/3189) 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 97c323911..bb7db1829 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 @@ -209,7 +209,12 @@ private static Map> getDataTypeClassMap() { map.put(e.getKey(), UUID.class); break; case IPv4: + map.put(e.getKey(), Inet4Address.class); + break; case IPv6: + // IPv4-mapped values (::ffff:a.b.c.d) are read as Inet4Address. + map.put(e.getKey(), InetAddress.class); + break; case Geometry: // should be mapped to Object because require conversion. default: diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index 1f63ad3c8..f836b2d7e 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -130,7 +130,7 @@ private Map, Function> initValueConverterMapIfNeeded(Ob } private Object convertValue(Object value, Class targetType, Map, Function> valueConverterMap) throws SQLException { - if (value == null || targetType == value.getClass() || targetType == Object.class) { + if (value == null || (targetType != null && targetType.isInstance(value))) { return value; } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImplTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImplTest.java index 41313b357..58378fad9 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImplTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImplTest.java @@ -5,6 +5,9 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; import java.sql.Connection; import java.sql.ResultSet; import java.sql.ResultSetMetaData; @@ -268,6 +271,46 @@ public void testGetPrecisionAndScaleOfWrappedTypes(String expression, int precis } } + @DataProvider(name = "ipAddressTypes") + public static Object[][] ipAddressTypesProvider() { + return new Object[][] { + { "toIPv4('10.0.0.1')", "IPv4", Inet4Address.class, Inet4Address.class }, + { "toNullable(toIPv4('10.0.0.1'))", "Nullable(IPv4)", Inet4Address.class, Inet4Address.class }, + { "toLowCardinality(toIPv4('10.0.0.1'))", "LowCardinality(IPv4)", Inet4Address.class, Inet4Address.class }, + { "toIPv6('2001:db8::1')", "IPv6", InetAddress.class, Inet6Address.class }, + { "toNullable(toIPv6('2001:db8::1'))", "Nullable(IPv6)", InetAddress.class, Inet6Address.class }, + { "toLowCardinality(toIPv6('2001:db8::1'))", "LowCardinality(IPv6)", InetAddress.class, Inet6Address.class }, + { "toIPv6('::ffff:10.0.0.1')", "IPv6", InetAddress.class, Inet4Address.class }, + }; + } + + @Test(groups = { "integration" }, dataProvider = "ipAddressTypes") + public void testGetColumnClassNameOfIpAddressTypes(String expression, String typeName, Class columnClass, + Class valueClass) throws Exception { + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT " + expression + " AS ip, [" + expression + "] AS ips")) { + ResultSetMetaData rsmd = rs.getMetaData(); + assertEquals(rsmd.getColumnTypeName(1), typeName); + assertEquals(rsmd.getColumnType(1), Types.OTHER); + assertEquals(rsmd.getColumnClassName(1), columnClass.getName()); + + assertTrue(rs.next()); + Object value = rs.getObject(1); + assertEquals(value.getClass(), valueClass); + assertTrue(Class.forName(rsmd.getColumnClassName(1)).isInstance(value)); + + ResultSet elements = rs.getArray(2).getResultSet(); + ResultSetMetaData elementsMetaData = elements.getMetaData(); + assertEquals(elementsMetaData.getColumnTypeName(2), typeName); + assertEquals(elementsMetaData.getColumnType(2), Types.OTHER); + assertEquals(elementsMetaData.getColumnClassName(2), columnClass.getName()); + assertTrue(elements.next()); + assertEquals(elements.getObject(2), value); + assertEquals(elements.getObject(2).getClass(), valueClass); + } + } + @Test(groups = { "integration" }) public void testColumnNamesStrippedFromTablePrefix() throws Exception { final String t1 = "rsmd_test_prefix_t1"; diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index cbae5d169..87b7e14d1 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -10,6 +10,8 @@ import java.io.StringReader; import java.lang.reflect.Array; import java.math.BigDecimal; +import java.net.Inet4Address; +import java.net.InetAddress; import java.net.URL; import java.sql.Blob; import java.sql.Clob; @@ -297,6 +299,24 @@ void testStringValues() throws SQLException { } } + @Test + void testInetAddressValues() throws Exception { + InetAddress[] array = new InetAddress[] { + InetAddress.getByName("2001:db8::1"), InetAddress.getByName("10.0.0.1"), null}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Nullable(IPv6))").get(0)); + + assertEquals(rs.getMetaData().getColumnClassName(2), InetAddress.class.getName()); + for (InetAddress value : array) { + assertTrue(rs.next()); + assertEquals(rs.getObject(2), value); + assertEquals(rs.getObject(2, InetAddress.class), value); + assertEquals(rs.getObject(2, Object.class), value); + } + + rs.absolute(1); + expectThrows(SQLException.class, () -> rs.getObject(2, Inet4Address.class)); + } + @Test void testEmptyArray() throws SQLException { ArrayResultSet rs = new ArrayResultSet(new Object[0], ClickHouseColumn.parse("v Array(Int32)").get(0));