diff --git a/docs/clickhouse-docs/jdbc.mdx b/docs/clickhouse-docs/jdbc.mdx index d24a75e97..7d12793ae 100644 --- a/docs/clickhouse-docs/jdbc.mdx +++ b/docs/clickhouse-docs/jdbc.mdx @@ -619,14 +619,14 @@ try (Connection conn = ...) { |-----------------------------|------------------------------|-----------------------------| | UUID | OTHER | java.util.UUID | | IPv4 | OTHER | java.net.Inet4Address | -| IPv6 | OTHER | java.net.Inet6Address | +| IPv6 | OTHER | java.net.InetAddress | | JSON | OTHER | java.lang.String | | AggregateFunction | OTHER | (binary representation) | | SimpleAggregateFunction | (wrapped type) | (wrapped class) | - `UUID` isn't JDBC standard type. However it is part of JDK. By default `java.util.UUID` is returned on `getObject()` method. - `UUID` can be read/written as `String` by using `getObject(columnIndex, String.class)` method. -- `IPv4` and `IPv6` aren't JDBC standard types. However they're part of JDK. By default `java.net.Inet4Address` and `java.net.Inet6Address` are returned on `getObject()` method. +- `IPv4` and `IPv6` aren't JDBC standard types. However they're part of JDK. By default `java.net.Inet4Address` and `java.net.Inet6Address` are returned on `getObject()` method. An IPv4-mapped `IPv6` value (`::ffff:a.b.c.d`) is returned as a `java.net.Inet4Address`. For this reason `ResultSetMetaData#getColumnClassName` reports `java.net.InetAddress` for `IPv6`. - `IPv4` and `IPv6` can be read/written as `String` by using `getObject(columnIndex, String.class)` method. **JSON Type** @@ -1023,12 +1023,13 @@ try (Connection conn = DriverManager.getConnection(url, properties)) { | SimpleAggregateFunction | ✅ | (wrapped type) | (wrapped class) | (wrapped type) | (wrapped class) | | UUID | ✅ | OTHER | java.util.UUID | VARCHAR | java.util.UUID | | IPv4 | ✅ | OTHER | java.net.Inet4Address | VARCHAR | java.net.Inet4Address | -| IPv6 | ✅ | OTHER | java.net.Inet6Address | VARCHAR | java.net.Inet6Address | +| IPv6 | ❌ | OTHER | java.net.InetAddress | VARCHAR | java.net.Inet6Address | | Dynamic | ❌ | OTHER | java.Object | not supported | not supported | | Variant | ❌ | OTHER | java.Object | not supported | not supported | - V1 uses `VARCHAR` for `UUID` but returns `java.util.UUID` object always. V2 fixes this by mapping `UUID` to `OTHER` and returns `java.util.UUID` object. - V1 uses `VARCHAR` for `IPv4` and `IPv6` but returns `java.net.Inet4Address` and `java.net.Inet6Address` objects always. V2 fixes this by mapping `IPv4` and `IPv6` to `OTHER` and returns `java.net.Inet4Address` and `java.net.Inet6Address` objects. +- V2 returns an IPv4-mapped `IPv6` value (`::ffff:a.b.c.d`) as a `java.net.Inet4Address`, V1 returns it as a `java.net.Inet6Address`. Use `getObject(columnIndex, Inet6Address.class)` to always get a `java.net.Inet6Address`. - `Dynamic` and `Variant` are new types in V2. Not supported in V1. - `JSON` is based on `Dynamic` type. Therefore it is supported only in V2. - IPv4 and IPv6 values can be read as `byte[]` by using `getBytes(columnIndex)` method. However it is recommended to use designated classes for these types. diff --git a/docs/features.md b/docs/features.md index ca416e24e..437fc783e 100644 --- a/docs/features.md +++ b/docs/features.md @@ -98,6 +98,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. An `Array(Nested(...))` column is read as a JDBC `ARRAY` whose elements are the nested values, each an array of tuples (`Object[]` of `Object[]`); its `getResultSet()` returns each element as a `java.sql.Array` equal to a top-level `Nested(...)` value. - 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. The `client_id_metadata` connection property is forwarded to the client and adds key/value pairs to the `User-Agent` header (see the `client-v2` `User-Agent` entry). - 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 d49a69be6..38bf7c283 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 81b50dbb0..907fe3949 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));