Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"})`
Expand Down
17 changes: 12 additions & 5 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,8 +91,6 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.net.ssl.SSLContext;

/**
* <p>Client is the starting point for all interactions with ClickHouse. </p>
*
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -1908,9 +1918,6 @@ public CompletableFuture<QueryResponse> query(String sqlQuery, Map<String, Objec
}
final QuerySettings requestSettings = new QuerySettings(buildRequestSettings(settings.getAllSettings()));

if (requestSettings.getFormat() == null) {
requestSettings.setFormat(ClickHouseFormat.RowBinaryWithNamesAndTypes);
}
applyFormatSpecificSettings(requestSettings);
ClientStatisticsHolder clientStats = new ClientStatisticsHolder();
// Origin of the duration of a failed operation. Taken where the client starts OP_DURATION, which is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public enum ClientConfigProperties {

RETRY_ON_FAILURE("retry", Integer.class, "3"),

INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class),
INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class, ClickHouseFormat.RowBinaryWithNamesAndTypes.name()),

MAX_THREADS_PER_CLIENT("max_threads_per_client", Integer.class, "0"),

Expand Down Expand Up @@ -347,9 +347,13 @@ public Object parseValue(String value) {
}

if (valueType.isEnum()) {
String configValue = value.trim();
if (configValue.isEmpty()) {
return null;
}
Object[] constants = valueType.getEnumConstants();
for (Object constant : constants) {
if (constant.toString().equals(value)) {
if (constant.toString().equalsIgnoreCase(configValue)) {
return constant;
}
}
Expand Down Expand Up @@ -395,7 +399,9 @@ public static Map<String, Object> parseConfigMap(Map<String, String> configMap)
default:
parsedValue = config.parseValue(value);
}
parsedConfig.put(config.getKey(), parsedValue);
if (parsedValue != null) {
parsedConfig.put(config.getKey(), parsedValue);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,10 +872,13 @@ private void logServerErrorResponse(HttpPost req, ClassicHttpResponse httpRespon
private void addHeaders(HttpPost req, Map<String, Object> 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -365,9 +365,10 @@ public void testDefaultSettings() {
.setSocketRcvbuf(100000)
.setSocketSndbuf(100000)
.binaryStringSupport(true)
.queryFormat(ClickHouseFormat.CSV.name())
.build()) {
Map<String, String> 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");
Expand All @@ -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");
}
}

Expand Down Expand Up @@ -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.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
{
Expand Down
16 changes: 16 additions & 0 deletions docs/clickhouse-docs/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 &lt; `0.11.0` &amp; ClickHouse &lt; `26.8` - `FORMAT` clause has priority over request format header.
- Client &gt; `0.11.0` &amp; ClickHouse &gt; `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.
Expand Down
4 changes: 2 additions & 2 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions docs/integration-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FormatName>` 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.
Expand Down
Loading
Loading