diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eff8d530..dd5114b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,9 @@ ### Bug Fixes +- **[jdbc-v2]** Fixes issue with `FORMAT` in query unable to override format set by client when used with ClickHouse 26.8+ + Default format is `RowBinaryWithNamesAndTypes` as before but set on client level and can be set to `null` to rely on + query `FORMAT` clause. (https://github.com/ClickHouse/clickhouse-java/issues/3086) - **[jdbc-v2]** Fixed `DatabaseMetaData#getTables` reporting `TABLE_TYPE = TABLE` for a table with the `BigQuery` engine (present in `system.table_engines` since ClickHouse `26.8`). The engine was missing from the engine-to-table-type mapping, so it fell back to the default `TABLE`, and `getTables(..., types = {"REMOTE TABLE"})` diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index dbe29a268..bf98524df 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -59,6 +59,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -90,8 +91,6 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -import javax.net.ssl.SSLContext; - /** *

Client is the starting point for all interactions with ClickHouse.

* @@ -1301,6 +1300,17 @@ public Builder setMetricsRecorder(MetricsRecorder metricsRecorder) { return this; } + /** + * Sets default format used when no format is specified in {@code QuerySettings}. + * + * @param format - valid ClickHouse format + * @return this instance of builder + */ + public Builder queryFormat(String format) { + this.setOption(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), ClickHouseFormat.valueOf(format).name()); + return this; + } + public Client build() { // check if endpoint are empty. so can not initiate client if (this.endpoints.isEmpty()) { @@ -1908,9 +1918,6 @@ public CompletableFuture query(String sqlQuery, Map parseConfigMap(Map configMap) default: parsedValue = config.parseValue(value); } - parsedConfig.put(config.getKey(), parsedValue); + if (parsedValue != null) { + parsedConfig.put(config.getKey(), parsedValue); + } } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java index 54d53eec9..8e85f9b8d 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java @@ -872,10 +872,13 @@ private void logServerErrorResponse(HttpPost req, ClassicHttpResponse httpRespon private void addHeaders(HttpPost req, Map requestConfig) { setHeader(req, HttpHeaders.CONTENT_TYPE, CONTENT_TYPE.getMimeType()); if (requestConfig.containsKey(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey())) { - setHeader( - req, - ClickHouseHttpProto.HEADER_FORMAT, - ((ClickHouseFormat) requestConfig.get(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey())).name()); + ClickHouseFormat format = (ClickHouseFormat) requestConfig.get(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey()); + if (format != null) { + setHeader( + req, + ClickHouseHttpProto.HEADER_FORMAT, + format.name()); + } } if (requestConfig.containsKey(ClientConfigProperties.QUERY_ID.getKey())) { setHeader( diff --git a/client-v2/src/main/java/com/clickhouse/client/api/query/QueryResponse.java b/client-v2/src/main/java/com/clickhouse/client/api/query/QueryResponse.java index 6c0de2e3e..2858f85ab 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/query/QueryResponse.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/query/QueryResponse.java @@ -82,6 +82,12 @@ public void close() throws Exception { } } + /** + * Returns format of the date stream accessible via {@link #getInputStream()} + * This format is set from server response header `X-ClickHouse-Format`. + * + * @return ClickHouseFormat - format matching server response format. + */ public ClickHouseFormat getFormat() { return format; } diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index b50d3700b..a1c9299b9 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -333,7 +333,7 @@ public void testDefaultSettings() { Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); } } - Assert.assertEquals(config.size(), 37); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 38); // to check everything is set. Increment when new added. } try (Client client = new Client.Builder() @@ -365,9 +365,10 @@ public void testDefaultSettings() { .setSocketRcvbuf(100000) .setSocketSndbuf(100000) .binaryStringSupport(true) + .queryFormat(ClickHouseFormat.CSV.name()) .build()) { Map config = client.getConfiguration(); - Assert.assertEquals(config.size(), 38); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 39); // to check everything is set. Increment when new added. Assert.assertEquals(config.get(ClientConfigProperties.DATABASE.getKey()), "mydb"); Assert.assertEquals(config.get(ClientConfigProperties.MAX_EXECUTION_TIME.getKey()), "10"); Assert.assertEquals(config.get(ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getKey()), "300000"); @@ -393,7 +394,7 @@ public void testDefaultSettings() { Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_SNDBUF_OPT.getKey()), "100000"); Assert.assertEquals(config.get(ClientConfigProperties.SSL_MODE.getKey()), "STRICT"); Assert.assertEquals(config.get(ClientConfigProperties.BINARY_STRING_SUPPORT.getKey()), "true"); - + Assert.assertEquals(config.get(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey()), "CSV"); } } @@ -437,7 +438,7 @@ public void testWithOldDefaults() { Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); } } - Assert.assertEquals(config.size(), 37); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 38); // to check everything is set. Increment when new added. } } diff --git a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java index 1a00d9348..a768f3847 100644 --- a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java @@ -2337,15 +2337,38 @@ public void testEmptyResponse() throws Exception { @Test(groups = {"integration"}) public void testSettingsNotChanged() throws Exception{ - final QuerySettings settings = Mockito.spy(new QuerySettings()); - try (QueryResponse response = client.query("select 1 FORMAT JSONEachRow", settings).get()) { + final QuerySettings settings = Mockito.spy(new QuerySettings().setFormat(ClickHouseFormat.JSONEachRow)); + try (QueryResponse response = client.query("select 1", settings).get()) { Mockito.verify(settings, Mockito.times(1)).getAllSettings(); Mockito.verifyNoMoreInteractions(settings); - Assert.assertNull(settings.getFormat()); + Assert.assertEquals(settings.getFormat(), ClickHouseFormat.JSONEachRow); Assert.assertEquals(response.getFormat(), ClickHouseFormat.JSONEachRow); } } + @Test(groups = {"integration"}) + public void testFormatSelectionPrecedence() throws Exception { + // 1. Explicit QuerySettings format overrides client default + QuerySettings settingsFormat = new QuerySettings().setFormat(ClickHouseFormat.JSONEachRow); + try (QueryResponse response = client.query("SELECT 1 AS num", settingsFormat).get()) { + Assert.assertEquals(response.getFormat(), ClickHouseFormat.JSONEachRow); + } + + // 2. Default client format is RowBinaryWithNamesAndTypes + try (QueryResponse response = client.query("SELECT 1 AS num").get()) { + Assert.assertEquals(response.getFormat(), ClickHouseFormat.RowBinaryWithNamesAndTypes); + } + + // 3. Client configured with format set to null allows query SQL FORMAT clause to take effect + try (Client nullFormatClient = newClient() + .setOption(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), null) + .build()) { + try (QueryResponse response = nullFormatClient.query("SELECT 1 AS num FORMAT JSONEachRow").get()) { + Assert.assertEquals(response.getFormat(), ClickHouseFormat.JSONEachRow); + } + } + } + @Test public void testDuplicateColumnNames() throws Exception { { diff --git a/docs/clickhouse-docs/client.mdx b/docs/clickhouse-docs/client.mdx index 6a2ff98b2..4284bed3f 100644 --- a/docs/clickhouse-docs/client.mdx +++ b/docs/clickhouse-docs/client.mdx @@ -585,6 +585,22 @@ This object should be closed as soon as possible to release a connection because ## Query API {#query-api} +**Format Selection** + +Client provides transparent access to the response stream from server. User is free to choose any ClickHouse format in request and read data via `InputStream` +from `QueryResponse` object. +Format can be requested via: +- `QuerySettings#setFormat()` this will set format header in a request. +- In `FORMAT` clause in query itself. +- Server settings `default_format` (/reference/settings/session-settings/default#default_format) + +There are differences in behavior depending on ClickHouse and Client version: +- Client < `0.11.0` & ClickHouse < `26.8` - `FORMAT` clause has priority over request format header. +- Client > `0.11.0` & ClickHouse > `26.8` - request header has priority over `FORMAT` clause. + - Client sets header when no format is specified in `QuerySettings` - this doesn't allow to override format in query. +- Client starting `0.11.0` - sets default `format` value on client level and not on operation level. This allows existig code +work without changes and new code to use `FORMAT` clause by settings `format` on client to `null`. + ### query(String sqlQuery) {#querystring-sqlquery} Sends `sqlQuery` as is. Response format is set by query settings. `QueryResponse` will hold a reference to the response stream that should be consumed by a reader for the supportig format. diff --git a/docs/features.md b/docs/features.md index 308f694de..4b780db1e 100644 --- a/docs/features.md +++ b/docs/features.md @@ -14,7 +14,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t - Proxy support: Can send requests through configured HTTP proxies, including proxy credentials. - Connection and socket tuning: Exposes pool sizing, keep-alive, reuse strategy, connect/request/socket timeouts, and low-level socket options. - Query execution: Executes SQL asynchronously and returns streaming query responses with response metadata and metrics. -- Query settings: Supports per-query database selection, output format, execution limits, roles, log comments, headers, reusable `Session` objects, session settings, server settings, and network timeout overrides. Settings explicitly set to `null` will not be sent to the server. The output format must be set through the query settings (`QuerySettings#setFormat`) and not with a `FORMAT` clause in the query: the client always sends the format of the settings in the `X-ClickHouse-Format` header, and a `26.8+` server uses that header in preference to a `FORMAT` clause in the query. +- Query settings: Supports per-query database selection, output format, execution limits, roles, log comments, headers, reusable `Session` objects, session settings, server settings, and network timeout overrides. Settings explicitly set to `null` will not be sent to the server. The default format (`RowBinaryWithNamesAndTypes`) is configured at the client level (`ClientConfigProperties.INPUT_OUTPUT_FORMAT`), sending the `X-ClickHouse-Format` request header. ClickHouse `26.8+` uses that request header in preference to a `FORMAT` clause in the SQL query. To use a `FORMAT` clause in the query string or rely on the server `default_format`, set `format` to `null` (or empty) on the client, connection, or query settings so no format header is sent. - Parameterized SQL: Accepts named query parameters and can send them through supported HTTP request encodings. - Result materialization helpers: Provides streaming `Records`, generic row access, and convenience APIs that materialize all rows into generic records or typed POJOs. - Binary format readers: Reads ClickHouse binary result formats including `Native`, `RowBinary`, `RowBinaryWithNames`, and `RowBinaryWithNamesAndTypes`. @@ -107,7 +107,7 @@ Compatibility-sensitive traits: - Binary access to `String`/`FixedString` columns is compatibility-sensitive: `getBytes(...)` and `getBinaryStream(...)` expose the raw column bytes (not a re-encoded text literal), and a `NULL` column returns `null` with `wasNull()` reporting `true`. The `binary_string_support` connection property is passed through to the underlying `client-v2` transport. - `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. +- 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. To use `FORMAT JSONEachRow` in SQL queries with ClickHouse `26.8+`, configure `format=JSONEachRow` in connection properties or set `format=` (to `null`) so the default binary format request header does not override the SQL `FORMAT` clause. - 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. diff --git a/docs/integration-client.md b/docs/integration-client.md index 05df55e08..a6075ec71 100644 --- a/docs/integration-client.md +++ b/docs/integration-client.md @@ -492,6 +492,40 @@ Consider these trade-offs: Always pick the format that minimizes unnecessary transcoding in your application layer. +### Format Selection + +The client provides transparent access to the response stream from ClickHouse. You can request any supported ClickHouse format in your request and read data via the `InputStream` from the `QueryResponse` object. + +A response format can be specified in several ways: +- **`QuerySettings#setFormat(ClickHouseFormat format)`**: Sets the format header (`X-ClickHouse-Format`) for a specific query request. +- **`FORMAT` clause in SQL**: Appending `FORMAT ` directly in the SQL query string. +- **Client default setting**: The client sets a default `format` option (`ClientConfigProperties.INPUT_OUTPUT_FORMAT`, defaulting to `RowBinaryWithNamesAndTypes`) at the client level. +- **Server setting**: ClickHouse server session setting (`default_format`). + +**Precedence and Version Differences:** + +- **Client < 0.11.0 & ClickHouse < 26.8:** The `FORMAT` clause in the query string takes priority over the request format header. +- **Client >= 0.11.0 & ClickHouse >= 26.8:** The request format header (`X-ClickHouse-Format`) takes priority over the `FORMAT` clause in the query string. +- **Client 0.11.0+:** Default `format` is set at the client level rather than at the operation level. This allows existing code to work without changes, while new code can use a SQL `FORMAT` clause by setting `format` on the client or in `QuerySettings` to `null`. + +**Inspecting Server Response Format:** + +Use `QueryResponse#getFormat()` to inspect the format of the response data stream (resolved from the server `X-ClickHouse-Format` response header): + +```java +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.query.QueryResponse; +import com.clickhouse.client.api.query.QuerySettings; +import com.clickhouse.data.ClickHouseFormat; + +public ClickHouseFormat inspectQueryFormat(Client client, String sql) throws Exception { + QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.JSONEachRow); + try (QueryResponse response = client.query(sql, settings).get()) { + return response.getFormat(); + } +} +``` + ## Step 6 — Read operations & tuning **Goal:** read results efficiently and configure the operation-level settings for heavy analytical reads. diff --git a/docs/integration-jdbc.md b/docs/integration-jdbc.md index 4d35fa885..3c28b377e 100644 --- a/docs/integration-jdbc.md +++ b/docs/integration-jdbc.md @@ -402,18 +402,67 @@ public boolean checkConnectionHealth(Connection conn, int timeoutSeconds) throws ## Step 4 — Formats under the hood -**Goal:** understand that JDBC hides format selection, so you can decide up front whether JDBC's fixed contract is sufficient. +**Goal:** understand how JDBC handles format selection internally and how to configure custom formats like `JSONEachRow`. -JDBC does **not** expose format selection. The driver picks formats internally by operation type: +JDBC uses the client's format selection mechanism under the hood. By default, the driver sends `X-ClickHouse-Format: RowBinaryWithNamesAndTypes` for query execution: | Operation | Internal format | Notes | |-----------|-----------------|-------| -| Query (`executeQuery`) | Binary row format from server | Converted to JDBC `ResultSet` rows | +| Query (`executeQuery`) | `RowBinaryWithNamesAndTypes` | Converted to JDBC `ResultSet` rows | | Simple INSERT via `Statement` | SQL text | `INSERT INTO t VALUES (...)` | | `PreparedStatement` INSERT | SQL text or RowBinary | RowBinary when `beta.row_binary_for_simple_insert=true` | | Writer statement INSERT | RowBinary | Streaming binary writer | | Batch INSERT | Multi-row SQL rewrite or RowBinary | Depends on statement shape | +### Format Selection and SQL `FORMAT` Clauses + +The response format can be configured using the `format` connection property (`ClientConfigProperties.INPUT_OUTPUT_FORMAT` or `"format"`). + +**Important for ClickHouse 26.8+:** +- On ClickHouse 26.8+, the request format header sent by the driver (`X-ClickHouse-Format`) takes priority over a `FORMAT` clause written in the SQL query string. +- By default, the driver sends `format=RowBinaryWithNamesAndTypes`. +- To use a SQL `FORMAT` clause (such as `SELECT ... FORMAT JSONEachRow`) with ClickHouse 26.8+, set `format=JSONEachRow` in connection properties or set `format=` (to `""` empty string) so the default binary format header is omitted and ClickHouse honors the query's `FORMAT` clause. + +### Usage of `JSONEachRow` in JDBC + +JDBC V2 supports streaming `JSONEachRow` responses as standard `ResultSet` instances. This feature is opt-in and requires configuring a `JsonParserFactory`. + +1. **Configure Driver Properties:** + Set `jdbc_json_parser_factory` (`DriverProperties.JSON_PARSER_FACTORY`) to the fully-qualified class name of a `JsonParserFactory` implementation (such as `JacksonJsonParserFactory` or `GsonJsonParserFactory`). + Set `format` (`ClientConfigProperties.INPUT_OUTPUT_FORMAT`) to `"JSONEachRow"` (or set `format=` when including `FORMAT JSONEachRow` in the query). + +```java +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.data_formats.JacksonJsonParserFactory; +import com.clickhouse.jdbc.DriverProperties; + +public Properties createJsonEachRowProperties() { + Properties props = new Properties(); + props.setProperty(DriverProperties.JSON_PARSER_FACTORY.getKey(), JacksonJsonParserFactory.class.getName()); + props.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), "JSONEachRow"); + return props; +} +``` + +2. **Execute Query and Process ResultSet:** + +```java +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +public void readJsonEachRowResultSet(Connection conn) throws Exception { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, name, payload FROM events ORDER BY id")) { + while (rs.next()) { + int id = rs.getInt("id"); + String name = rs.getString("name"); + Object payload = rs.getObject("payload"); // returns parser-native List/Map + } + } +} +``` + ### When JDBC's format contract is not enough | Goal | JDBC approach | Better alternative | @@ -421,7 +470,7 @@ JDBC does **not** expose format selection. The driver picks formats internally b | Simple CRUD / reporting | Standard JDBC — sufficient | — | | Bulk ingest (millions of rows) | Batch `PreparedStatement` + RowBinary beta | Java Client stream insert | | Complex type handling | `getObject()` with type map | Java Client POJO/binary readers | -| Export to a file format | Not supported via JDBC | Java Client with format selection | +| Export to a file format | `format` property / JSONEachRow | Java Client with format selection | | BI tool integration | JDBC is the right choice | — | ### Hybrid usage: dropping down to the Java Client @@ -452,7 +501,7 @@ This hybrid approach allows you to use standard JDBC for simple CRUD and metadat ### Common Pitfalls -- **No format selection API** — you cannot request `Native`, `Parquet`, or `JSONEachRow` through standard JDBC. +- **Format selection scope** — format selection can be configured via connection properties (`format=JSONEachRow` or setting `jdbc_json_parser_factory`), but standard JDBC `ResultSet` requires compatible row formats (`RowBinaryWithNamesAndTypes` or `JSONEachRow`). Other wire formats like `Native` or `Parquet` require dropping down to the Java Client. - **Row-oriented output only** — no column-oriented or parallel block consumption. - **Type mapping layer** may lose precision or structure for complex types. - **Text INSERT overhead** — default SQL-based inserts are slower than binary streaming. Use the [Java Client](integration-client.md) for maximum throughput. diff --git a/docs/releases/0_11_0.md b/docs/releases/0_11_0.md index 6c40120f9..09ca60ff7 100644 --- a/docs/releases/0_11_0.md +++ b/docs/releases/0_11_0.md @@ -11,3 +11,13 @@ was removed, because the kind of the operation is always known where the metrics Metrics are created by the client, and the constructor takes an internal type (`com.clickhouse.client.api.internal.ClientStatisticsHolder`), so application code is not expected to call it. Code that does call it must pass `OperationType.QUERY` or `OperationType.INSERT`. + +## CLIENT-V2, JDBC-V2: Format Selection + +There are, generally, two ways to set format: in `FORMAT` clause or in request header. The problem is +when both are set. Before ClickHouse `26.8` `FORMAT` clause has priority and with `26.8` it was changed. +Now there is a problem with clients relying on old logic. See issue https://github.com/ClickHouse/clickhouse-java/issues/3086 + +Another problem is that client sets default format when settings missing it. This is fixed in current version +by setting default format on client level and not on operation. + diff --git a/examples/jdbc-v2-json-processors/README.md b/examples/jdbc-v2-json-processors/README.md index 3eaeb1cb7..1959cfd6d 100644 --- a/examples/jdbc-v2-json-processors/README.md +++ b/examples/jdbc-v2-json-processors/README.md @@ -81,13 +81,13 @@ Each read call in `run()` follows the same three-step shape: 2. **Customize if needed** — only inside the subclass, by overriding the protected hook. 3. **Execute** — `readAll(label, factoryClass)` opens a fresh connection - with `JSON_PARSER_FACTORY=`, runs the `SELECT ... FORMAT JSONEachRow` - and iterates the `ResultSet`. + with `JSON_PARSER_FACTORY=` and `format=JSONEachRow`, runs the + `SELECT ... FORMAT JSONEachRow` and iterates the `ResultSet`. -Because JDBC selects `JSONEachRow` through SQL text, set the JSON output -server settings explicitly on the connection when numeric accessors are used: +Configure the format and JSON output server settings explicitly on the connection: ```java +props.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), "JSONEachRow"); props.setProperty(ClientConfigProperties.serverSetting("output_format_json_quote_64bit_integers"), "0"); props.setProperty(ClientConfigProperties.serverSetting("output_format_json_quote_64bit_floats"), "0"); props.setProperty(ClientConfigProperties.serverSetting("output_format_json_quote_decimals"), "0"); diff --git a/examples/jdbc-v2-json-processors/src/main/java/com/clickhouse/examples/jdbc_v2/json_processors/JdbcV2JsonProcessorsExample.java b/examples/jdbc-v2-json-processors/src/main/java/com/clickhouse/examples/jdbc_v2/json_processors/JdbcV2JsonProcessorsExample.java index 4b54e5c88..25e221f6f 100644 --- a/examples/jdbc-v2-json-processors/src/main/java/com/clickhouse/examples/jdbc_v2/json_processors/JdbcV2JsonProcessorsExample.java +++ b/examples/jdbc-v2-json-processors/src/main/java/com/clickhouse/examples/jdbc_v2/json_processors/JdbcV2JsonProcessorsExample.java @@ -137,6 +137,7 @@ private Properties baseProperties() { var properties = new Properties(); properties.setProperty("user", user); properties.setProperty("password", password); + properties.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), "JSONEachRow"); properties.setProperty(ClientConfigProperties.serverSetting("allow_experimental_json_type"), "1"); properties.setProperty(ClientConfigProperties.serverSetting("output_format_json_quote_64bit_integers"), "0"); properties.setProperty(ClientConfigProperties.serverSetting("output_format_json_quote_64bit_floats"), "0"); 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 d52adc967..677f51add 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java @@ -935,7 +935,9 @@ public void testUnwrapping() throws Exception { @Test(groups = { "integration" }) public void testRawJSONQueryThroughUnderlyingClient() throws Exception { ObjectMapper mapper = new ObjectMapper(); - try (Connection conn = getJdbcConnection(); + Properties config = new Properties(); + config.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), ""); + try (Connection conn = getJdbcConnection(config); QueryResponse response = conn.unwrap(ConnectionImpl.class).getClient() .query("SELECT 1 AS x FORMAT JSON") .get()) { diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java index 54f4d0624..1f1629faf 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java @@ -297,8 +297,9 @@ public void testJsonEachRowCursorPositionDetectsLastRow() throws SQLException { public void testJsonEachRowGetObjectReturnsParserNativeArray() throws SQLException { Properties properties = new Properties(); properties.setProperty(DriverProperties.JSON_PARSER_FACTORY.getKey(), JacksonJsonParserFactory.class.getName()); + properties.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), "JSONEachRow"); try (Connection conn = getJdbcConnection(properties); Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("SELECT [1, 2, 3] AS arr FORMAT JSONEachRow")) { + try (ResultSet rs = stmt.executeQuery("SELECT [1, 2, 3] AS arr")) { Assert.assertTrue(rs.next()); Object value = rs.getObject("arr"); Assert.assertTrue(value instanceof List, "Expected parser-native List but got " + value.getClass()); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index e4a4e5c08..116925ffd 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -7,6 +7,7 @@ import com.clickhouse.client.api.data_formats.JsonParserFactory; import com.clickhouse.client.api.internal.ServerSettings; import com.clickhouse.client.api.query.GenericRecord; +import com.clickhouse.data.ClickHouseFormat; import com.clickhouse.data.ClickHouseVersion; import com.clickhouse.jdbc.internal.SqlParserFacade; import org.apache.commons.lang3.RandomStringUtils; @@ -862,8 +863,17 @@ public void testCancelInsertWithSession() throws Exception { } @Test(groups = {"integration"}) - public void testTextFormatInResponse() throws Exception { - try (Connection conn = getJdbcConnection(); + public void testUnsupportedFormat() throws Exception { + Properties config1 = new Properties(); + config1.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), ""); + try (Connection conn = getJdbcConnection(config1); + Statement stmt = conn.createStatement()) { + Assert.expectThrows(SQLException.class, () -> stmt.executeQuery("SELECT 1 FORMAT JSON")); + } + + Properties config2 = new Properties(); + config2.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), ClickHouseFormat.CSV.name()); + try (Connection conn = getJdbcConnection(config2); Statement stmt = conn.createStatement()) { Assert.expectThrows(SQLException.class, () -> stmt.executeQuery("SELECT 1 FORMAT JSON")); } @@ -873,6 +883,24 @@ public void testTextFormatInResponse() throws Exception { public void testJSONEachRowFormat(Class parserFactory) throws Exception { Properties properties = new Properties(); properties.setProperty(DriverProperties.JSON_PARSER_FACTORY.getKey(), parserFactory.getName()); + properties.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), "JSONEachRow"); + try (Connection conn = getJdbcConnection(properties)) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT 1 AS num, 'test' AS str")) { + assertTrue(rs.next()); + assertEquals(rs.getInt("num"), 1); + assertEquals(rs.getString("str"), "test"); + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = {"integration"}, dataProvider = "testJSONEachRowFormatDP") + public void testJSONEachRowFormatWithSqlClause(Class parserFactory) throws Exception { + Properties properties = new Properties(); + properties.setProperty(DriverProperties.JSON_PARSER_FACTORY.getKey(), parserFactory.getName()); + properties.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), ""); try (Connection conn = getJdbcConnection(properties)) { try (Statement stmt = conn.createStatement()) { try (ResultSet rs = stmt.executeQuery("SELECT 1 AS num, 'test' AS str FORMAT JSONEachRow")) { @@ -887,10 +915,12 @@ public void testJSONEachRowFormat(Class parserFactory) throws @Test(groups = {"integration"}) public void testJSONEachRowFormatRequiresParserFactory() throws Exception { - try (Connection conn = getJdbcConnection(); + Properties properties = new Properties(); + properties.setProperty(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), "JSONEachRow"); + try (Connection conn = getJdbcConnection(properties); Statement stmt = conn.createStatement()) { try { - stmt.executeQuery("SELECT 1 AS num FORMAT JSONEachRow"); + stmt.executeQuery("SELECT 1 AS num"); fail("Expected SQLException"); } catch (SQLException e) { assertTrue(e.getMessage().contains(DriverProperties.JSON_PARSER_FACTORY.getKey()),