Skip to content

Commit 4c427f6

Browse files
committed
fix(jdbc-v2): support explicit FORMAT JSON queries
1 parent fe86ee2 commit 4c427f6

5 files changed

Lines changed: 74 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
### Bug Fixes
66

7+
- **[jdbc-v2]** Fixed explicit `FORMAT JSON` queries failing in `executeQuery()` with an unsupported output-format
8+
error. The driver now exposes the server-rendered JSON as a streaming, single-column `results` string `ResultSet`,
9+
with one result row per response line, matching the legacy JDBC behavior. Queries without an explicit text format
10+
continue to use the typed `RowBinaryWithNamesAndTypes` path. (https://github.com/ClickHouse/clickhouse-java/issues/2715)
11+
712
- **[client-v2]** Fixed binary varint decoding for length and count fields so overflowing or overlong values fail with an `IOException` instead of being decoded into corrupted or negative `int` values. (https://github.com/ClickHouse/clickhouse-java/issues/2902)
813

914
- **[client-v2]** Fixed container query parameters being sent unquoted, so `Client.query(sql, params, settings)` binding

docs/features.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Compatibility-sensitive traits:
6565
- Prepared statements: Supports `?` parameters through client-side SQL rendering and validates that all parameters are bound before execution.
6666
- SQL parsing and classification: Classifies SQL to distinguish queries, updates, inserts, `USE`, and role-changing statements, with selectable parser backends.
6767
- JDBC escape processing: Translates supported JDBC escape syntax for dates, timestamps, and functions before execution.
68-
- Result set streaming: Streams result sets from ClickHouse binary formats and `FORMAT JSONEachRow`, enforces max-row limits, and manages result-set lifecycle correctly.
68+
- Result set streaming: Streams result sets from ClickHouse binary formats and `FORMAT JSONEachRow`, and exposes explicit `FORMAT JSON` output as a line-oriented single-column string result set; enforces max-row limits and manages result-set lifecycle correctly.
6969
- Result-set metadata: Exposes JDBC `ResultSetMetaData` backed by ClickHouse column schema.
7070
- Database metadata: Implements JDBC `DatabaseMetaData` for ClickHouse catalogs, schemas, tables, columns, and related capability reporting.
7171
- Parameter metadata: Reports prepared-statement parameter counts.
@@ -87,6 +87,7 @@ Compatibility-sensitive traits:
8787
- `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.
8888
- 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.
8989
- 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.
90+
- JDBC `FORMAT JSON` preserves the server-rendered payload as a line-oriented, single-column `results` result set, with one JDBC row per response line. It does not reconstruct typed ClickHouse columns from the JSON document; callers that need typed column access should omit the explicit format and use the default binary result path.
9091
- Binary parameters passed through `setBytes()` are encoded as ClickHouse `unhex(...)` expressions rather than text literals; empty byte arrays map to an empty string expression.
9192
- 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.
9293
- `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.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.clickhouse.jdbc;
2+
3+
import com.clickhouse.client.api.data_formats.JSONEachRowFormatReader;
4+
import com.clickhouse.client.api.data_formats.JsonParser;
5+
6+
import java.io.BufferedReader;
7+
import java.io.InputStream;
8+
import java.io.InputStreamReader;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.Collections;
11+
import java.util.Map;
12+
13+
final class LineAsStringFormatReader extends JSONEachRowFormatReader {
14+
static final String COLUMN_NAME = "results";
15+
16+
LineAsStringFormatReader(InputStream input) {
17+
super(new LineParser(input));
18+
}
19+
20+
private static final class LineParser implements JsonParser {
21+
private final BufferedReader reader;
22+
23+
private LineParser(InputStream input) {
24+
this.reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
25+
}
26+
27+
@Override
28+
public Map<String, Object> nextRow() throws Exception {
29+
String line = reader.readLine();
30+
return line == null ? null : Collections.<String, Object>singletonMap(COLUMN_NAME, line);
31+
}
32+
33+
@Override
34+
public void close() throws Exception {
35+
reader.close();
36+
}
37+
}
38+
}

jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr
180180
}
181181

182182
ClickHouseFormatReader reader;
183-
if (response.getFormat() == ClickHouseFormat.JSONEachRow) {
183+
if (response.getFormat() == ClickHouseFormat.JSON) {
184+
reader = new LineAsStringFormatReader(response.getInputStream());
185+
} else if (response.getFormat() == ClickHouseFormat.JSONEachRow) {
184186
if (connection.getJsonParserFactory() == null) {
185187
throw new SQLException("Response is in JSONEachRow format, but " +
186188
DriverProperties.JSON_PARSER_FACTORY.getKey() + " is not configured. Set " +
@@ -191,7 +193,7 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr
191193
} else if (!response.getFormat().isText()) {
192194
reader = connection.getClient().newBinaryFormatReader(response);
193195
} else {
194-
throw new SQLException("Only RowBinaryWithNameAndTypes and JSONEachRow are supported for output format. Please check your query.",
196+
throw new SQLException("Only RowBinaryWithNamesAndTypes, JSON, and JSONEachRow are supported for output format. Please check your query.",
195197
ExceptionUtils.SQL_STATE_CLIENT_ERROR);
196198
}
197199

jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.sql.Array;
2121
import java.sql.Connection;
2222
import java.sql.Date;
23+
import java.sql.PreparedStatement;
2324
import java.sql.ResultSet;
2425
import java.sql.ResultSetMetaData;
2526
import java.sql.SQLException;
@@ -862,10 +863,30 @@ public void testCancelInsertWithSession() throws Exception {
862863
}
863864

864865
@Test(groups = {"integration"})
865-
public void testTextFormatInResponse() throws Exception {
866-
try (Connection conn = getJdbcConnection();
867-
Statement stmt = conn.createStatement()) {
868-
Assert.expectThrows(SQLException.class, () -> stmt.executeQuery("SELECT 1 FORMAT JSON"));
866+
public void testJSONFormatInResponse() throws Exception {
867+
try (Connection conn = getJdbcConnection()) {
868+
try (PreparedStatement stmt = conn.prepareStatement("SELECT 1 AS x FORMAT JSON");
869+
ResultSet rs = stmt.executeQuery()) {
870+
assertEquals(rs.getMetaData().getColumnCount(), 1);
871+
assertEquals(rs.getMetaData().getColumnLabel(1), LineAsStringFormatReader.COLUMN_NAME);
872+
873+
StringBuilder json = new StringBuilder();
874+
while (rs.next()) {
875+
json.append(rs.getString(1)).append('\n');
876+
}
877+
878+
String output = json.toString();
879+
assertTrue(output.contains("\"data\""), output);
880+
assertTrue(output.contains("\"x\""), output);
881+
assertTrue(output.contains("\"rows\""), output);
882+
}
883+
884+
try (Statement stmt = conn.createStatement();
885+
ResultSet rs = stmt.executeQuery("SELECT 2 AS y")) {
886+
assertTrue(rs.next());
887+
assertEquals(rs.getInt("y"), 2);
888+
assertFalse(rs.next());
889+
}
869890
}
870891
}
871892

0 commit comments

Comments
 (0)