diff --git a/docs/client-v2-json-support.md b/docs/client-v2-json-support.md index ac34027c3..8893ab43f 100644 --- a/docs/client-v2-json-support.md +++ b/docs/client-v2-json-support.md @@ -352,6 +352,47 @@ Notes: ## Usage in `jdbc-v2` +### `FORMAT JSON` output + +ClickHouse `FORMAT JSON` produces a JSON value with a ClickHouse-specific +structure. Its `meta` array describes columns, while its `data` array contains +result rows; row counts and statistics are provided separately. It is not +mapped to a JDBC `ResultSet` by `Statement.executeQuery(...)`; callers that +need this output should use the underlying `client-v2` instance exposed by the +JDBC connection and parse the `QueryResponse` stream with a JSON library. + +```java +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +ObjectMapper mapper = new ObjectMapper(); + +try (Connection conn = DriverManager.getConnection( + "jdbc:clickhouse://localhost:8123/default", props); + QueryResponse response = conn.unwrap(ConnectionImpl.class) + .getClient() + .query("SELECT 1 AS x FORMAT JSON") + .get()) { + JsonNode result = mapper.readTree(response.getInputStream()); + JsonNode meta = result.required("meta"); + JsonNode data = result.required("data"); + + String columnName = meta.get(0).required("name").asText(); + int x = data.get(0).required("x").asInt(); +} +``` + +The example reads directly from the response `InputStream` without copying the +JSON response into a `String`. For large responses, use Jackson's streaming +`JsonParser` to process the `data` array one row at a time after reading the +`meta` array. + +The returned `Client` is owned by the JDBC connection. Close each +`QueryResponse`, as shown above, but do not close the client returned by +`ConnectionImpl#getClient()`. + +### Row-oriented JSONEachRow output + The output format is selected by appending `FORMAT JSONEachRow` to the SQL statement. The driver does not rewrite the SQL and does not apply a default format on the caller's behalf. diff --git a/docs/features.md b/docs/features.md index 11a0e4ac4..4eedf6ae3 100644 --- a/docs/features.md +++ b/docs/features.md @@ -57,6 +57,7 @@ Compatibility-sensitive traits: - Driver and client properties: Separates JDBC-specific properties from passthrough client options used by the underlying `client-v2` transport. - DataSource support: Provides a JDBC `DataSource` implementation backed by the same driver configuration model. - Connection lifecycle: Supports connection close, validity checks, ping-based health checks, and network timeout management. +- Underlying client access: `ConnectionImpl#getClient()` exposes the connection-owned `client-v2` instance for operations that are not representable through the JDBC API, including direct consumption of raw response streams. - Schema and database context: Supports database selection through URL, `setSchema`, `USE`, and statement-level settings. - Non-transactional operation: Exposes ClickHouse-appropriate transaction behavior with auto-commit semantics and unsupported transactional features. - Statement execution: Supports `execute`, `executeQuery`, `executeUpdate`, large update counts, and forward-only/read-only statements. @@ -87,6 +88,7 @@ Compatibility-sensitive traits: - `Geometry` has a stable JDBC mapping: metadata reports SQL type `ARRAY` with type name `Geometry`, read paths return nested Java arrays rather than custom wrappers, and write paths depend on the caller preserving the intended point/array nesting shape. - JDBC `Geometry` writes share the same ambiguity as the client serializer: variant selection is inferred from nesting depth, so `Ring` versus `LineString` and `Polygon` versus `MultiLineString` are not currently distinguishable when writing through the generic `Geometry` path. - JDBC `FORMAT JSONEachRow` support is opt-in through the `jdbc_json_parser_factory` driver property, whose value must be a fully-qualified `JsonParserFactory` class name with a public no-argument constructor; JSONEachRow numeric and structured value behavior follows the selected parser and configured server output settings. Inferred JSON arrays are returned from `ResultSet.getObject(...)` as parser-native `List` values rather than JDBC `Array` values because JSONEachRow does not include element metadata. JDBC temporal typed accessors such as `getTimestamp(...)` are not guaranteed for JSONEachRow result sets; callers that need stable JDBC temporal conversions should use the binary default format or perform application-level conversion from string/object values. +- Standard `FORMAT JSON` output has ClickHouse-specific `meta` and `data` fields and is not exposed as a JDBC `ResultSet`. JDBC callers that need it should unwrap to `ConnectionImpl`, call `getClient()`, and parse the `QueryResponse` stream directly. - Binary parameters passed through `setBytes()` are encoded as ClickHouse `unhex(...)` expressions rather than text literals; empty byte arrays map to an empty string expression. - Stream and reader setters (`setAsciiStream`, `setUnicodeStream`, `setBinaryStream`, `setCharacterStream`, `setNCharacterStream`) are treated as text input encoded with the same string-escaping rules, including length-based truncation when a length is supplied. - `getString()` formatting for temporal values is stable output: `Date` uses `yyyy-MM-dd`, `DateTime` uses `yyyy-MM-dd HH:mm:ss`, and `DateTime64` preserves fractional precision, all interpreted in server timezone context where applicable. diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java index 791ee0f49..7f09295cb 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -706,8 +706,15 @@ public int getNetworkTimeout() throws SQLException { } /** - * Returns instance of the client used to execute queries by this connection. - * @return - client instance + * Returns the {@link Client} instance used by this connection. + *
+ * This can be used for operations that are not representable through the JDBC + * API, such as consuming raw or format-specific response formats directly + * from {@link com.clickhouse.client.api.query.QueryResponse#getInputStream()}. + * The returned client is owned by this connection and must not be closed by + * callers; closing the connection closes the client. + * + * @return client instance */ public Client getClient() throws SQLException { ensureOpen(); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java index 1b3d5cdae..d52adc967 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java @@ -8,6 +8,10 @@ import com.clickhouse.client.api.DataTypeUtils; import com.clickhouse.client.api.ServerException; import com.clickhouse.client.api.internal.ServerSettings; +import com.clickhouse.client.api.query.QueryResponse; +import com.clickhouse.data.ClickHouseFormat; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.common.ConsoleNotifier; @@ -921,11 +925,31 @@ public void testUnwrapping() throws Exception { Connection conn = getJdbcConnection(); Assert.assertTrue(conn.isWrapperFor(Connection.class)); Assert.assertTrue(conn.isWrapperFor(JdbcV2Wrapper.class)); + Assert.assertTrue(conn.isWrapperFor(ConnectionImpl.class)); Assert.assertEquals(conn.unwrap(Connection.class), conn); Assert.assertEquals(conn.unwrap(JdbcV2Wrapper.class), conn); + Assert.assertEquals(conn.unwrap(ConnectionImpl.class), conn); assertThrows(SQLException.class, () -> conn.unwrap(ResultSet.class)); } + @Test(groups = { "integration" }) + public void testRawJSONQueryThroughUnderlyingClient() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + try (Connection conn = getJdbcConnection(); + QueryResponse response = conn.unwrap(ConnectionImpl.class).getClient() + .query("SELECT 1 AS x FORMAT JSON") + .get()) { + assertEquals(response.getFormat(), ClickHouseFormat.JSON); + + JsonNode output = mapper.readTree(response.getInputStream()); + JsonNode meta = output.required("meta"); + JsonNode data = output.required("data"); + assertEquals(meta.get(0).required("name").asText(), "x"); + assertEquals(data.get(0).required("x").asInt(), 1); + assertEquals(output.required("rows").asInt(), 1); + } + } + @Test(groups = { "integration" }) public void testBearerTokenAuth() throws Exception { if (isCloud()) {