org.bouncycastle
bcprov-jdk18on
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 786fbfe5e..f6762f3d6 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
@@ -11,6 +11,7 @@
import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader;
import com.clickhouse.client.api.data_formats.internal.MapBackedRecord;
import com.clickhouse.client.api.data_formats.internal.ProcessParser;
+import com.clickhouse.client.api.enums.CompressionAlgorithm;
import com.clickhouse.client.api.enums.Protocol;
import com.clickhouse.client.api.enums.ProxyType;
import com.clickhouse.client.api.enums.SSLMode;
@@ -656,10 +657,32 @@ public Builder compressClientRequest(boolean enabled) {
return this;
}
+ /**
+ * Algorithm of a compressed request or response body. The algorithm is requested with the HTTP
+ * content-coding of the operation, so a compressed body always uses the algorithm set here and
+ * never one the server picks on its own. {@link CompressionAlgorithm#NONE} disables compression.
+ * Default is {@link CompressionAlgorithm#LZ4}.
+ *
+ * {@link CompressionAlgorithm#ZSTD} needs {@code com.github.luben:zstd-jni} on the classpath - the
+ * dependency is {@code provided}, so an application that selects the algorithm has to declare it.
+ *
+ * A request body follows this algorithm only together with {@link #useHttpCompression(boolean)};
+ * the ClickHouse framing of a request compressed without it is always LZ4.
+ *
+ * @param algorithm - algorithm of a compressed body
+ * @return same instance of the builder
+ */
+ public Builder compressionAlgorithm(CompressionAlgorithm algorithm) {
+ ValidationUtils.checkNotNull(algorithm, "algorithm");
+ this.configuration.put(ClientConfigProperties.COMPRESSION_ALGORITHM.getKey(), algorithm.name());
+ return this;
+ }
+
/**
* Configures the client to use HTTP compression. In this case compression is controlled by
- * http headers. Client compression will set {@code Content-Encoding: lz4} header and server
- * compression will set {@code Accept-Encoding: lz4} header. Default is false.
+ * http headers. Client compression will set the {@code Content-Encoding} header and server
+ * compression will set the {@code Accept-Encoding} header, both to the content coding of
+ * {@link #compressionAlgorithm(CompressionAlgorithm)}. Default is false.
*
* @param enabled - indicates if http compression is enabled
* @return
@@ -695,8 +718,6 @@ public Builder setLZ4UncompressedBufferSize(int size) {
/**
* Disable native compression. If set to true then native compression will be disabled.
* If from some reason the native compressor is not working then it can be disabled.
- * Applies to LZ4 only: a response the server compressed with ZSTD is always read with the
- * native library of zstd-jni, because the server picks the codec of the response.
* @param disable
* @return
*/
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java
index bbce7a37b..b2dfa8c46 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java
@@ -2,6 +2,7 @@
import com.clickhouse.client.api.data_formats.ClickHouseFormatReader;
import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader;
+import com.clickhouse.client.api.enums.CompressionAlgorithm;
import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream;
import com.clickhouse.data.ClickHouseDataType;
@@ -245,6 +246,26 @@ public Object parseValue(String value) {
.collect(Collectors.toList());
}
},
+
+ /**
+ * Algorithm of a compressed request or response body. The algorithm is requested with the HTTP
+ * content-coding of the operation ({@code Accept-Encoding} for a response, {@code Content-Encoding}
+ * for a request), so a compressed body always uses the algorithm the client asked for and never one
+ * the server picks on its own. {@link CompressionAlgorithm#NONE} disables compression of both
+ * directions.
+ *
+ * The name of an algorithm and its content-coding token are both accepted, in any case.
+ *
+ * Appended at the end of the enum on purpose: adding a constant in the middle would shift the ordinal
+ * of every following constant (see {@code docs/changes_checklist.md}).
+ */
+ COMPRESSION_ALGORITHM("client.compression_algorithm", CompressionAlgorithm.class,
+ CompressionAlgorithm.LZ4.name()) {
+ @Override
+ public Object parseValue(String value) {
+ return value == null ? null : CompressionAlgorithm.fromValue(value);
+ }
+ },
;
private static final Logger LOG = LoggerFactory.getLogger(ClientConfigProperties.class);
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/enums/CompressionAlgorithm.java b/client-v2/src/main/java/com/clickhouse/client/api/enums/CompressionAlgorithm.java
new file mode 100644
index 000000000..9b79162a8
--- /dev/null
+++ b/client-v2/src/main/java/com/clickhouse/client/api/enums/CompressionAlgorithm.java
@@ -0,0 +1,71 @@
+package com.clickhouse.client.api.enums;
+
+/**
+ * Enumerates the compression algorithms the client can ask the server for and can apply itself.
+ *
+ *
The algorithm is requested with the HTTP content-coding of the operation - {@code Accept-Encoding}
+ * for a response and {@code Content-Encoding} for a request - so a compressed body always uses the
+ * algorithm of the request and never one the server picks on its own.
+ *
+ *
+ * - {@link #LZ4} - default. Needs {@code org.lz4:lz4-java}, which the client depends on.
+ * - {@link #ZSTD} - needs {@code com.github.luben:zstd-jni} on the classpath. The dependency is
+ * {@code provided}, so an application that selects this algorithm has to declare it.
+ * - {@link #GZIP} - supported by the JDK, so it needs no additional dependency.
+ * - {@link #NONE} - no compression, whatever {@code compress}/{@code decompress} are set to.
+ *
+ */
+public enum CompressionAlgorithm {
+
+ /**
+ * ClickHouse LZ4. Default algorithm.
+ */
+ LZ4("lz4"),
+
+ /**
+ * Zstandard. Requires {@code com.github.luben:zstd-jni} on the classpath.
+ */
+ ZSTD("zstd"),
+
+ /**
+ * gzip. Supported by the JDK.
+ */
+ GZIP("gzip"),
+
+ /**
+ * No compression.
+ */
+ NONE("none");
+
+ private final String httpContentCoding;
+
+ CompressionAlgorithm(String httpContentCoding) {
+ this.httpContentCoding = httpContentCoding;
+ }
+
+ /**
+ * Returns the HTTP content-coding token of the algorithm, as used in the {@code Accept-Encoding}
+ * and {@code Content-Encoding} headers.
+ *
+ * @return content-coding token
+ */
+ public String getHttpContentCoding() {
+ return httpContentCoding;
+ }
+
+ /**
+ * Case-insensitive variant of {@link #valueOf(String)} that also accepts the content-coding token.
+ *
+ * @param value algorithm name or content-coding token in any case
+ * @return matching algorithm
+ * @throws IllegalArgumentException when the value does not match any algorithm
+ */
+ public static CompressionAlgorithm fromValue(String value) {
+ for (CompressionAlgorithm algorithm : values()) {
+ if (algorithm.name().equalsIgnoreCase(value) || algorithm.httpContentCoding.equalsIgnoreCase(value)) {
+ return algorithm;
+ }
+ }
+ throw new IllegalArgumentException("Unknown compression algorithm '" + value + "'");
+ }
+}
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/ClickHouseLZ4InputStream.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/ClickHouseLZ4InputStream.java
index aff39fcee..8550fb6c2 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/internal/ClickHouseLZ4InputStream.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/ClickHouseLZ4InputStream.java
@@ -4,8 +4,6 @@
import com.clickhouse.data.ClickHouseByteUtils;
import com.clickhouse.data.ClickHouseCityHash;
import com.clickhouse.data.ClickHouseUtils;
-import com.github.luben.zstd.Zstd;
-import com.github.luben.zstd.ZstdException;
import net.jpountz.lz4.LZ4FastDecompressor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,11 +13,6 @@
import java.io.InputStream;
import java.nio.ByteBuffer;
-/**
- * Reads the framed output of the ClickHouse HTTP {@code compress=1} interface. Each block is
- * self-describing: its header carries the compression method the server used, so the codec is
- * detected per block instead of being assumed. LZ4, ZSTD and uncompressed blocks are supported.
- */
public class ClickHouseLZ4InputStream extends InputStream {
private static Logger LOG = LoggerFactory.getLogger(ClickHouseLZ4InputStream.class);
@@ -34,7 +27,7 @@ public class ClickHouseLZ4InputStream extends InputStream {
public ClickHouseLZ4InputStream(InputStream in, LZ4FastDecompressor decompressor, int bufferSize) {
super();
- LOG.debug("Reading compressed response with buffer size {}", bufferSize);
+ LOG.debug("Using LZ4 decompressor with buffer size {}", bufferSize);
this.decompressor = decompressor;
this.in = in;
this.buffer = ByteBuffer.allocate(bufferSize);
@@ -73,8 +66,6 @@ public int read(byte[] b, int off, int len) throws IOException {
static final byte MAGIC = (byte) 0x82;
- static final byte MAGIC_ZSTD = (byte) 0x90;
- static final byte MAGIC_NONE = (byte) 0x02;
static final int HEADER_LENGTH = 25;
final byte[] headerBuff = new byte[HEADER_LENGTH];
@@ -116,10 +107,9 @@ private int refill() throws IOException {
return -1;
}
- // 1 byte - compression method (0x82 LZ4, 0x90 ZSTD, 0x02 uncompressed)
- final byte method = headerBuff[16];
- if (method != MAGIC && method != MAGIC_ZSTD && method != MAGIC_NONE) {
- throw new ClientException("Invalid compression method byte: '" + method + "'");
+ if (headerBuff[16] != MAGIC) {
+ // 1 byte - 0x82 (shows this is LZ4)
+ throw new ClientException("Invalid LZ4 magic byte: '" + headerBuff[16] + "'");
}
// 4 bytes - size of the compressed data including 9 bytes of the header
@@ -128,14 +118,8 @@ private int refill() throws IOException {
int uncompressedSize = getInt32(headerBuff, 21);
int offset = 9;
- if (compressedSizeWithHeader < offset || uncompressedSize < 0) {
- throw new ClientException(ClickHouseUtils.format(
- "Corrupted stream: block declares %s compressed and %s uncompressed bytes",
- compressedSizeWithHeader, uncompressedSize));
- }
-
final byte[] block = new byte[compressedSizeWithHeader];
- block[0] = method;
+ block[0] = MAGIC;
setInt32(block, 1, compressedSizeWithHeader);
setInt32(block, 5, uncompressedSize);
// compressed data: compressed_size - 9 bytes
@@ -154,55 +138,12 @@ private int refill() throws IOException {
if (buffer.capacity() < uncompressedSize) {
buffer = ByteBuffer.allocate(uncompressedSize);
}
- decompress(method, block, offset, remaining, uncompressedSize);
+ decompressor.decompress(ByteBuffer.wrap(block), offset, buffer, 0, uncompressedSize);
buffer.position(0);
buffer.limit(uncompressedSize);
return uncompressedSize;
}
- /**
- * Decompresses a single block into {@link #buffer} with the codec the block header declares.
- *
- * @param method compression method byte of the block
- * @param block block, including its 9 bytes of header
- * @param offset offset of the compressed data in the block
- * @param compressedSize size of the compressed data
- * @param uncompressedSize size of the data after decompression
- */
- private void decompress(byte method, byte[] block, int offset, int compressedSize, int uncompressedSize) {
- switch (method) {
- case MAGIC:
- decompressor.decompress(ByteBuffer.wrap(block), offset, buffer, 0, uncompressedSize);
- break;
- case MAGIC_ZSTD:
- long decompressedSize;
- try {
- decompressedSize = Zstd.decompressByteArray(buffer.array(), buffer.arrayOffset(),
- uncompressedSize, block, offset, compressedSize);
- } catch (ZstdException e) {
- throw new ClientException("Failed to decompress ZSTD block: " + e.getMessage(), e);
- } catch (LinkageError e) {
- // the server picks the codec of the response, so ZSTD cannot be avoided by configuration
- throw new ClientException("Server compressed the response with ZSTD but the native library of "
- + "zstd-jni is not available on this platform", e);
- }
- if (decompressedSize != uncompressedSize) {
- throw new ClientException(ClickHouseUtils.format(
- "Corrupted stream: decompressed %s bytes while %s were expected",
- decompressedSize, uncompressedSize));
- }
- break;
- default: // MAGIC_NONE
- if (compressedSize != uncompressedSize) {
- throw new ClientException(ClickHouseUtils.format(
- "Corrupted stream: uncompressed block holds %s bytes while %s were expected",
- compressedSize, uncompressedSize));
- }
- System.arraycopy(block, offset, buffer.array(), buffer.arrayOffset(), uncompressedSize);
- break;
- }
- }
-
/**
* Read int32 Little Endian
* @param bytes
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 9b303a960..e944f9424 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
@@ -11,6 +11,7 @@
import com.clickhouse.client.api.DataTransferException;
import com.clickhouse.client.api.ServerException;
import com.clickhouse.client.api.TransportException;
+import com.clickhouse.client.api.enums.CompressionAlgorithm;
import com.clickhouse.client.api.enums.ProxyType;
import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.http.ClickHouseHttpProto;
@@ -123,7 +124,6 @@ public class HttpAPIClientHelper {
private static final int ERROR_BODY_BUFFER_SIZE = 1024; // Error messages are usually small
- private final String DEFAULT_HTTP_COMPRESSION_ALGO = "lz4";
private static final Pattern PATTERN_HEADER_VALUE_ASCII = Pattern.compile(
"\\p{Graph}+(?:[ ]\\p{Graph}+)*");
@@ -447,7 +447,7 @@ private ServerException readNotClickHouseError(HttpEntity httpEntity, String que
}
break;
} catch (ClientException e) {
- // response body is not the framed output of the compress=1 interface
+ // Invalid LZ4 Magic
if (body instanceof ClickHouseLZ4InputStream) {
ClickHouseLZ4InputStream stream = (ClickHouseLZ4InputStream) body;
body = stream.getInputStream();
@@ -478,7 +478,7 @@ private static ServerException readClickHouseError(HttpEntity httpEntity, int se
try {
rBytes = body.read(buffer);
} catch (ClientException e) {
- // response body is not the framed output of the compress=1 interface
+ // Invalid LZ4 Magic
if (body instanceof ClickHouseLZ4InputStream) {
ClickHouseLZ4InputStream stream = (ClickHouseLZ4InputStream) body;
body = stream.getInputStream();
@@ -925,14 +925,17 @@ private void addHeaders(HttpPost req, Map requestConfig) {
boolean serverCompression = ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getOrDefault(requestConfig);
boolean useHttpCompression = ClientConfigProperties.USE_HTTP_COMPRESSION.getOrDefault(requestConfig);
boolean appCompressedData = ClientConfigProperties.APP_COMPRESSED_DATA.getOrDefault(requestConfig);
+ CompressionAlgorithm algorithm = ClientConfigProperties.COMPRESSION_ALGORITHM.getOrDefault(requestConfig);
- if (useHttpCompression) {
+ if (algorithm != CompressionAlgorithm.NONE) {
if (serverCompression) {
- setHeader(req, HttpHeaders.ACCEPT_ENCODING, DEFAULT_HTTP_COMPRESSION_ALGO);
+ // the codec of a compressed response is the one requested here: the server picks its own
+ // default codec for the compress=1 framing and does not let a client select it
+ setHeader(req, HttpHeaders.ACCEPT_ENCODING, algorithm.getHttpContentCoding());
}
- if (clientCompression && !appCompressedData) {
- setHeader(req, HttpHeaders.CONTENT_ENCODING, DEFAULT_HTTP_COMPRESSION_ALGO);
+ if (useHttpCompression && clientCompression && !appCompressedData) {
+ setHeader(req, HttpHeaders.CONTENT_ENCODING, algorithm.getHttpContentCoding());
}
}
@@ -971,17 +974,24 @@ private void addRequestParams(Map requestConfig, BiConsumer
boolean clientCompression = ClientConfigProperties.COMPRESS_CLIENT_REQUEST.getOrDefault(requestConfig);
boolean useHttpCompression = ClientConfigProperties.USE_HTTP_COMPRESSION.getOrDefault(requestConfig);
boolean appCompressedData = ClientConfigProperties.APP_COMPRESSED_DATA.getOrDefault(requestConfig);
+ CompressionAlgorithm algorithm = ClientConfigProperties.COMPRESSION_ALGORITHM.getOrDefault(requestConfig);
if (httpEntity.getContentEncoding() != null && !appCompressedData) {
// http header is set and data is not compressed
return new CompressedEntity(httpEntity, false, CompressorStreamFactory.getSingleton());
- } else if (clientCompression && !appCompressedData) {
+ } else if (clientCompression && !appCompressedData && algorithm != CompressionAlgorithm.NONE) {
int buffSize = ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getOrDefault(requestConfig);
return new LZ4Entity(httpEntity, useHttpCompression, false, true,
buffSize, false, lz4Factory);
@@ -1026,20 +1037,13 @@ private HttpEntity wrapRequestEntity(HttpEntity httpEntity, Map
}
private HttpEntity wrapResponseEntity(HttpEntity httpEntity, int httpStatus, Map requestConfig) {
- boolean serverCompression = ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getOrDefault(requestConfig);
- boolean useHttpCompression = ClientConfigProperties.USE_HTTP_COMPRESSION.getOrDefault(requestConfig);
-
if (httpEntity.getContentEncoding() != null) {
- // http compressed response
+ // the algorithm of a compressed response is the one the request asked for
return new CompressedEntity(httpEntity, true, CompressorStreamFactory.getSingleton());
}
- // data compression
- if (serverCompression && !(httpStatus == HttpStatus.SC_FORBIDDEN || httpStatus == HttpStatus.SC_UNAUTHORIZED)) {
- int buffSize = ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getOrDefault(requestConfig);
- return new LZ4Entity(httpEntity, useHttpCompression, true, false, buffSize, true, lz4Factory);
- }
-
+ // a response without a content coding is not compressed: the server answers an unsupported
+ // Accept-Encoding, and a request that asks for no compression, with a plain body
return httpEntity;
}
diff --git a/client-v2/src/test/java/com/clickhouse/client/api/ClientConfigPropertiesTest.java b/client-v2/src/test/java/com/clickhouse/client/api/ClientConfigPropertiesTest.java
index 9c23ca522..43991c051 100644
--- a/client-v2/src/test/java/com/clickhouse/client/api/ClientConfigPropertiesTest.java
+++ b/client-v2/src/test/java/com/clickhouse/client/api/ClientConfigPropertiesTest.java
@@ -1,6 +1,7 @@
package com.clickhouse.client.api;
+import com.clickhouse.client.api.enums.CompressionAlgorithm;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -76,4 +77,42 @@ public void testParseConfigMapSanitizesSslCipherSuites() {
Assert.assertEquals(parsed.get(ClientConfigProperties.SSL_CIPHER_SUITES.getKey()),
Arrays.asList("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"));
}
-}
\ No newline at end of file
+
+ @DataProvider(name = "compressionAlgorithms")
+ public static Object[][] compressionAlgorithms() {
+ return new Object[][]{
+ // raw client.compression_algorithm value -> expected algorithm
+ {"LZ4", CompressionAlgorithm.LZ4},
+ {"lz4", CompressionAlgorithm.LZ4},
+ {"ZSTD", CompressionAlgorithm.ZSTD},
+ {"zstd", CompressionAlgorithm.ZSTD},
+ {"GZIP", CompressionAlgorithm.GZIP},
+ {"gzip", CompressionAlgorithm.GZIP},
+ {"NONE", CompressionAlgorithm.NONE},
+ {"none", CompressionAlgorithm.NONE},
+ };
+ }
+
+ @Test(groups = {"unit"}, dataProvider = "compressionAlgorithms")
+ public void testCompressionAlgorithmParsed(String raw, CompressionAlgorithm expected) {
+ Assert.assertEquals(ClientConfigProperties.COMPRESSION_ALGORITHM.parseValue(raw), expected);
+
+ Map config = new HashMap<>();
+ config.put(ClientConfigProperties.COMPRESSION_ALGORITHM.getKey(), raw);
+ Assert.assertEquals(ClientConfigProperties.parseConfigMap(config)
+ .get(ClientConfigProperties.COMPRESSION_ALGORITHM.getKey()), expected);
+ }
+
+ @Test(groups = {"unit"})
+ public void testCompressionAlgorithmDefaultsToLz4() {
+ Assert.assertEquals(ClientConfigProperties.COMPRESSION_ALGORITHM.getDefObjVal(), CompressionAlgorithm.LZ4);
+ Assert.assertEquals(
+ ClientConfigProperties.COMPRESSION_ALGORITHM.getOrDefault(Collections.emptyMap()),
+ CompressionAlgorithm.LZ4);
+ }
+
+ @Test(groups = {"unit"}, expectedExceptions = IllegalArgumentException.class)
+ public void testUnknownCompressionAlgorithmRejected() {
+ ClientConfigProperties.COMPRESSION_ALGORITHM.parseValue("snappy");
+ }
+}
diff --git a/client-v2/src/test/java/com/clickhouse/client/api/internal/ClickHouseLZ4InputStreamTest.java b/client-v2/src/test/java/com/clickhouse/client/api/internal/ClickHouseLZ4InputStreamTest.java
deleted file mode 100644
index 5283d4ad8..000000000
--- a/client-v2/src/test/java/com/clickhouse/client/api/internal/ClickHouseLZ4InputStreamTest.java
+++ /dev/null
@@ -1,212 +0,0 @@
-package com.clickhouse.client.api.internal;
-
-import com.clickhouse.client.api.ClientException;
-import com.clickhouse.data.ClickHouseCityHash;
-import com.github.luben.zstd.Zstd;
-import com.github.luben.zstd.ZstdException;
-import net.jpountz.lz4.LZ4Factory;
-import org.testng.Assert;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-
-public class ClickHouseLZ4InputStreamTest {
-
- private static final int BUFFER_SIZE = 8192;
-
- private static final int CHECKSUM_LENGTH = 16;
-
- private static final int BLOCK_HEADER_LENGTH = 9;
-
- private static final int METHOD_OFFSET = CHECKSUM_LENGTH;
-
- private static final int COMPRESSED_SIZE_OFFSET = CHECKSUM_LENGTH + 1;
-
- private static final int UNCOMPRESSED_SIZE_OFFSET = CHECKSUM_LENGTH + 5;
-
- private static final int DATA_OFFSET = CHECKSUM_LENGTH + BLOCK_HEADER_LENGTH;
-
- @Test(groups = {"unit"}, dataProvider = "compressionMethodProvider")
- public void testReadsBlockOfEveryCompressionMethod(byte method) throws IOException {
- byte[] payload = payload();
-
- Assert.assertEquals(readFully(frame(method, payload), BUFFER_SIZE), payload);
- }
-
- @Test(groups = {"unit"}, dataProvider = "compressionMethodProvider")
- public void testReadsBlockLargerThanInitialBuffer(byte method) throws IOException {
- byte[] payload = payload();
-
- Assert.assertEquals(readFully(frame(method, payload), 16), payload);
- }
-
- @DataProvider(name = "compressionMethodProvider")
- public Object[][] compressionMethodProvider() {
- return new Object[][]{
- {ClickHouseLZ4InputStream.MAGIC},
- {ClickHouseLZ4InputStream.MAGIC_ZSTD},
- {ClickHouseLZ4InputStream.MAGIC_NONE},
- };
- }
-
- @Test(groups = {"unit"})
- public void testReadsConsecutiveBlocksOfDifferentMethods() throws IOException {
- byte[] first = "first block\n".getBytes(StandardCharsets.UTF_8);
- byte[] second = "second block\n".getBytes(StandardCharsets.UTF_8);
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- stream.write(frame(ClickHouseLZ4InputStream.MAGIC_ZSTD, first));
- stream.write(frame(ClickHouseLZ4InputStream.MAGIC, second));
-
- byte[] expected = new byte[first.length + second.length];
- System.arraycopy(first, 0, expected, 0, first.length);
- System.arraycopy(second, 0, expected, first.length, second.length);
- Assert.assertEquals(readFully(stream.toByteArray(), BUFFER_SIZE), expected);
- }
-
- @Test(groups = {"unit"}, dataProvider = "corruptedFrameProvider")
- public void testRejectsCorruptedFrame(byte[] frame, String expectedMessage, Class> expectedCause) {
- ClientException e = Assert.expectThrows(ClientException.class, () -> readFully(frame, BUFFER_SIZE));
- Assert.assertTrue(e.getMessage().contains(expectedMessage), e.getMessage());
- if (expectedCause == null) {
- Assert.assertNull(e.getCause());
- } else {
- Assert.assertTrue(expectedCause.isInstance(e.getCause()), String.valueOf(e.getCause()));
- }
- }
-
- @DataProvider(name = "corruptedFrameProvider")
- public Object[][] corruptedFrameProvider() {
- byte[] payload = payload();
-
- byte[] unknownMethod = frame(ClickHouseLZ4InputStream.MAGIC_ZSTD, payload);
- unknownMethod[METHOD_OFFSET] = (byte) 0x42;
-
- byte[] impossibleCompressedSize = frame(ClickHouseLZ4InputStream.MAGIC_ZSTD, payload);
- ClickHouseLZ4InputStream.setInt32(impossibleCompressedSize, COMPRESSED_SIZE_OFFSET, 4);
-
- byte[] negativeUncompressedSize = frame(ClickHouseLZ4InputStream.MAGIC_ZSTD, payload);
- ClickHouseLZ4InputStream.setInt32(negativeUncompressedSize, UNCOMPRESSED_SIZE_OFFSET, -1);
-
- byte[] corruptedPayload = frame(ClickHouseLZ4InputStream.MAGIC_ZSTD, payload);
- corruptedPayload[corruptedPayload.length - 1] ^= 0xFF;
-
- // the ZSTD frame of the block loses its magic, so the codec itself rejects the data
- byte[] undecompressibleZstdBlock = frame(ClickHouseLZ4InputStream.MAGIC_ZSTD, payload);
- Arrays.fill(undecompressibleZstdBlock, DATA_OFFSET, DATA_OFFSET + 4, (byte) 0);
- reseal(undecompressibleZstdBlock);
-
- byte[] shortZstdBlock = frame(ClickHouseLZ4InputStream.MAGIC_ZSTD, payload);
- ClickHouseLZ4InputStream.setInt32(shortZstdBlock, UNCOMPRESSED_SIZE_OFFSET, payload.length + 8);
- reseal(shortZstdBlock);
-
- byte[] shortUncompressedBlock = frame(ClickHouseLZ4InputStream.MAGIC_NONE, payload);
- ClickHouseLZ4InputStream.setInt32(shortUncompressedBlock, UNCOMPRESSED_SIZE_OFFSET, payload.length - 1);
- reseal(shortUncompressedBlock);
-
- return new Object[][]{
- {unknownMethod, "Invalid compression method byte", null},
- {impossibleCompressedSize, "block declares 4 compressed", null},
- {negativeUncompressedSize, "-1 uncompressed bytes", null},
- {corruptedPayload, "checksum mismatch", null},
- {undecompressibleZstdBlock, "Failed to decompress ZSTD block", ZstdException.class},
- {shortZstdBlock, "decompressed " + payload.length + " bytes while "
- + (payload.length + 8) + " were expected", null},
- {shortUncompressedBlock, "uncompressed block holds " + payload.length + " bytes while "
- + (payload.length - 1) + " were expected", null},
- };
- }
-
- @Test(groups = {"unit"})
- public void testKeepsUnframedBodyReadableThroughHeaderBuffer() throws IOException {
- byte[] body = "Code: 62. DB::Exception: Syntax error: failed at position 1\n"
- .getBytes(StandardCharsets.UTF_8);
-
- try (ClickHouseLZ4InputStream in = new ClickHouseLZ4InputStream(new ByteArrayInputStream(body),
- LZ4Factory.fastestInstance().fastDecompressor(), BUFFER_SIZE)) {
- Assert.expectThrows(ClientException.class, () -> in.read(new byte[64]));
- Assert.assertEquals(in.getHeaderBuffer(),
- Arrays.copyOf(body, ClickHouseLZ4InputStream.HEADER_LENGTH));
- }
- }
-
- private static byte[] payload() {
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 100; i++) {
- sb.append(i).append('\t').append("value-").append(i).append('\n');
- }
- return sb.toString().getBytes(StandardCharsets.UTF_8);
- }
-
- private static byte[] readFully(byte[] frames, int bufferSize) throws IOException {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- try (InputStream in = new ClickHouseLZ4InputStream(new ByteArrayInputStream(frames),
- LZ4Factory.fastestInstance().fastDecompressor(), bufferSize)) {
- byte[] chunk = new byte[64];
- int read;
- while ((read = in.read(chunk)) != -1) {
- out.write(chunk, 0, read);
- }
- }
- return out.toByteArray();
- }
-
- /**
- * Builds a block the way the server frames the output of the HTTP {@code compress=1}
- * interface: 16 bytes of CityHash128 checksum over the block, then a 9 byte header holding the
- * compression method, the compressed size (header included) and the uncompressed size, then
- * the compressed data.
- */
- private static byte[] frame(byte method, byte[] data) {
- byte[] compressed;
- switch (method) {
- case ClickHouseLZ4InputStream.MAGIC:
- byte[] lz4 = new byte[LZ4Factory.fastestInstance().fastCompressor().maxCompressedLength(data.length)];
- int lz4Length = LZ4Factory.fastestInstance().fastCompressor()
- .compress(data, 0, data.length, lz4, 0, lz4.length);
- compressed = new byte[lz4Length];
- System.arraycopy(lz4, 0, compressed, 0, lz4Length);
- break;
- case ClickHouseLZ4InputStream.MAGIC_ZSTD:
- compressed = Zstd.compress(data, 3);
- break;
- default:
- compressed = data;
- break;
- }
-
- byte[] block = new byte[BLOCK_HEADER_LENGTH + compressed.length];
- block[0] = method;
- ClickHouseLZ4InputStream.setInt32(block, 1, block.length);
- ClickHouseLZ4InputStream.setInt32(block, 5, data.length);
- System.arraycopy(compressed, 0, block, BLOCK_HEADER_LENGTH, compressed.length);
-
- byte[] frame = new byte[CHECKSUM_LENGTH + block.length];
- System.arraycopy(block, 0, frame, CHECKSUM_LENGTH, block.length);
- reseal(frame);
- return frame;
- }
-
- /**
- * Recomputes the checksum of a frame, so a block mutated after {@link #frame(byte, byte[])}
- * still passes the checksum and reaches the decompression of the reader. The checksum covers
- * the compressed size the header declares, which is what the reader hashes.
- */
- private static void reseal(byte[] frame) {
- long[] checksum = ClickHouseCityHash.cityHash128(frame, CHECKSUM_LENGTH,
- ClickHouseLZ4InputStream.getInt32(frame, COMPRESSED_SIZE_OFFSET));
- setInt64(frame, 0, checksum[0]);
- setInt64(frame, 8, checksum[1]);
- }
-
- private static void setInt64(byte[] bytes, int offset, long value) {
- for (int i = 0; i < 8; i++) {
- bytes[offset + i] = (byte) (0xFF & (value >> (8 * i)));
- }
- }
-}
diff --git a/client-v2/src/test/java/com/clickhouse/client/query/QueryServerContentCompressionTests.java b/client-v2/src/test/java/com/clickhouse/client/query/QueryServerContentCompressionTests.java
index 1001b08be..9dc385318 100644
--- a/client-v2/src/test/java/com/clickhouse/client/query/QueryServerContentCompressionTests.java
+++ b/client-v2/src/test/java/com/clickhouse/client/query/QueryServerContentCompressionTests.java
@@ -1,8 +1,40 @@
-package com.clickhouse.client.query;
-
-public class QueryServerContentCompressionTests extends QueryTests {
-
- QueryServerContentCompressionTests() {
- super(true, false);
- }
-}
+package com.clickhouse.client.query;
+
+import com.clickhouse.client.api.Client;
+import com.clickhouse.client.api.enums.CompressionAlgorithm;
+import com.clickhouse.client.api.query.GenericRecord;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.util.List;
+
+public class QueryServerContentCompressionTests extends QueryTests {
+
+ QueryServerContentCompressionTests() {
+ super(true, false);
+ }
+
+ @Test(groups = {"integration"}, dataProvider = "compressionAlgorithms")
+ public void testQueryWithCompressionAlgorithm(CompressionAlgorithm algorithm) throws Exception {
+ try (Client client = newClient().compressionAlgorithm(algorithm).build()) {
+ List records = client.queryAll("SELECT number, toString(number) AS str " +
+ "FROM system.numbers LIMIT 1000");
+
+ Assert.assertEquals(records.size(), 1000);
+ Assert.assertEquals(records.get(0).getLong("number"), 0);
+ Assert.assertEquals(records.get(999).getLong("number"), 999);
+ Assert.assertEquals(records.get(999).getString("str"), "999");
+ }
+ }
+
+ @DataProvider(name = "compressionAlgorithms")
+ public Object[][] compressionAlgorithms() {
+ return new Object[][]{
+ {CompressionAlgorithm.LZ4},
+ {CompressionAlgorithm.ZSTD},
+ {CompressionAlgorithm.GZIP},
+ {CompressionAlgorithm.NONE},
+ };
+ }
+}
diff --git a/docs/features.md b/docs/features.md
index 8df78cace..3647168b0 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -31,7 +31,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t
- Session handling: Supports client-wide and per-operation HTTP sessions, operation-level session overrides, runtime updates of client `session_id`, and server-side session validation through `session_check`.
- Metadata discovery: Loads table schemas from table names or queries and allows schema registration for typed read/write operations.
- Server information loading: Can refresh server version, current user, and server time zone information.
-- Compression support: Supports response compression, ClickHouse LZ4 request compression, HTTP content compression, and caller-supplied precompressed insert bodies. A compressed response is read with the codec of each block, so the server is free to answer with LZ4, ZSTD or uncompressed blocks.
+- Compression support: Supports response compression, ClickHouse LZ4 request compression, HTTP content compression, and caller-supplied precompressed insert bodies. The algorithm of a compressed body is selected with `client.compression_algorithm` (`LZ4` by default, also `ZSTD`, `GZIP` and `NONE`) and is requested with the HTTP content coding of the operation, so a compressed response uses the algorithm the client asked for on every server version.
- Retry behavior: Can retry failed operations for configured failure causes and retry limits.
- Client-side request cancellation: `Client.cancelTransportRequest(String queryId)` aborts the in-flight HTTP request and its IO for the operation started with the given query id. It requires the caller to set the query id in operation settings, is best-effort (it cancels client-side IO but the result is not guaranteed), and does not issue a server-side `KILL QUERY` - the server stops the query on its own once the client disconnects. A cancelled operation that is being retried stops instead of issuing another request, also when the cancellation lands between two attempts (for example from `DataStreamWriter#onRetry()`).
- Metrics and observability: Exposes client/server operation metrics and optionally integrates connection-pool gauges with Micrometer.
diff --git a/docs/releases/0_11_0.md b/docs/releases/0_11_0.md
index 6c40120f9..682e90089 100644
--- a/docs/releases/0_11_0.md
+++ b/docs/releases/0_11_0.md
@@ -2,6 +2,37 @@
# Migration Guide
+## CLIENT-V2: A Compressed Response Uses the Algorithm the Client Asks For
+
+A compressed response was requested with the `compress=1` framing of the HTTP interface, whose codec the server
+chooses on its own. ClickHouse `26.9` changed that codec from `LZ4` to `ZSTD(3)`; the framed output follows the
+built-in default codec and no setting overrides it, so a client that expects `LZ4` cannot read the response of a
+`26.9` server at all.
+
+The client now requests a response with the HTTP content coding of the algorithm it will decode, so the algorithm of
+a compressed body is always the one the client asked for:
+
+- The new property `client.compression_algorithm` (builder method `Client.Builder#compressionAlgorithm`) selects the
+ algorithm out of `LZ4` (default), `ZSTD`, `GZIP` and `NONE`. Both the name and the content-coding token of an
+ algorithm are accepted, in any case.
+- The default is `LZ4`, so an application that does not set the property keeps reading `LZ4` on every server version.
+- `ZSTD` needs `com.github.luben:zstd-jni` on the classpath. The dependency stays `provided`, so an application that
+ selects `ZSTD` has to declare it.
+- `NONE` disables compression of the request and the response, whatever `compress` and `decompress` are set to.
+
+What to check in an application:
+
+- **A user profile that forbids setting changes.** A client that reads a compressed response now also sends
+ `enable_http_compression=1`, which the server rejects for a profile with `readonly = 1`. Use `readonly = 2`, which
+ allows setting changes, or set `client.compression_algorithm` to `NONE`.
+- **Code that inspects the response encoding.** A compressed response now carries `Content-Encoding` with the
+ requested coding instead of the `compress=1` framing of ClickHouse.
+
+The algorithm of a compressed *request* body is unchanged: it follows `client.compression_algorithm` only together
+with `Client.Builder#useHttpCompression`, and the ClickHouse framing of a request compressed without it is always
+`LZ4`.
+
+
## CLIENT-V2: `OperationMetrics` Has a Single Constructor
`com.clickhouse.client.api.metrics.OperationMetrics` now has one constructor,
From c8fd801e440b36226341ca45f7efa92a4458d6d0 Mon Sep 17 00:00:00 2001
From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com>
Date: Tue, 8 Sep 2026 19:01:03 +0000
Subject: [PATCH 4/6] Let an operation select the algorithm and pin the request
contract
Read the algorithm through a resolver, so a per-operation option set as the
name of an algorithm is accepted: a per-operation option is stored unparsed.
Add the typed setters QuerySettings#compressionAlgorithm and
InsertSettings#compressionAlgorithm.
Warn when a request is compressed without http compression and another
algorithm than LZ4 is selected: the ClickHouse framing of a request is LZ4.
Pin the request contract with a mock-server test: compress=1 is not requested,
a response is requested with the content coding of the algorithm, NONE
requests no compression, and an operation overrides the client.
---
CHANGELOG.md | 4 +-
.../com/clickhouse/client/api/Client.java | 4 +-
.../api/enums/CompressionAlgorithm.java | 4 +-
.../client/api/insert/InsertSettings.java | 14 ++
.../api/internal/HttpAPIClientHelper.java | 35 +++--
.../client/api/query/QuerySettings.java | 14 ++
.../api/CompressionRequestUnitTest.java | 127 ++++++++++++++++++
docs/releases/0_11_0.md | 5 +-
8 files changed, 192 insertions(+), 15 deletions(-)
create mode 100644 client-v2/src/test/java/com/clickhouse/client/api/CompressionRequestUnitTest.java
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18b11238d..acf9cd7be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,9 @@
now requested with the content coding of the new `client.compression_algorithm` property
(`Client.Builder#compressionAlgorithm`), which defaults to `LZ4` and keeps the algorithm of a compressed body the
same on every server version. Set the property to `ZSTD`, `GZIP` or `NONE` to select another algorithm; `ZSTD`
- needs `com.github.luben:zstd-jni` on the classpath, which stays a `provided` dependency. A client that reads a
+ needs `com.github.luben:zstd-jni` on the classpath, which the client does not bring - the dependency of
+ `clickhouse-jdbc` stays `provided`, so packaging is unchanged and an application that selects `ZSTD` declares the
+ dependency itself. A client that reads a
compressed response now also sends `enable_http_compression=1`, which a user profile that forbids setting changes
(`readonly = 1`) rejects - such a profile has to use `readonly = 2` or `client.compression_algorithm = NONE`.
(https://github.com/ClickHouse/clickhouse-java/issues/3105)
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 f6762f3d6..e798b85e8 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
@@ -663,8 +663,8 @@ public Builder compressClientRequest(boolean enabled) {
* never one the server picks on its own. {@link CompressionAlgorithm#NONE} disables compression.
* Default is {@link CompressionAlgorithm#LZ4}.
*
- * {@link CompressionAlgorithm#ZSTD} needs {@code com.github.luben:zstd-jni} on the classpath - the
- * dependency is {@code provided}, so an application that selects the algorithm has to declare it.
+ * {@link CompressionAlgorithm#ZSTD} needs {@code com.github.luben:zstd-jni} on the classpath, which the
+ * client does not bring: an application that selects the algorithm declares the dependency itself.
*
* A request body follows this algorithm only together with {@link #useHttpCompression(boolean)};
* the ClickHouse framing of a request compressed without it is always LZ4.
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/enums/CompressionAlgorithm.java b/client-v2/src/main/java/com/clickhouse/client/api/enums/CompressionAlgorithm.java
index 9b79162a8..d9df4f26d 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/enums/CompressionAlgorithm.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/enums/CompressionAlgorithm.java
@@ -9,8 +9,8 @@
*
*
* - {@link #LZ4} - default. Needs {@code org.lz4:lz4-java}, which the client depends on.
- * - {@link #ZSTD} - needs {@code com.github.luben:zstd-jni} on the classpath. The dependency is
- * {@code provided}, so an application that selects this algorithm has to declare it.
+ * - {@link #ZSTD} - needs {@code com.github.luben:zstd-jni} on the classpath, which the client does not
+ * bring: an application that selects this algorithm declares the dependency itself.
* - {@link #GZIP} - supported by the JDK, so it needs no additional dependency.
* - {@link #NONE} - no compression, whatever {@code compress}/{@code decompress} are set to.
*
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java
index 078019b17..0434bf25c 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java
@@ -2,6 +2,7 @@
import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.ClientConfigProperties;
+import com.clickhouse.client.api.enums.CompressionAlgorithm;
import com.clickhouse.client.api.Session;
import com.clickhouse.client.api.internal.CommonSettings;
import org.apache.hc.core5.http.HttpHeaders;
@@ -213,6 +214,19 @@ public InsertSettings compressClientRequest(boolean enabled) {
return this;
}
+ /**
+ * Algorithm of a compressed request or response body of this operation. The algorithm is requested with
+ * the HTTP content coding of the operation, so a compressed body always uses the algorithm set here.
+ * {@code CompressionAlgorithm.NONE} disables compression. Defaults to the algorithm of the client.
+ *
+ * @param algorithm - algorithm of a compressed body
+ * @return same instance of the settings
+ */
+ public InsertSettings compressionAlgorithm(CompressionAlgorithm algorithm) {
+ settings.setOption(ClientConfigProperties.COMPRESSION_ALGORITHM.getKey(), algorithm);
+ return this;
+ }
+
public InsertSettings useHttpCompression(boolean enabled) {
settings.setOption(ClientConfigProperties.USE_HTTP_COMPRESSION.getKey(), enabled);
return this;
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 e944f9424..10ab559a0 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
@@ -163,7 +163,14 @@ public HttpAPIClientHelper(Map configuration, Object metricsRegi
boolean usingServerCompression = ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getOrDefault(configuration);
boolean useHttpCompression = ClientConfigProperties.USE_HTTP_COMPRESSION.getOrDefault(configuration);
- LOG.debug("client compression: {}, server compression: {}, http compression: {}", usingClientCompression, usingServerCompression, useHttpCompression);
+ CompressionAlgorithm algorithm = compressionAlgorithm(configuration);
+ LOG.debug("client compression: {}, server compression: {}, http compression: {}, algorithm: {}",
+ usingClientCompression, usingServerCompression, useHttpCompression, algorithm);
+ if (usingClientCompression && !useHttpCompression
+ && !(algorithm == CompressionAlgorithm.LZ4 || algorithm == CompressionAlgorithm.NONE)) {
+ LOG.warn("Request compression uses LZ4 instead of {}: the ClickHouse framing of a request is LZ4 " +
+ "unless http compression is used", algorithm);
+ }
defaultRetryCauses = new HashSet<>(ClientConfigProperties.CLIENT_RETRY_ON_FAILURE.getOrDefault(configuration));
if (defaultRetryCauses.contains(ClientFaultCause.None)) {
@@ -774,9 +781,7 @@ private TransportResponse doExecuteRequest(TransportRequest transportRequest, Sp
spanRecorder.recordHttpStatus(requestSpan, httpResponse.getCode());
}
- httpResponse.setEntity(wrapResponseEntity(httpResponse.getEntity(),
- httpResponse.getCode(),
- requestConfig));
+ httpResponse.setEntity(wrapResponseEntity(httpResponse.getEntity()));
if (httpResponse.containsHeader(ClickHouseHttpProto.HEADER_EXCEPTION_CODE)) {
throw readError(req, httpResponse);
@@ -925,7 +930,7 @@ private void addHeaders(HttpPost req, Map requestConfig) {
boolean serverCompression = ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getOrDefault(requestConfig);
boolean useHttpCompression = ClientConfigProperties.USE_HTTP_COMPRESSION.getOrDefault(requestConfig);
boolean appCompressedData = ClientConfigProperties.APP_COMPRESSED_DATA.getOrDefault(requestConfig);
- CompressionAlgorithm algorithm = ClientConfigProperties.COMPRESSION_ALGORITHM.getOrDefault(requestConfig);
+ CompressionAlgorithm algorithm = compressionAlgorithm(requestConfig);
if (algorithm != CompressionAlgorithm.NONE) {
if (serverCompression) {
@@ -974,7 +979,7 @@ private void addRequestParams(Map requestConfig, BiConsumer requestConfig, BiConsumer requestConfig) {
+ Object value = requestConfig.get(ClientConfigProperties.COMPRESSION_ALGORITHM.getKey());
+ if (value == null) {
+ return ClientConfigProperties.COMPRESSION_ALGORITHM.getDefObjVal();
+ }
+ return value instanceof CompressionAlgorithm
+ ? (CompressionAlgorithm) value
+ : CompressionAlgorithm.fromValue(String.valueOf(value));
+ }
+
private HttpEntity wrapRequestEntity(HttpEntity httpEntity, Map requestConfig) {
boolean clientCompression = ClientConfigProperties.COMPRESS_CLIENT_REQUEST.getOrDefault(requestConfig);
boolean useHttpCompression = ClientConfigProperties.USE_HTTP_COMPRESSION.getOrDefault(requestConfig);
boolean appCompressedData = ClientConfigProperties.APP_COMPRESSED_DATA.getOrDefault(requestConfig);
- CompressionAlgorithm algorithm = ClientConfigProperties.COMPRESSION_ALGORITHM.getOrDefault(requestConfig);
+ CompressionAlgorithm algorithm = compressionAlgorithm(requestConfig);
if (httpEntity.getContentEncoding() != null && !appCompressedData) {
// http header is set and data is not compressed
@@ -1036,7 +1055,7 @@ private HttpEntity wrapRequestEntity(HttpEntity httpEntity, Map
}
}
- private HttpEntity wrapResponseEntity(HttpEntity httpEntity, int httpStatus, Map requestConfig) {
+ private HttpEntity wrapResponseEntity(HttpEntity httpEntity) {
if (httpEntity.getContentEncoding() != null) {
// the algorithm of a compressed response is the one the request asked for
return new CompressedEntity(httpEntity, true, CompressorStreamFactory.getSingleton());
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java
index 9df39f407..04343bced 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java
@@ -4,6 +4,7 @@
import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.ClientConfigProperties;
import com.clickhouse.client.api.Session;
+import com.clickhouse.client.api.enums.CompressionAlgorithm;
import com.clickhouse.client.api.internal.CommonSettings;
import com.clickhouse.client.api.internal.ServerSettings;
import com.clickhouse.client.api.internal.ValidationUtils;
@@ -241,6 +242,19 @@ public TimeZone getServerTimeZone() {
* @return same instance of the builder
* @see Client.Builder#httpHeaders(Map)
*/
+ /**
+ * Algorithm of a compressed response body of this operation. The algorithm is requested with the HTTP
+ * content coding of the operation, so a compressed body always uses the algorithm set here.
+ * {@link CompressionAlgorithm#NONE} disables compression. Defaults to the algorithm of the client.
+ *
+ * @param algorithm - algorithm of a compressed body
+ * @return same instance of the settings
+ */
+ public QuerySettings compressionAlgorithm(CompressionAlgorithm algorithm) {
+ settings.setOption(ClientConfigProperties.COMPRESSION_ALGORITHM.getKey(), algorithm);
+ return this;
+ }
+
public QuerySettings httpHeader(String key, String value) {
settings.httpHeader(key, value);
return this;
diff --git a/client-v2/src/test/java/com/clickhouse/client/api/CompressionRequestUnitTest.java b/client-v2/src/test/java/com/clickhouse/client/api/CompressionRequestUnitTest.java
new file mode 100644
index 000000000..c9867ef9a
--- /dev/null
+++ b/client-v2/src/test/java/com/clickhouse/client/api/CompressionRequestUnitTest.java
@@ -0,0 +1,127 @@
+package com.clickhouse.client.api;
+
+import com.clickhouse.client.api.enums.CompressionAlgorithm;
+import com.clickhouse.client.api.enums.Protocol;
+import com.clickhouse.client.api.query.QuerySettings;
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.common.ConsoleNotifier;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.http.Request;
+import com.github.tomakehurst.wiremock.verification.LoggedRequest;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.util.List;
+
+public class CompressionRequestUnitTest {
+
+ private WireMockServer server;
+
+ @BeforeClass(groups = {"unit"})
+ public void startServer() {
+ server = new WireMockServer(WireMockConfiguration.options()
+ .dynamicPort()
+ .notifier(new ConsoleNotifier(false)));
+ server.start();
+ server.addStubMapping(WireMock.post(WireMock.anyUrl())
+ .willReturn(WireMock.aResponse().withBody("")).build());
+ }
+
+ @AfterClass(groups = {"unit"})
+ public void stopServer() {
+ if (server != null) {
+ server.stop();
+ }
+ }
+
+ @DataProvider(name = "responseCompressionRequests")
+ public static Object[][] responseCompressionRequests() {
+ return new Object[][]{
+ // algorithm -> content coding the response is requested with (null: no compression requested)
+ {CompressionAlgorithm.LZ4, "lz4"},
+ {CompressionAlgorithm.ZSTD, "zstd"},
+ {CompressionAlgorithm.GZIP, "gzip"},
+ {CompressionAlgorithm.NONE, null},
+ };
+ }
+
+ @Test(groups = {"unit"}, dataProvider = "responseCompressionRequests")
+ public void testResponseCompressionRequestedWithContentCoding(CompressionAlgorithm algorithm, String coding) {
+ LoggedRequest request = runQuery(builder -> builder.compressionAlgorithm(algorithm), null);
+
+ Assert.assertEquals(header(request, "Accept-Encoding"), coding);
+ // the codec of the compress=1 framing is the one of the server, so it is never requested
+ Assert.assertFalse(request.queryParameter("compress").isPresent(),
+ "compress=1 must not be requested");
+ Assert.assertEquals(request.queryParameter("enable_http_compression").isPresent(), coding != null);
+ }
+
+ @Test(groups = {"unit"})
+ public void testAlgorithmDefaultsToLz4() {
+ LoggedRequest request = runQuery(builder -> builder, null);
+
+ Assert.assertEquals(header(request, "Accept-Encoding"), "lz4");
+ Assert.assertFalse(request.queryParameter("compress").isPresent());
+ }
+
+ @Test(groups = {"unit"})
+ public void testOperationOverridesClientAlgorithm() {
+ LoggedRequest request = runQuery(builder -> builder.compressionAlgorithm(CompressionAlgorithm.LZ4),
+ new QuerySettings().compressionAlgorithm(CompressionAlgorithm.GZIP));
+
+ Assert.assertEquals(header(request, "Accept-Encoding"), "gzip");
+ }
+
+ @Test(groups = {"unit"})
+ public void testOperationAcceptsAlgorithmName() {
+ LoggedRequest request = runQuery(builder -> builder,
+ (QuerySettings) new QuerySettings()
+ .setOption(ClientConfigProperties.COMPRESSION_ALGORITHM.getKey(), "zstd"));
+
+ Assert.assertEquals(header(request, "Accept-Encoding"), "zstd");
+ }
+
+ @Test(groups = {"unit"})
+ public void testRequestCompressedWithContentCodingOfAlgorithm() {
+ LoggedRequest request = runQuery(builder -> builder
+ .compressionAlgorithm(CompressionAlgorithm.GZIP)
+ .compressClientRequest(true)
+ .useHttpCompression(true), null);
+
+ Assert.assertEquals(header(request, "Content-Encoding"), "gzip");
+ }
+
+ private LoggedRequest runQuery(java.util.function.UnaryOperator configure,
+ QuerySettings settings) {
+ server.resetRequests();
+ Client.Builder builder = new Client.Builder()
+ .addEndpoint(Protocol.HTTP, "localhost", server.port(), false)
+ .setUsername("default")
+ .setPassword("")
+ .retryOnFailures();
+
+ try (Client client = configure.apply(builder).build()) {
+ try {
+ if (settings == null) {
+ client.query("SELECT 1").get();
+ } else {
+ client.query("SELECT 1", settings).get();
+ }
+ } catch (Exception e) {
+ // the stub answers an empty body, so only the request itself is of interest here
+ }
+ }
+
+ List requests = server.findAll(WireMock.postRequestedFor(WireMock.anyUrl()));
+ Assert.assertEquals(requests.size(), 1, "expected exactly one request");
+ return requests.get(0);
+ }
+
+ private static String header(Request request, String name) {
+ return request.containsHeader(name) ? request.getHeader(name) : null;
+ }
+}
diff --git a/docs/releases/0_11_0.md b/docs/releases/0_11_0.md
index 682e90089..443dab628 100644
--- a/docs/releases/0_11_0.md
+++ b/docs/releases/0_11_0.md
@@ -16,8 +16,9 @@ a compressed body is always the one the client asked for:
algorithm out of `LZ4` (default), `ZSTD`, `GZIP` and `NONE`. Both the name and the content-coding token of an
algorithm are accepted, in any case.
- The default is `LZ4`, so an application that does not set the property keeps reading `LZ4` on every server version.
-- `ZSTD` needs `com.github.luben:zstd-jni` on the classpath. The dependency stays `provided`, so an application that
- selects `ZSTD` has to declare it.
+- `ZSTD` needs `com.github.luben:zstd-jni` on the classpath, which the client does not bring: the dependency of
+ `clickhouse-jdbc` stays `provided`, so packaging is unchanged and an application that selects `ZSTD` declares the
+ dependency itself.
- `NONE` disables compression of the request and the response, whatever `compress` and `decompress` are set to.
What to check in an application:
From d34138194ea38df14e289d46463c95a0029e977a Mon Sep 17 00:00:00 2001
From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com>
Date: Tue, 8 Sep 2026 19:40:25 +0000
Subject: [PATCH 5/6] Count the new default setting and cover the request
contract of an operation
The client now seeds one more default setting, so the canary counts of
ClientTests, which the test itself asks to increment when a setting is added,
move to the new size. They were the whole failure of the client-v2 legs: the
failing module stopped before the JaCoCo merge of the coverage profile, which
reports the coverage of new code as zero.
Two paths of the new setting had no test: an insert that selects its own
algorithm, and a request compressed without http compression, whose framing
stays the ClickHouse one while the response still follows the algorithm.
The javadoc of httpHeader(String, String) is restored - the new method of
QuerySettings was inserted between the javadoc and its method.
---
.../client/api/query/QuerySettings.java | 18 ++---
.../com/clickhouse/client/ClientTests.java | 6 +-
.../api/CompressionRequestUnitTest.java | 68 ++++++++++++++++---
3 files changed, 72 insertions(+), 20 deletions(-)
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java
index 04343bced..100694dac 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java
@@ -233,15 +233,6 @@ public TimeZone getServerTimeZone() {
return (TimeZone) settings.getOption(ClientConfigProperties.SERVER_TIMEZONE.getKey());
}
- /**
- * Defines list of headers that should be sent with current request. The Client will use a header value
- * defined in {@code headers} instead of any other.
- *
- * @param key - header name.
- * @param value - header value.
- * @return same instance of the builder
- * @see Client.Builder#httpHeaders(Map)
- */
/**
* Algorithm of a compressed response body of this operation. The algorithm is requested with the HTTP
* content coding of the operation, so a compressed body always uses the algorithm set here.
@@ -255,6 +246,15 @@ public QuerySettings compressionAlgorithm(CompressionAlgorithm algorithm) {
return this;
}
+ /**
+ * Defines list of headers that should be sent with current request. The Client will use a header value
+ * defined in {@code headers} instead of any other.
+ *
+ * @param key - header name.
+ * @param value - header value.
+ * @return same instance of the builder
+ * @see Client.Builder#httpHeaders(Map)
+ */
public QuerySettings httpHeader(String key, String value) {
settings.httpHeader(key, value);
return this;
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..a9c2342f8 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()
@@ -367,7 +367,7 @@ public void testDefaultSettings() {
.binaryStringSupport(true)
.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");
@@ -437,7 +437,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/api/CompressionRequestUnitTest.java b/client-v2/src/test/java/com/clickhouse/client/api/CompressionRequestUnitTest.java
index c9867ef9a..a722e1693 100644
--- a/client-v2/src/test/java/com/clickhouse/client/api/CompressionRequestUnitTest.java
+++ b/client-v2/src/test/java/com/clickhouse/client/api/CompressionRequestUnitTest.java
@@ -2,7 +2,9 @@
import com.clickhouse.client.api.enums.CompressionAlgorithm;
import com.clickhouse.client.api.enums.Protocol;
+import com.clickhouse.client.api.insert.InsertSettings;
import com.clickhouse.client.api.query.QuerySettings;
+import com.clickhouse.data.ClickHouseFormat;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.common.ConsoleNotifier;
@@ -15,7 +17,10 @@
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
import java.util.List;
+import java.util.function.UnaryOperator;
public class CompressionRequestUnitTest {
@@ -95,16 +100,51 @@ public void testRequestCompressedWithContentCodingOfAlgorithm() {
Assert.assertEquals(header(request, "Content-Encoding"), "gzip");
}
- private LoggedRequest runQuery(java.util.function.UnaryOperator configure,
- QuerySettings settings) {
+ @Test(groups = {"unit"})
+ public void testRequestFramingStaysLz4WithoutHttpCompression() {
+ LoggedRequest request = runQuery(builder -> builder
+ .compressionAlgorithm(CompressionAlgorithm.GZIP)
+ .compressClientRequest(true)
+ .useHttpCompression(false), null);
+
+ // the ClickHouse framing of a request is LZ4, so the algorithm applies to the response only
+ Assert.assertNull(header(request, "Content-Encoding"));
+ Assert.assertEquals(header(request, "Accept-Encoding"), "gzip");
+ Assert.assertTrue(request.queryParameter("enable_http_compression").isPresent(),
+ "the response is compressed with the requested content coding");
+ Assert.assertFalse(request.queryParameter("compress").isPresent(),
+ "compress=1 must not be requested");
+ Assert.assertTrue(request.queryParameter("decompress").isPresent(),
+ "a request compressed without http compression keeps the ClickHouse framing");
+ }
+
+ @Test(groups = {"unit"})
+ public void testInsertOperationOverridesClientAlgorithm() {
server.resetRequests();
- Client.Builder builder = new Client.Builder()
- .addEndpoint(Protocol.HTTP, "localhost", server.port(), false)
- .setUsername("default")
- .setPassword("")
- .retryOnFailures();
+ try (Client client = newBuilder()
+ .compressionAlgorithm(CompressionAlgorithm.LZ4)
+ .compressClientRequest(true)
+ .useHttpCompression(true)
+ .build()) {
+ try {
+ client.insert("some_table",
+ new ByteArrayInputStream("1\n".getBytes(StandardCharsets.UTF_8)),
+ ClickHouseFormat.TSV,
+ new InsertSettings().compressionAlgorithm(CompressionAlgorithm.GZIP)).get();
+ } catch (Exception e) {
+ // the stub answers an empty body, so only the request itself is of interest here
+ }
+ }
- try (Client client = configure.apply(builder).build()) {
+ LoggedRequest request = lastRequest();
+ Assert.assertEquals(header(request, "Content-Encoding"), "gzip");
+ Assert.assertEquals(header(request, "Accept-Encoding"), "gzip");
+ }
+
+ private LoggedRequest runQuery(UnaryOperator configure,
+ QuerySettings settings) {
+ server.resetRequests();
+ try (Client client = configure.apply(newBuilder()).build()) {
try {
if (settings == null) {
client.query("SELECT 1").get();
@@ -116,6 +156,18 @@ private LoggedRequest runQuery(java.util.function.UnaryOperator
}
}
+ return lastRequest();
+ }
+
+ private Client.Builder newBuilder() {
+ return new Client.Builder()
+ .addEndpoint(Protocol.HTTP, "localhost", server.port(), false)
+ .setUsername("default")
+ .setPassword("")
+ .retryOnFailures();
+ }
+
+ private LoggedRequest lastRequest() {
List requests = server.findAll(WireMock.postRequestedFor(WireMock.anyUrl()));
Assert.assertEquals(requests.size(), 1, "expected exactly one request");
return requests.get(0);
From b776b5d4378855c34faaf99f3836b1c8fbe7fb1e Mon Sep 17 00:00:00 2001
From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com>
Date: Thu, 10 Sep 2026 09:10:17 +0000
Subject: [PATCH 6/6] test(client-v2): bump ClientTests default-settings canary
counts to 39/40/39
main bumped the same canary constants to 38/39/38 for a default property
added on its side. The two changes are textually identical, so the merge
kept one copy while both properties exist, leaving the expectation one
short of the real configuration size.
---
.../src/test/java/com/clickhouse/client/ClientTests.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
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 aef988818..45feadd70 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(), 38); // to check everything is set. Increment when new added.
+ Assert.assertEquals(config.size(), 39); // to check everything is set. Increment when new added.
}
try (Client client = new Client.Builder()
@@ -368,7 +368,7 @@ public void testDefaultSettings() {
.queryFormat(ClickHouseFormat.CSV.name())
.build()) {
Map config = client.getConfiguration();
- Assert.assertEquals(config.size(), 39); // to check everything is set. Increment when new added.
+ Assert.assertEquals(config.size(), 40); // 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");
@@ -438,7 +438,7 @@ public void testWithOldDefaults() {
Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match");
}
}
- 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.
}
}