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));