Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/client-v2-json-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
11 changes: 9 additions & 2 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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();
Expand Down
24 changes: 24 additions & 0 deletions jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Comment thread
chernser marked this conversation as resolved.

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()) {
Expand Down
Loading