com.clickhouse.client.api.Client Maven / Gradle / Ivy
Show all versions of client-v2 Show documentation
package com.clickhouse.client.api;
import com.clickhouse.client.ClickHouseClient;
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.ClickHouseRequest;
import com.clickhouse.client.ClickHouseResponse;
import com.clickhouse.client.api.command.CommandResponse;
import com.clickhouse.client.api.command.CommandSettings;
import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader;
import com.clickhouse.client.api.data_formats.NativeFormatReader;
import com.clickhouse.client.api.data_formats.RowBinaryFormatReader;
import com.clickhouse.client.api.data_formats.RowBinaryWithNamesAndTypesFormatReader;
import com.clickhouse.client.api.data_formats.RowBinaryWithNamesFormatReader;
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.Protocol;
import com.clickhouse.client.api.enums.ProxyType;
import com.clickhouse.client.api.insert.DataSerializationException;
import com.clickhouse.client.api.insert.InsertResponse;
import com.clickhouse.client.api.insert.InsertSettings;
import com.clickhouse.client.api.insert.POJOSerializer;
import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream;
import com.clickhouse.client.api.internal.ClientStatisticsHolder;
import com.clickhouse.client.api.internal.ClientV1AdaptorHelper;
import com.clickhouse.client.api.internal.HttpAPIClientHelper;
import com.clickhouse.client.api.internal.MapUtils;
import com.clickhouse.client.api.internal.SerializerUtils;
import com.clickhouse.client.api.internal.SettingsConverter;
import com.clickhouse.client.api.internal.TableSchemaParser;
import com.clickhouse.client.api.internal.ValidationUtils;
import com.clickhouse.client.api.metadata.TableSchema;
import com.clickhouse.client.api.metrics.ClientMetrics;
import com.clickhouse.client.api.metrics.OperationMetrics;
import com.clickhouse.client.api.query.GenericRecord;
import com.clickhouse.client.api.query.POJOSetter;
import com.clickhouse.client.api.query.QueryResponse;
import com.clickhouse.client.api.query.QuerySettings;
import com.clickhouse.client.api.query.Records;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.config.ClickHouseDefaults;
import com.clickhouse.client.http.ClickHouseHttpProto;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.data.ClickHouseColumn;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.data.ClickHouseFormat;
import com.clickhouse.data.format.BinaryStreamUtils;
import org.apache.hc.client5.http.ConnectTimeoutException;
import org.apache.hc.core5.concurrent.DefaultThreadFactory;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NoHttpResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import static java.time.temporal.ChronoUnit.MILLIS;
import static java.time.temporal.ChronoUnit.SECONDS;
/**
* Client is the starting point for all interactions with ClickHouse.
*
* {@link Builder} is designed to construct a client object with required configuration:
* {@code
*
* Client client = new Client.Builder()
* .addEndpoint(Protocol.HTTP, node.getHost(), node.getPort())
* .addUsername("default")
* .build();
* }
*
*
*
* When client object is created any operation method can be called on it:
* {@code
*
* if (client.ping()) {
* QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.RowBinaryWithNamesAndTypes);
* try (QueryResponse response = client.query("SELECT * FROM " + table, settings).get(10, TimeUnit.SECONDS)) {
* ...
* }
* }
*
* }
*
*
*
* Client is thread-safe. It uses exclusive set of object to perform an operation.
*
*/
public class Client implements AutoCloseable {
private HttpAPIClientHelper httpClientHelper = null;
private final Set endpoints;
private final Map configuration;
private final List serverNodes = new ArrayList<>();
// POJO serializer mapping (class -> (schema -> (format -> serializer)))
private final Map, Map>> serializers;
// POJO deserializer mapping (class -> (schema -> (format -> deserializer)))
private final Map, Map>> deserializers;
private static final Logger LOG = LoggerFactory.getLogger(Client.class);
private final ExecutorService sharedOperationExecutor;
private final Map globalClientStats = new ConcurrentHashMap<>();
private boolean useNewImplementation = false;
private ClickHouseClient oldClient = null;
private Map tableSchemaCache = new ConcurrentHashMap<>();
private Map tableSchemaHasDefaults = new ConcurrentHashMap<>();
private Client(Set endpoints, Map configuration, boolean useNewImplementation,
ExecutorService sharedOperationExecutor) {
this.endpoints = endpoints;
this.configuration = configuration;
this.endpoints.forEach(endpoint -> {
this.serverNodes.add(ClickHouseNode.of(endpoint, this.configuration));
});
this.serializers = new ConcurrentHashMap<>();
this.deserializers = new ConcurrentHashMap<>();
boolean isAsyncEnabled = MapUtils.getFlag(this.configuration, ClickHouseClientOption.ASYNC.getKey());
if (isAsyncEnabled && sharedOperationExecutor == null) {
this.sharedOperationExecutor = Executors.newCachedThreadPool(new DefaultThreadFactory("chc-operation"));
} else {
this.sharedOperationExecutor = sharedOperationExecutor;
}
this.useNewImplementation = useNewImplementation;
if (useNewImplementation) {
this.httpClientHelper = new HttpAPIClientHelper(configuration);
LOG.info("Using new http client implementation");
} else {
this.oldClient = ClientV1AdaptorHelper.createClient(configuration);
LOG.info("Using old http client implementation");
}
}
/**
* Returns default database name that will be used by operations if not specified.
*
* @return String - actual default database name.
*/
public String getDefaultDatabase() {
return this.configuration.get("database");
}
/**
* Frees the resources associated with the client.
*
* - Shuts down the shared operation executor by calling {@code shutdownNow()}
*
*/
@Override
public void close() {
try {
if (sharedOperationExecutor != null && !sharedOperationExecutor.isShutdown()) {
this.sharedOperationExecutor.shutdownNow();
}
} catch (Exception e) {
LOG.error("Failed to close shared operation executor", e);
}
if (oldClient != null) {
oldClient.close();
}
}
public static class Builder {
private Set endpoints;
// Read-only configuration
private Map configuration;
private boolean useNewImplementation = true;
private ExecutorService sharedOperationExecutor = null;
public Builder() {
this.endpoints = new HashSet<>();
this.configuration = new HashMap();
// TODO: set defaults configuration values
this.setConnectTimeout(30, SECONDS)
.setSocketTimeout(2, SECONDS)
.setSocketRcvbuf(804800)
.setSocketSndbuf(804800)
.compressServerResponse(true)
.compressClientRequest(false);
}
/**
* Server address to which client may connect. If there are multiple endpoints then client will
* connect to one of them.
* Acceptable formats are:
*
* - {@code http://localhost:8123}
* - {@code https://localhost:8443}
*
*
* @param endpoint - URL formatted string with protocol, host and port.
*/
public Builder addEndpoint(String endpoint) {
try {
URL endpointURL = new java.net.URL(endpoint);
if (endpointURL.getProtocol().equalsIgnoreCase("https")) {
addEndpoint(Protocol.HTTP, endpointURL.getHost(), endpointURL.getPort(), true);
} else if (endpointURL.getProtocol().equalsIgnoreCase("http")) {
addEndpoint(Protocol.HTTP, endpointURL.getHost(), endpointURL.getPort(), false);
} else {
throw new IllegalArgumentException("Only HTTP and HTTPS protocols are supported");
}
} catch (java.net.MalformedURLException e) {
throw new IllegalArgumentException("Endpoint should be a valid URL string", e);
}
return this;
}
/**
* Server address to which client may connect. If there are multiple endpoints then client will
* connect to one of them.
*
* @param protocol - Endpoint protocol
* @param host - Endpoint host
* @param port - Endpoint port
*/
public Builder addEndpoint(Protocol protocol, String host, int port, boolean secure) {
ValidationUtils.checkNonBlank(host, "host");
ValidationUtils.checkNotNull(protocol, "protocol");
ValidationUtils.checkRange(port, 1, ValidationUtils.TCP_PORT_NUMBER_MAX, "port");
if (secure) {
// For some reason com.clickhouse.client.http.ApacheHttpConnectionImpl.newConnection checks only client config
// for SSL, so we need to set it here. But it actually should be set for each node separately.
this.configuration.put(ClickHouseClientOption.SSL.getKey(), "true");
}
String endpoint = String.format("%s%s://%s:%d", protocol.toString().toLowerCase(), secure ? "s": "", host, port);
this.endpoints.add(endpoint);
return this;
}
/**
* Sets a configuration option. This method can be used to set any configuration option.
* There is no specific validation is done on the key or value.
*
* @param key - configuration option name
* @param value - configuration option value
*/
public Builder setOption(String key, String value) {
this.configuration.put(key, value);
return this;
}
/**
* Username for authentication with server. Required for all operations.
* Same username will be used for all endpoints.
*
* @param username - a valid username
*/
public Builder setUsername(String username) {
this.configuration.put("user", username);
return this;
}
/**
* Password for authentication with server. Required for all operations.
* Same password will be used for all endpoints.
*
* @param password - plain text password
*/
public Builder setPassword(String password) {
this.configuration.put("password", password);
return this;
}
/**
* Access token for authentication with server. Required for all operations.
* Same access token will be used for all endpoints.
*
* @param accessToken - plain text access token
*/
public Builder setAccessToken(String accessToken) {
this.configuration.put("access_token", accessToken);
return this;
}
/**
* Configures client to use build-in connection pool
* @param enable - if connection pool should be enabled
* @return
*/
public Builder enableConnectionPool(boolean enable) {
this.configuration.put("connection_pool_enabled", String.valueOf(enable));
return this;
}
/**
* Default connection timeout in milliseconds. Timeout is applied to establish a connection.
*
* @param timeout - connection timeout in milliseconds
*/
public Builder setConnectTimeout(long timeout) {
this.configuration.put("connect_timeout", String.valueOf(timeout));
return this;
}
/**
* Default connection timeout in milliseconds. Timeout is applied to establish a connection.
*
* @param timeout - connection timeout value
* @param unit - time unit
*/
public Builder setConnectTimeout(long timeout, ChronoUnit unit) {
return this.setConnectTimeout(Duration.of(timeout, unit).toMillis());
}
/**
* Set timeout for waiting a free connection from a pool when all connections are leased.
* This configuration is important when need to fail fast in high concurrent scenarios.
* Default is 10 s.
* @param timeout - connection timeout in milliseconds
* @param unit - time unit
*/
public Builder setConnectionRequestTimeout(long timeout, ChronoUnit unit) {
this.configuration.put("connection_request_timeout", String.valueOf(Duration.of(timeout, unit).toMillis()));
return this;
}
/**
* Sets the maximum number of connections that can be opened at the same time to a single server. Limit is not
* a hard stop. It is done to prevent threads stuck inside a connection pool waiting for a connection.
* Default is 10. It is recommended to set a higher value for a high concurrent applications. It will let
* more threads to get a connection and execute a query.
*
* @param maxConnections - maximum number of connections
*/
public Builder setMaxConnections(int maxConnections) {
this.configuration.put(ClickHouseHttpOption.MAX_OPEN_CONNECTIONS.getKey(), String.valueOf(maxConnections));
return this;
}
/**
* Sets how long any connection would be considered as active and able for a lease.
* After this time connection will be marked for sweep and will not be returned from a pool.
* Has more effect than keep-alive timeout.
* @param timeout - time in unit
* @param unit - time unit
* @return
*/
public Builder setConnectionTTL(long timeout, ChronoUnit unit) {
this.configuration.put(ClickHouseClientOption.CONNECTION_TTL.getKey(), String.valueOf(Duration.of(timeout, unit).toMillis()));
return this;
}
/**
* Sets keep alive timeout for a connection to override server value. If set to -1 then server value will be used.
* Default is -1.
* Doesn't override connection TTL value.
* {@see Client#setConnectionTTL}
* @param timeout - time in unit
* @param unit - time unit
* @return
*/
public Builder setKeepAliveTimeout(long timeout, ChronoUnit unit) {
this.configuration.put(ClickHouseHttpOption.KEEP_ALIVE_TIMEOUT.getKey(), String.valueOf(Duration.of(timeout, unit).toMillis()));
return this;
}
/**
* Sets strategy of how connections are reuse.
* Default is {@link ConnectionReuseStrategy#FIFO} to evenly distribute load between them.
*
* @param strategy - strategy for connection reuse
* @return
*/
public Builder setConnectionReuseStrategy(ConnectionReuseStrategy strategy) {
this.configuration.put("connection_reuse_strategy", strategy.name());
return this;
}
// SOCKET SETTINGS
/**
* Default socket timeout in milliseconds. Timeout is applied to read and write operations.
*
* @param timeout - socket timeout in milliseconds
*/
public Builder setSocketTimeout(long timeout) {
this.configuration.put("socket_timeout", String.valueOf(timeout));
return this;
}
/**
* Default socket timeout in milliseconds. Timeout is applied to read and write operations.
*
* @param timeout - socket timeout value
* @param unit - time unit
*/
public Builder setSocketTimeout(long timeout, ChronoUnit unit) {
return this.setSocketTimeout(Duration.of(timeout, unit).toMillis());
}
/**
* Default socket receive buffer size in bytes.
*
* @param size - socket receive buffer size in bytes
*/
public Builder setSocketRcvbuf(long size) {
this.configuration.put("socket_rcvbuf", String.valueOf(size));
return this;
}
/**
* Default socket send buffer size in bytes.
*
* @param size - socket send buffer size in bytes
*/
public Builder setSocketSndbuf(long size) {
this.configuration.put("socket_sndbuf", String.valueOf(size));
return this;
}
/**
* Default socket reuse address option.
*
* @param value - socket reuse address option
*/
public Builder setSocketReuseAddress(boolean value) {
this.configuration.put("socket_reuseaddr", String.valueOf(value));
return this;
}
/**
* Default socket keep alive option.If set to true socket will be kept alive
* until terminated by one of the parties.
*
* @param value - socket keep alive option
*/
public Builder setSocketKeepAlive(boolean value) {
this.configuration.put("socket_keepalive", String.valueOf(value));
return this;
}
/**
* Default socket tcp_no_delay option. If set to true, disables Nagle's algorithm.
*
* @param value - socket tcp no delay option
*/
public Builder setSocketTcpNodelay(boolean value) {
this.configuration.put("socket_tcp_nodelay", String.valueOf(value));
return this;
}
/**
* Default socket linger option. If set to true, socket will linger for the specified time.
*
* @param secondsToWait - socket linger time in seconds
*/
public Builder setSocketLinger(int secondsToWait) {
this.configuration.put("socket_linger", String.valueOf(secondsToWait));
return this;
}
/**
* Server response compression. If set to true server will compress the response.
* Has most effect for read operations.
*
* @param enabled - indicates if server response compression is enabled
*/
public Builder compressServerResponse(boolean enabled) {
this.configuration.put(ClickHouseClientOption.COMPRESS.getKey(), String.valueOf(enabled));
return this;
}
/**
* Client request compression. If set to true client will compress the request.
* Has most effect for write operations.
*
* @param enabled - indicates if client request compression is enabled
*/
public Builder compressClientRequest(boolean enabled) {
this.configuration.put(ClickHouseClientOption.DECOMPRESS.getKey(), String.valueOf(enabled));
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.
*
* @param enabled - indicates if http compression is enabled
* @return
*/
public Builder useHttpCompression(boolean enabled) {
this.configuration.put("client.use_http_compression", String.valueOf(enabled));
return this;
}
/**
* Sets buffer size for uncompressed data in LZ4 compression.
* For outgoing data it is the size of a buffer that will be compressed.
* For incoming data it is the size of a buffer that will be decompressed.
*
* @param size - size of the buffer in bytes
* @return
*/
public Builder setLZ4UncompressedBufferSize(int size) {
this.configuration.put("compression.lz4.uncompressed_buffer_size", String.valueOf(size));
return this;
}
/**
* Sets the default database name that will be used by operations if not specified.
* @param database - actual default database name.
*/
public Builder setDefaultDatabase(String database) {
this.configuration.put("database", database);
return this;
}
public Builder addProxy(ProxyType type, String host, int port) {
ValidationUtils.checkNotNull(type, "type");
ValidationUtils.checkNonBlank(host, "host");
ValidationUtils.checkRange(port, 1, ValidationUtils.TCP_PORT_NUMBER_MAX, "port");
this.configuration.put(ClickHouseClientOption.PROXY_TYPE.getKey(), type.name());
this.configuration.put(ClickHouseClientOption.PROXY_HOST.getKey(), host);
this.configuration.put(ClickHouseClientOption.PROXY_PORT.getKey(), String.valueOf(port));
return this;
}
public Builder setProxyCredentials(String user, String pass) {
this.configuration.put("proxy_user", user);
this.configuration.put("proxy_password", pass);
return this;
}
/**
* Sets the maximum time for operation to complete. By default, it is set to 3 hours.
* @param timeout
* @param timeUnit
* @return
*/
public Builder setExecutionTimeout(long timeout, ChronoUnit timeUnit) {
this.configuration.put(ClickHouseClientOption.MAX_EXECUTION_TIME.getKey(), String.valueOf(Duration.of(timeout, timeUnit).toMillis()));
return this;
}
/**
* Switches to new implementation of the client. Default is true.
* @deprecated
*/
public Builder useNewImplementation(boolean useNewImplementation) {
this.useNewImplementation = useNewImplementation;
return this;
}
public Builder setHttpCookiesEnabled(boolean enabled) {
//TODO: extract to settings string constants
this.configuration.put("client.http.cookies_enabled", String.valueOf(enabled));
return this;
}
/**
* Defines path to the trust store file. It cannot be combined with
* certificates. Either trust store or certificates should be used.
* {@see setSSLTrustStorePassword} and {@see setSSLTrustStoreType}
* @param path
* @return
*/
public Builder setSSLTrustStore(String path) {
this.configuration.put(ClickHouseClientOption.TRUST_STORE.getKey(), path);
return this;
}
/**
* Password for the SSL Trust Store.
*
* @param password
* @return
*/
public Builder setSSLTrustStorePassword(String password) {
this.configuration.put(ClickHouseClientOption.KEY_STORE_PASSWORD.getKey(), password);
return this;
}
/**
* Type of the SSL Trust Store. Usually JKS
*
* @param type
* @return
*/
public Builder setSSLTrustStoreType(String type) {
this.configuration.put(ClickHouseClientOption.KEY_STORE_TYPE.getKey(), type);
return this;
}
/**
* Defines path to the key store file. It cannot be combined with
* certificates. Either key store or certificates should be used.
*
* {@see setSSLKeyStorePassword} and {@see setSSLKeyStoreType}
* @param path
* @return
*/
public Builder setRootCertificate(String path) {
this.configuration.put(ClickHouseClientOption.SSL_ROOT_CERTIFICATE.getKey(), path);
return this;
}
/**
* Client certificate for mTLS.
* @param path
* @return
*/
public Builder setClientCertificate(String path) {
this.configuration.put(ClickHouseClientOption.SSL_CERTIFICATE.getKey(), path);
return this;
}
/**
* Client key for mTLS.
* @param path
* @return
*/
public Builder setClientKey(String path) {
this.configuration.put(ClickHouseClientOption.SSL_KEY.getKey(), path);
return this;
}
/**
* Configure client to use server timezone for date/datetime columns. Default is true.
* If this options is selected then server timezone should be set as well.
*
* @param useServerTimeZone - if to use server timezone
* @return
*/
public Builder useServerTimeZone(boolean useServerTimeZone) {
this.configuration.put(ClickHouseClientOption.USE_SERVER_TIME_ZONE.getKey(), String.valueOf(useServerTimeZone));
return this;
}
/**
* Configure client to use specified timezone. If set using server TimeZone should be
* set to false
*
* @param timeZone
* @return
*/
public Builder useTimeZone(String timeZone) {
this.configuration.put(ClickHouseClientOption.USE_TIME_ZONE.getKey(), timeZone);
return this;
}
/**
* Specify server timezone to use. If not set then UTC will be used.
*
* @param timeZone - server timezone
* @return
*/
public Builder setServerTimeZone(String timeZone) {
this.configuration.put(ClickHouseClientOption.SERVER_TIME_ZONE.getKey(), timeZone);
return this;
}
/**
* Configures client to execute requests in a separate thread. By default, operations (query, insert)
* are executed in the same thread as the caller.
* It is possible to set a shared executor for all operations. See {@link #setSharedOperationExecutor(ExecutorService)}
*
* Note: Async operations a using executor what expects having a queue of tasks for a pool of executors.
* The queue size limit is small it may quickly become a problem for scheduling new tasks.
*
* @param async - if to use async requests
* @return
*/
public Builder useAsyncRequests(boolean async) {
this.configuration.put(ClickHouseClientOption.ASYNC.getKey(), String.valueOf(async));
return this;
}
/**
* Sets an executor for running operations. If async operations are enabled and no executor is specified
* client will create a default executor.
*
* @param executorService - executor service for async operations
* @return
*/
public Builder setSharedOperationExecutor(ExecutorService executorService) {
this.sharedOperationExecutor = executorService;
return this;
}
/**
* Set size of a buffers that are used to read/write data from the server. It is mainly used to copy data from
* a socket to application memory and visa-versa. Setting is applied for both read and write operations.
* Default is 8192 bytes.
*
* @param size - size in bytes
* @return
*/
public Builder setClientNetworkBufferSize(int size) {
this.configuration.put("client_network_buffer_size", String.valueOf(size));
return this;
}
/**
* Sets list of causes that should be retried on.
* Default {@code [NoHttpResponse, ConnectTimeout, ConnectionRequestTimeout]}
* Use {@link ClientFaultCause#None} to disable retries.
*
* @param causes - list of causes
* @return
*/
public Builder retryOnFailures(ClientFaultCause ...causes) {
StringJoiner joiner = new StringJoiner(VALUES_LIST_DELIMITER);
for (ClientFaultCause cause : causes) {
joiner.add(cause.name());
}
this.configuration.put("client_retry_on_failures", joiner.toString());
return this;
}
public Builder setMaxRetries(int maxRetries) {
this.configuration.put(ClickHouseClientOption.RETRY.getKey(), String.valueOf(maxRetries));
return this;
}
/**
* Configures client to reuse allocated byte buffers for numbers. It affects how binary format reader is working.
* If set to 'true' then {@link Client#newBinaryFormatReader(QueryResponse)} will construct reader that will
* reuse buffers for numbers. It improves performance for large datasets by reducing number of allocations
* (therefore GC pressure).
* Enabling this feature is safe because each reader suppose to be used by a single thread and readers are not reused.
*
* Default is false.
* @param reuse - if to reuse buffers
* @return
*/
public Builder allowBinaryReaderToReuseBuffers(boolean reuse) {
this.configuration.put("client_allow_binary_reader_to_reuse_buffers", String.valueOf(reuse));
return this;
}
/**
* Defines list of headers that should be sent with each request. The Client will use a header value
* defined in {@code headers} instead of any other.
* Operation settings may override these headers.
*
* @see InsertSettings#httpHeaders(Map)
* @see QuerySettings#httpHeaders(Map)
* @see CommandSettings#httpHeaders(Map)
* @param key - a name of the header.
* @param value - a value of the header.
* @return same instance of the builder
*/
public Builder httpHeader(String key, String value) {
this.configuration.put(ClientSettings.HTTP_HEADER_PREFIX + key, value);
return this;
}
/**
* {@see #httpHeader(String, String)} but for multiple values.
* @param key - name of the header
* @param values - collection of values
* @return same instance of the builder
*/
public Builder httpHeader(String key, Collection values) {
this.configuration.put(ClientSettings.HTTP_HEADER_PREFIX + key, ClientSettings.commaSeparated(values));
return this;
}
/**
* {@see #httpHeader(String, String)} but for multiple headers.
* @param headers - map of headers
* @return same instance of the builder
*/
public Builder httpHeaders(Map headers) {
headers.forEach(this::httpHeader);
return this;
}
/**
* Defines list of server settings that should be sent with each request. The Client will use a setting value
* defined in {@code settings} instead of any other.
* Operation settings may override these values.
*
* @see InsertSettings#serverSetting(String, String) (Map)
* @see QuerySettings#serverSetting(String, String) (Map)
* @see CommandSettings#serverSetting(String, String) (Map)
* @param name - name of the setting without special prefix
* @param value - value of the setting
* @return same instance of the builder
*/
public Builder serverSetting(String name, String value) {
this.configuration.put(ClientSettings.SERVER_SETTING_PREFIX + name, value);
return this;
}
/**
* {@see #serverSetting(String, String)} but for multiple values.
* @param name - name of the setting without special prefix
* @param values - collection of values
* @return same instance of the builder
*/
public Builder serverSetting(String name, Collection values) {
this.configuration.put(ClientSettings.SERVER_SETTING_PREFIX + name, ClientSettings.commaSeparated(values));
return this;
}
public Client build() {
setDefaults();
// check if endpoint are empty. so can not initiate client
if (this.endpoints.isEmpty()) {
throw new IllegalArgumentException("At least one endpoint is required");
}
// check if username and password are empty. so can not initiate client?
if (!this.configuration.containsKey("access_token") && (!this.configuration.containsKey("user") || !this.configuration.containsKey("password"))) {
throw new IllegalArgumentException("Username and password are required");
}
if (this.configuration.containsKey(ClickHouseClientOption.TRUST_STORE) &&
this.configuration.containsKey(ClickHouseClientOption.SSL_CERTIFICATE)) {
throw new IllegalArgumentException("Trust store and certificates cannot be used together");
}
// Check timezone settings
String useTimeZoneValue = this.configuration.get(ClickHouseClientOption.USE_TIME_ZONE.getKey());
String serverTimeZoneValue = this.configuration.get(ClickHouseClientOption.SERVER_TIME_ZONE.getKey());
boolean useServerTimeZone = MapUtils.getFlag(this.configuration, ClickHouseClientOption.USE_SERVER_TIME_ZONE.getKey());
if (useTimeZoneValue != null) {
if (useServerTimeZone) {
throw new IllegalArgumentException("USE_TIME_ZONE option cannot be used when using server timezone");
}
try {
LOG.info("Using timezone: {} instead of server one", ZoneId.of(useTimeZoneValue));
} catch (Exception e) {
throw new IllegalArgumentException("Invalid timezone value: " + useTimeZoneValue);
}
} else if (useServerTimeZone) {
if (serverTimeZoneValue == null) {
serverTimeZoneValue = "UTC";
}
try {
LOG.info("Using server timezone: {}", ZoneId.of(serverTimeZoneValue));
} catch (Exception e) {
throw new IllegalArgumentException("Invalid server timezone value: " + serverTimeZoneValue);
}
} else {
throw new IllegalArgumentException("Nor server timezone nor specific timezone is set");
}
return new Client(this.endpoints, this.configuration, this.useNewImplementation, this.sharedOperationExecutor);
}
private static final int DEFAULT_NETWORK_BUFFER_SIZE = 300_000;
private void setDefaults() {
// set default database name if not specified
if (!configuration.containsKey("database")) {
setDefaultDatabase((String) ClickHouseDefaults.DATABASE.getDefaultValue());
}
if (!configuration.containsKey(ClickHouseClientOption.MAX_EXECUTION_TIME.getKey())) {
setExecutionTimeout(0, MILLIS);
}
if (!configuration.containsKey(ClickHouseClientOption.MAX_THREADS_PER_CLIENT.getKey())) {
configuration.put(ClickHouseClientOption.MAX_THREADS_PER_CLIENT.getKey(),
String.valueOf(ClickHouseClientOption.MAX_THREADS_PER_CLIENT.getDefaultValue()));
}
if (!configuration.containsKey("compression.lz4.uncompressed_buffer_size")) {
setLZ4UncompressedBufferSize(ClickHouseLZ4OutputStream.UNCOMPRESSED_BUFF_SIZE);
}
if (!configuration.containsKey(ClickHouseClientOption.USE_SERVER_TIME_ZONE.getKey())) {
useServerTimeZone(true);
}
if (!configuration.containsKey(ClickHouseClientOption.SERVER_TIME_ZONE.getKey())) {
setServerTimeZone("UTC");
}
if (!configuration.containsKey(ClickHouseClientOption.ASYNC.getKey())) {
useAsyncRequests(false);
}
if (!configuration.containsKey(ClickHouseHttpOption.MAX_OPEN_CONNECTIONS.getKey())) {
setMaxConnections(10);
}
if (!configuration.containsKey("connection_request_timeout")) {
setConnectionRequestTimeout(10, SECONDS);
}
if (!configuration.containsKey("connection_reuse_strategy")) {
setConnectionReuseStrategy(ConnectionReuseStrategy.FIFO);
}
if (!configuration.containsKey("connection_pool_enabled")) {
enableConnectionPool(true);
}
if (!configuration.containsKey("connection_ttl")) {
setConnectionTTL(-1, MILLIS);
}
if (!configuration.containsKey("client_retry_on_failures")) {
retryOnFailures(ClientFaultCause.NoHttpResponse, ClientFaultCause.ConnectTimeout, ClientFaultCause.ConnectionRequestTimeout);
}
if (!configuration.containsKey("client_network_buffer_size")) {
setClientNetworkBufferSize(DEFAULT_NETWORK_BUFFER_SIZE);
}
if (!configuration.containsKey(ClickHouseClientOption.RETRY.getKey())) {
setMaxRetries(3);
}
if (!configuration.containsKey("client_allow_binary_reader_to_reuse_buffers")) {
allowBinaryReaderToReuseBuffers(false);
}
}
}
private ClickHouseNode getServerNode() {
// TODO: implement load balancing using existing logic
return this.serverNodes.get(0);
}
/**
* Pings the server to check if it is alive
* @return true if the server is alive, false otherwise
*/
public boolean ping() {
return ping(getOperationTimeout());
}
/**
* Pings the server to check if it is alive. Maximum timeout is 10 minutes.
*
* @param timeout timeout in milliseconds
* @return true if the server is alive, false otherwise
*/
public boolean ping(long timeout) {
if (useNewImplementation) {
try (QueryResponse response = query("SELECT 1 FORMAT TabSeparated").get(timeout, TimeUnit.MILLISECONDS)) {
return true;
} catch (Exception e) {
return false;
}
} else {
return oldClient.ping(getServerNode(), Math.toIntExact(timeout));
}
}
/**
* Registers a POJO class and maps its fields to a table schema
* Note: table schema will be stored in cache to be used while other operations. Cache key is
* {@link TableSchema schemaId}. Call this method
* to update cache.
*
* @param clazz - class of a POJO
* @param schema - correlating table schema
*/
public synchronized void register(Class> clazz, TableSchema schema) {
LOG.debug("Registering POJO: {}", clazz.getName());
String schemaKey;
if (schema.getTableName() != null && schema.getQuery() == null) {
schemaKey = schema.getTableName();
} else if (schema.getQuery() != null && schema.getTableName() == null) {
schemaKey = schema.getQuery();
} else {
throw new IllegalArgumentException("Table schema has both query and table name set. Only one is allowed.");
}
tableSchemaCache.put(schemaKey, schema);
//Create a new POJOSerializer with static .serialize(object, columns) methods
Map classGetters = new HashMap<>();
Map classSetters = new HashMap<>();
for (Method method : clazz.getMethods()) {//Clean up the method names
String methodName = method.getName();
if (methodName.startsWith("get") || methodName.startsWith("has")) {
methodName = methodName.substring(3).toLowerCase();
classGetters.put(methodName, method);
} else if (methodName.startsWith("is")) {
methodName = methodName.substring(2).toLowerCase();
classGetters.put(methodName, method);
} else if (methodName.startsWith("set")) {
methodName = methodName.substring(3).toLowerCase();
classSetters.put(methodName, method);
}
}
Map schemaSerializers = new HashMap<>();
Map schemaDeserializers = new ConcurrentHashMap<>();
boolean defaultsSupport = schema.hasDefaults();
tableSchemaHasDefaults.put(schemaKey, defaultsSupport);
for (ClickHouseColumn column : schema.getColumns()) {
String propertyName = column.getColumnName().toLowerCase().replace("_", "").replace(".", "");
Method getterMethod = classGetters.get(propertyName);
if (getterMethod != null) {
schemaSerializers.put(column.getColumnName(), (obj, stream) -> {
Object value = getterMethod.invoke(obj);
if (defaultsSupport) {
if (value != null) {//Because we now support defaults, we have to send nonNull
BinaryStreamUtils.writeNonNull(stream);//Write 0 for no default
if (column.isNullable()) {//If the column is nullable
BinaryStreamUtils.writeNonNull(stream);//Write 0 for not null
}
} else {//So if the object is null
if (column.hasDefault()) {
BinaryStreamUtils.writeNull(stream);//Send 1 for default
return;
} else if (column.isNullable()) {//And the column is nullable
BinaryStreamUtils.writeNonNull(stream);
BinaryStreamUtils.writeNull(stream);//Then we send null, write 1
return;//And we're done
} else if (column.getDataType() == ClickHouseDataType.Array) {//If the column is an array
BinaryStreamUtils.writeNonNull(stream);//Then we send nonNull
} else {
throw new IllegalArgumentException(String.format("An attempt to write null into not nullable column '%s'", column.getColumnName()));
}
}
} else {
// If column is nullable && the object is also null add the not null marker
if (column.isNullable() && value != null) {
BinaryStreamUtils.writeNonNull(stream);
}
if (!column.isNullable() && value == null) {
if (column.getDataType() == ClickHouseDataType.Array)
BinaryStreamUtils.writeNonNull(stream);
else
throw new IllegalArgumentException(String.format("An attempt to write null into not nullable column '%s'", column.getColumnName()));
}
}
//Handle the different types
SerializerUtils.serializeData(stream, value, column);
});
} else {
LOG.warn("No getter method found for column: {}", propertyName);
}
// Deserialization stuff
Method setterMethod = classSetters.get(propertyName);
if (setterMethod != null) {
schemaDeserializers.put(column.getColumnName(), SerializerUtils.compilePOJOSetter(setterMethod, column));
} else {
LOG.warn("No setter method found for column: {}", propertyName);
}
}
Map> classSerializers = serializers.computeIfAbsent(clazz, k -> new HashMap<>());
Map> classDeserializers = deserializers.computeIfAbsent(clazz, k -> new HashMap<>());
classSerializers.put(schemaKey, schemaSerializers);
classDeserializers.put(schemaKey, schemaDeserializers);
}
/**
* Sends write request to database. List of objects is converted into a most suitable format
* then it is sent to a server. Members of the list must be pre-registered using
* {@link #register(Class, TableSchema)} method:
*
* {@code
* client.register(SamplePOJO.class, tableSchema);
* List
*
* @param tableName - destination table name
* @param data - data stream to insert
* @return {@code CompletableFuture} - a promise to insert response
*/
public CompletableFuture insert(String tableName, List> data) {
return insert(tableName, data, new InsertSettings());
}
/**
* Sends write request to database. List of objects is converted into a most suitable format
* then it is sent to a server. Members of the list must be pre-registered using
* {@link #register(Class, TableSchema)} method:
*
* {@code
* client.register(SamplePOJO.class, tableSchema);
* List
*
* @param tableName - destination table name
* @param data - data stream to insert
* @param settings - insert operation settings
* @throws IllegalArgumentException when data is empty or not registered
* @return {@code CompletableFuture} - a promise to insert response
*/
public CompletableFuture insert(String tableName, List> data, InsertSettings settings) {
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("Data cannot be empty");
}
String operationId = registerOperationMetrics();
settings.setOperationId(operationId);
if (useNewImplementation) {
globalClientStats.get(operationId).start(ClientMetrics.OP_DURATION);
}
globalClientStats.get(operationId).start(ClientMetrics.OP_SERIALIZATION);
//Add format to the settings
if (settings == null) {
settings = new InsertSettings();
}
boolean hasDefaults = this.tableSchemaHasDefaults.get(tableName);
ClickHouseFormat format = hasDefaults? ClickHouseFormat.RowBinaryWithDefaults : ClickHouseFormat.RowBinary;
TableSchema tableSchema = tableSchemaCache.get(tableName);
if (tableSchema == null) {
throw new IllegalArgumentException("Table schema not found for table: " + tableName + ". Did you forget to register it?");
}
//Lookup the Serializer for the POJO
Map classSerializers = serializers.getOrDefault(data.get(0).getClass(), Collections.emptyMap())
.getOrDefault(tableName, Collections.emptyMap());
List serializersForTable = new ArrayList<>();
for (ClickHouseColumn column : tableSchema.getColumns()) {
POJOSerializer serializer = classSerializers.get(column.getColumnName());
if (serializer == null) {
throw new IllegalArgumentException("No serializer found for column '" + column.getColumnName() + "'. Did you forget to register it?");
}
serializersForTable.add(serializer);
}
if (useNewImplementation) {
String retry = configuration.get(ClickHouseClientOption.RETRY.getKey());
final int maxRetries = retry == null ? (int) ClickHouseClientOption.RETRY.getDefaultValue() : Integer.parseInt(retry);
settings.setOption(ClickHouseClientOption.FORMAT.getKey(), format.name());
final InsertSettings finalSettings = settings;
Supplier supplier = () -> {
// Selecting some node
ClickHouseNode selectedNode = getNextAliveNode();
ClientException lastException = null;
for (int i = 0; i <= maxRetries; i++) {
// Execute request
try (ClassicHttpResponse httpResponse =
httpClientHelper.executeRequest(selectedNode, finalSettings.getAllSettings(),
out -> {
out.write("INSERT INTO ".getBytes());
out.write(tableName.getBytes());
out.write(" \n FORMAT ".getBytes());
out.write(format.name().getBytes());
out.write(" \n".getBytes());
for (Object obj : data) {
for (POJOSerializer serializer : serializersForTable) {
try {
serializer.serialize(obj, out);
} catch (InvocationTargetException | IllegalAccessException | IOException e) {
throw new DataSerializationException(obj, serializer, e);
}
}
}
out.close();
})) {
// Check response
if (httpResponse.getCode() == HttpStatus.SC_SERVICE_UNAVAILABLE) {
LOG.warn("Failed to get response. Server returned {}. Retrying.", httpResponse.getCode());
selectedNode = getNextAliveNode();
continue;
}
ClientStatisticsHolder clientStats = globalClientStats.remove(operationId);
OperationMetrics metrics = new OperationMetrics(clientStats);
String summary = HttpAPIClientHelper.getHeaderVal(httpResponse.getFirstHeader(ClickHouseHttpProto.HEADER_SRV_SUMMARY), "{}");
ProcessParser.parseSummary(summary, metrics);
String queryId = HttpAPIClientHelper.getHeaderVal(httpResponse.getFirstHeader(ClickHouseHttpProto.HEADER_QUERY_ID), finalSettings.getQueryId(), String::valueOf);
metrics.operationComplete();
metrics.setQueryId(queryId);
return new InsertResponse(metrics);
} catch ( NoHttpResponseException | ConnectionRequestTimeoutException | ConnectTimeoutException e) {
lastException = httpClientHelper.wrapException("Insert request initiation failed", e);
if (httpClientHelper.shouldRetry(e, finalSettings.getAllSettings())) {
LOG.warn("Retrying", e);
selectedNode = getNextAliveNode();
} else {
throw lastException;
}
} catch (IOException e) {
throw new ClientException("Insert request failed", e);
}
}
throw new ClientException("Insert request failed after retries", lastException);
};
return runAsyncOperation(supplier, settings.getAllSettings());
} else {
//Create an output stream to write the data to
ByteArrayOutputStream stream = new ByteArrayOutputStream();
//Call the static .serialize method on the POJOSerializer for each object in the list
for (Object obj : data) {
for (POJOSerializer serializer : serializersForTable) {
try {
serializer.serialize(obj, stream);
} catch (InvocationTargetException | IllegalAccessException | IOException e) {
throw new DataSerializationException(obj, serializer, e);
}
}
}
globalClientStats.get(operationId).stop(ClientMetrics.OP_SERIALIZATION);
return insert(tableName, new ByteArrayInputStream(stream.toByteArray()), format, settings);
}
}
/**
* Sends write request to database. Input data is read from the input stream.
*
* @param tableName - destination table name
* @param data - data stream to insert
* @param format - format of the data in the stream
* @return {@code CompletableFuture} - a promise to insert response
*/
public CompletableFuture insert(String tableName, InputStream data, ClickHouseFormat format) {
return insert(tableName, data, format, new InsertSettings());
}
/**
* Sends write request to database. Input data is read from the input stream.
*
* @param tableName - destination table name
* @param data - data stream to insert
* @param format - format of the data in the stream
* @param settings - insert operation settings
* @return {@code CompletableFuture} - a promise to insert response
*/
public CompletableFuture insert(String tableName,
InputStream data,
ClickHouseFormat format,
InsertSettings settings) {
String operationId = (String) settings.getOperationId();
ClientStatisticsHolder clientStats = null;
if (operationId != null) {
clientStats = globalClientStats.remove(operationId);
}
if (clientStats == null) {
clientStats = new ClientStatisticsHolder();
}
clientStats.start(ClientMetrics.OP_DURATION);
final ClientStatisticsHolder finalClientStats = clientStats;
Supplier responseSupplier;
if (useNewImplementation) {
String retry = configuration.get(ClickHouseClientOption.RETRY.getKey());
final int maxRetries = retry == null ? (int) ClickHouseClientOption.RETRY.getDefaultValue() : Integer.parseInt(retry);
final int writeBufferSize = settings.getInputStreamCopyBufferSize() <= 0 ?
Integer.parseInt(configuration.getOrDefault(ClickHouseClientOption.WRITE_BUFFER_SIZE.getKey(), "8192")) :
settings.getInputStreamCopyBufferSize();
if (writeBufferSize <= 0) {
throw new IllegalArgumentException("Buffer size must be greater than 0");
}
settings.setOption(ClickHouseClientOption.FORMAT.getKey(), format.name());
final InsertSettings finalSettings = settings;
responseSupplier = () -> {
// Selecting some node
ClickHouseNode selectedNode = getNextAliveNode();
ClientException lastException = null;
for (int i = 0; i <= maxRetries; i++) {
// Execute request
try (ClassicHttpResponse httpResponse =
httpClientHelper.executeRequest(selectedNode, finalSettings.getAllSettings(),
out -> {
out.write("INSERT INTO ".getBytes());
out.write(tableName.getBytes());
out.write(" FORMAT ".getBytes());
out.write(format.name().getBytes());
out.write(" \n".getBytes());
byte[] buffer = new byte[writeBufferSize];
int bytesRead;
while ((bytesRead = data.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.close();
})) {
// Check response
if (httpResponse.getCode() == HttpStatus.SC_SERVICE_UNAVAILABLE) {
LOG.warn("Failed to get response. Server returned {}. Retrying.", httpResponse.getCode());
selectedNode = getNextAliveNode();
continue;
}
OperationMetrics metrics = new OperationMetrics(finalClientStats);
String summary = HttpAPIClientHelper.getHeaderVal(httpResponse.getFirstHeader(ClickHouseHttpProto.HEADER_SRV_SUMMARY), "{}");
ProcessParser.parseSummary(summary, metrics);
String queryId = HttpAPIClientHelper.getHeaderVal(httpResponse.getFirstHeader(ClickHouseHttpProto.HEADER_QUERY_ID), finalSettings.getQueryId(), String::valueOf);
metrics.operationComplete();
metrics.setQueryId(queryId);
return new InsertResponse(metrics);
} catch ( NoHttpResponseException | ConnectionRequestTimeoutException | ConnectTimeoutException e) {
lastException = httpClientHelper.wrapException("Insert request initiation failed", e);
if (httpClientHelper.shouldRetry(e, finalSettings.getAllSettings())) {
LOG.warn("Retrying", e);
selectedNode = getNextAliveNode();
} else {
throw lastException;
}
} catch (IOException e) {
throw new ClientException("Insert request failed", e);
}
if (i < maxRetries) {
try {
data.reset();
} catch (IOException ioe) {
throw new ClientException("Failed to reset stream before next attempt", ioe);
}
}
}
throw new ClientException("Insert request failed after retries", lastException);
};
} else {
responseSupplier = () -> {
ClickHouseRequest.Mutation request = ClientV1AdaptorHelper
.createMutationRequest(oldClient.write(getServerNode()), tableName, settings, configuration).format(format);
CompletableFuture future = null;
future = request.data(output -> {
//Copy the data from the input stream to the output stream
byte[] buffer = new byte[settings.getInputStreamCopyBufferSize()];
int bytesRead;
while ((bytesRead = data.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
output.close();
}).option(ClickHouseClientOption.ASYNC, false).execute();
try {
int operationTimeout = getOperationTimeout();
ClickHouseResponse clickHouseResponse;
if (operationTimeout > 0) {
clickHouseResponse = future.get(operationTimeout, TimeUnit.MILLISECONDS);
} else {
clickHouseResponse = future.get();
}
InsertResponse response = new InsertResponse(clickHouseResponse, finalClientStats);
return response;
} catch (ExecutionException e) {
throw new ClientException("Failed to get insert response", e.getCause());
} catch (InterruptedException | TimeoutException e) {
throw new ClientException("Operation has likely timed out.", e);
}
};
}
return runAsyncOperation(responseSupplier, settings.getAllSettings());
}
/**
* Sends SQL query to server. Default settings are applied.
* @param sqlQuery - complete SQL query.
* @return {@code CompletableFuture} - a promise to query response.
*/
public CompletableFuture query(String sqlQuery) {
return query(sqlQuery, null, null);
}
/**
* Sends SQL query to server.
* Notes:
*
* - Server response format can be specified thru `settings` or in SQL query.
* - If specified in both, the `sqlQuery` will take precedence.
*
* @param sqlQuery - complete SQL query.
* @param settings - query operation settings.
* @return {@code CompletableFuture} - a promise to query response.
*/
public CompletableFuture query(String sqlQuery, QuerySettings settings) {
return query(sqlQuery, null, settings);
}
/**
* Sends SQL query to server with parameters. The map `queryParams` should contain keys that
* match the placeholders in the SQL query.
* For a parametrized query like:
* {@code
* SELECT * FROM table WHERE int_column = {id:UInt8} and string_column = {phrase:String}
* }
*
*
* Code to set the queryParams would be:
* {@code
* Map queryParams = new HashMap<>();
* queryParams.put("id", 1);
* queryParams.put("phrase", "hello");
* }
*
*
* Notes:
*
* - Server response format can be specified thru {@code settings} or in SQL query.
* - If specified in both, the {@code sqlQuery} will take precedence.
*
*
* @param sqlQuery - complete SQL query.
* @param settings - query operation settings.
* @param queryParams - query parameters that are sent to the server. (Optional)
* @return {@code CompletableFuture} - a promise to query response.
*/
public CompletableFuture query(String sqlQuery, Map queryParams, QuerySettings settings) {
if (settings == null) {
settings = new QuerySettings();
}
if (settings.getFormat() == null) {
settings.setFormat(ClickHouseFormat.RowBinaryWithNamesAndTypes);
}
ClientStatisticsHolder clientStats = new ClientStatisticsHolder();
clientStats.start(ClientMetrics.OP_DURATION);
applyDefaults(settings);
Supplier responseSupplier;
if (useNewImplementation) {
String retry = configuration.get(ClickHouseClientOption.RETRY.getKey());
final int maxRetries = retry == null ? (int) ClickHouseClientOption.RETRY.getDefaultValue() : Integer.parseInt(retry);
if (queryParams != null) {
settings.setOption("statement_params", queryParams);
}
final QuerySettings finalSettings = settings;
responseSupplier = () -> {
// Selecting some node
ClickHouseNode selectedNode = getNextAliveNode();
ClientException lastException = null;
for (int i = 0; i <= maxRetries; i++) {
try {
ClassicHttpResponse httpResponse =
httpClientHelper.executeRequest(selectedNode, finalSettings.getAllSettings(), output -> {
output.write(sqlQuery.getBytes(StandardCharsets.UTF_8));
output.close();
});
// Check response
if (httpResponse.getCode() == HttpStatus.SC_SERVICE_UNAVAILABLE) {
LOG.warn("Failed to get response. Server returned {}. Retrying.", httpResponse.getCode());
selectedNode = getNextAliveNode();
continue;
}
OperationMetrics metrics = new OperationMetrics(clientStats);
String summary = HttpAPIClientHelper.getHeaderVal(httpResponse
.getFirstHeader(ClickHouseHttpProto.HEADER_SRV_SUMMARY), "{}");
ProcessParser.parseSummary(summary, metrics);
String queryId = HttpAPIClientHelper.getHeaderVal(httpResponse
.getFirstHeader(ClickHouseHttpProto.HEADER_QUERY_ID), finalSettings.getQueryId());
metrics.setQueryId(queryId);
metrics.operationComplete();
return new QueryResponse(httpResponse, finalSettings.getFormat(), finalSettings, metrics);
} catch ( NoHttpResponseException | ConnectionRequestTimeoutException | ConnectTimeoutException e) {
lastException = httpClientHelper.wrapException("Query request initiation failed", e);
if (httpClientHelper.shouldRetry(e, finalSettings.getAllSettings())) {
LOG.warn("Retrying.", e);
selectedNode = getNextAliveNode();
} else {
throw lastException;
}
} catch (ClientException e) {
throw e;
} catch (Exception e) {
throw new ClientException("Query request failed", e);
}
}
throw new ClientException("Query request failed after retries", lastException);
};
} else {
ClickHouseRequest> request = oldClient.read(getServerNode());
request.options(SettingsConverter.toRequestOptions(settings.getAllSettings()));
request.settings(SettingsConverter.toRequestSettings(settings.getAllSettings(), queryParams));
request.option(ClickHouseClientOption.ASYNC, false); // we have own async handling
request.query(sqlQuery, settings.getQueryId());
final ClickHouseFormat format = settings.getFormat();
request.format(format);
final QuerySettings finalSettings = settings;
responseSupplier = () -> {
LOG.trace("Executing request: {}", request);
try {
int operationTimeout = getOperationTimeout();
ClickHouseResponse clickHouseResponse;
if (operationTimeout > 0) {
clickHouseResponse = request.execute().get(operationTimeout, TimeUnit.MILLISECONDS);
} else {
clickHouseResponse = request.execute().get();
}
return new QueryResponse(clickHouseResponse, format, clientStats, finalSettings);
} catch (ClientException e) {
throw e;
} catch (Exception e) {
throw new ClientException("Failed to get query response", e);
}
};
}
return runAsyncOperation(responseSupplier, settings.getAllSettings());
}
/**
* Queries data in one of descriptive format and creates a reader out of the response stream.
* Format is selected internally so is ignored when passed in settings. If query contains format
* statement then it may cause incompatibility error.
*
* @param sqlQuery
* @return
*/
public CompletableFuture queryRecords(String sqlQuery) {
return queryRecords(sqlQuery, null);
}
/**
* Queries data in one of descriptive format and creates a reader out of the response stream.
* Format is selected internally so is ignored when passed in settings. If query contains format
* statement then it may cause incompatibility error.
*
* @param sqlQuery
* @param settings
* @return
*/
public CompletableFuture queryRecords(String sqlQuery, QuerySettings settings) {
if (settings == null) {
settings = new QuerySettings();
}
settings.setFormat(ClickHouseFormat.RowBinaryWithNamesAndTypes);
settings.waitEndOfQuery(true); // we rely on the summery
return query(sqlQuery, settings).thenApply(response -> {
try {
return new Records(response, newBinaryFormatReader(response));
} catch (Exception e) {
throw new ClientException("Failed to get query response", e);
}
});
}
/**
* Queries data in descriptive format and reads result to a collection.
* Use this method for queries that would return only a few records only because client
* will read whole dataset and convert it into a list of GenericRecord
* @param sqlQuery - SQL query
* @return - complete list of records
*/
public List queryAll(String sqlQuery) {
try {
int operationTimeout = getOperationTimeout();
QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.RowBinaryWithNamesAndTypes)
.waitEndOfQuery(true);
try (QueryResponse response = operationTimeout == 0 ? query(sqlQuery, settings).get() :
query(sqlQuery, settings).get(operationTimeout, TimeUnit.MILLISECONDS)) {
List records = new ArrayList<>();
if (response.getResultRows() > 0) {
RowBinaryWithNamesAndTypesFormatReader reader =
(RowBinaryWithNamesAndTypesFormatReader) newBinaryFormatReader(response);
Map record;
while (reader.readRecord((record = new LinkedHashMap<>()))) {
records.add(new MapBackedRecord(record, reader.getSchema()));
}
}
return records;
}
} catch (ExecutionException e) {
throw new ClientException("Failed to get query response", e.getCause());
} catch (Exception e) {
throw new ClientException("Failed to get query response", e);
}
}
public List queryAll(String sqlQuery, Class clazz, TableSchema schema) {
return queryAll(sqlQuery, clazz, schema, null);
}
/**
* WARNING: Experimental API
*
* Queries data and returns collection with whole result. Data is read directly to a DTO
* to save memory on intermediate structures. DTO will be instantiated with default constructor or
* by using allocator
* {@code class} should be registered before calling this method using {@link #register(Class, TableSchema)}
* Internally deserializer is compiled at the register stage. Compilation is done using ASM library by
* writing a bytecode
* Note: this method will cache schema and it will use sql as a key for storage.
*
*
* @param sqlQuery - query to execute
* @param clazz - class of the DTO
* @param allocator - optional supplier to create new instances of the DTO.
* @throws IllegalArgumentException when class is not registered or no setters found
* @return List of POJOs filled with data
* @param
*/
public List queryAll(String sqlQuery, Class clazz, TableSchema schema, Supplier allocator) {
Map classDeserializers = deserializers.getOrDefault(clazz,
Collections.emptyMap()).getOrDefault(schema.getTableName() == null?
schema.getQuery() : schema.getTableName(), Collections.emptyMap());
if (classDeserializers.isEmpty()) {
throw new IllegalArgumentException("No deserializers found for the query and class '" + clazz + "'. Did you forget to register it?");
}
try {
int operationTimeout = getOperationTimeout();
QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.RowBinaryWithNamesAndTypes);
try (QueryResponse response = operationTimeout == 0 ? query(sqlQuery, settings).get() :
query(sqlQuery, settings).get(operationTimeout, TimeUnit.MILLISECONDS)) {
List records = new ArrayList<>();
RowBinaryWithNamesAndTypesFormatReader reader =
(RowBinaryWithNamesAndTypesFormatReader) newBinaryFormatReader(response);
while (true) {
Object record = allocator == null ? clazz.getDeclaredConstructor().newInstance() : allocator.get();
if (reader.readToPOJO(classDeserializers, record)) {
records.add((T) record);
} else {
break;
}
}
return records;
}
} catch (ExecutionException e) {
throw new ClientException("Failed to get query response", e.getCause());
} catch (Exception e) {
throw new ClientException("Failed to get query response", e);
}
}
/**
* Fetches schema of a table and returns complete information about each column.
* Information includes column name, type, default value, etc.
*
* See {@link #register(Class, TableSchema)}
*
* @param table - table name
* @return {@code TableSchema} - Schema of the table
*/
public TableSchema getTableSchema(String table) {
return getTableSchema(table, getDefaultDatabase());
}
/**
* Fetches schema of a table and returns complete information about each column.
* Information includes column name, type, default value, etc.
* See {@link #register(Class, TableSchema)}
*
* @param table - table name
* @param database - database name
* @return {@code TableSchema} - Schema of the table
*/
public TableSchema getTableSchema(String table, String database) {
final String sql = "DESCRIBE TABLE " + table + " FORMAT " + ClickHouseFormat.TSKV.name();
return getTableSchemaImpl(sql, table, null, database);
}
/**
* Creates table schema from a query.
* @param sql - SQL query which schema to return
* @return table schema for the query
*/
public TableSchema getTableSchemaFromQuery(String sql) {
final String describeQuery = "DESC (" + sql + ") FORMAT " + ClickHouseFormat.TSKV.name();
return getTableSchemaImpl(describeQuery, null, sql, getDefaultDatabase());
}
private TableSchema getTableSchemaImpl(String describeQuery, String name, String originalQuery, String database) {
int operationTimeout = getOperationTimeout();
try (QueryResponse response = operationTimeout == 0 ? query(describeQuery).get() :
query(describeQuery).get(getOperationTimeout(), TimeUnit.SECONDS)) {
return new TableSchemaParser().readTSKV(response.getInputStream(), name, originalQuery, database);
} catch (TimeoutException e) {
throw new ClientException("Operation has likely timed out after " + getOperationTimeout() + " seconds.", e);
} catch (ExecutionException e) {
throw new ClientException("Failed to get table schema", e.getCause());
} catch (Exception e) {
throw new ClientException("Failed to get table schema", e);
}
}
/**
* Executes a SQL command and doesn't care response. Useful for DDL statements, like `CREATE`, `DROP`, `ALTER`.
* Method however returns execution errors from a server or summary in case of successful execution.
*
* @param sql - SQL command
* @param settings - execution settings
* @return {@code CompletableFuture} - a promise to command response
*/
public CompletableFuture execute(String sql, CommandSettings settings) {
return query(sql, settings)
.thenApplyAsync(response -> {
try {
return new CommandResponse(response);
} catch (Exception e) {
throw new ClientException("Failed to get command response", e);
}
});
}
/**
* Executes a SQL command and doesn't care response. Useful for DDL statements, like `CREATE`, `DROP`, `ALTER`.
* Method however returns execution errors from a server or summary in case of successful execution.
*
* @param sql - SQL command
* @return {@code CompletableFuture} - a promise to command response
*/
public CompletableFuture execute(String sql) {
return query(sql)
.thenApply(response -> {
try {
return new CommandResponse(response);
} catch (Exception e) {
throw new ClientException("Failed to get command response", e);
}
});
}
/**
* Create an instance of {@link ClickHouseBinaryFormatReader} based on response. Table schema is option and only
* required for {@link ClickHouseFormat#RowBinaryWithNames}, {@link ClickHouseFormat#RowBinary}.
* Format {@link ClickHouseFormat#RowBinaryWithDefaults} is not supported for output (read operations).
* @param response
* @param schema
* @return
*/
public ClickHouseBinaryFormatReader newBinaryFormatReader(QueryResponse response, TableSchema schema) {
ClickHouseBinaryFormatReader reader = null;
// Using caching buffer allocator is risky so this parameter is not exposed to the user
boolean useCachingBufferAllocator = MapUtils.getFlag(configuration, "client_allow_binary_reader_to_reuse_buffers");
BinaryStreamReader.ByteBufferAllocator byteBufferPool = useCachingBufferAllocator ?
new BinaryStreamReader.CachingByteBufferAllocator() :
new BinaryStreamReader.DefaultByteBufferAllocator();
switch (response.getFormat()) {
case Native:
reader = new NativeFormatReader(response.getInputStream(), response.getSettings(),
byteBufferPool);
break;
case RowBinaryWithNamesAndTypes:
reader = new RowBinaryWithNamesAndTypesFormatReader(response.getInputStream(), response.getSettings(),
byteBufferPool);
break;
case RowBinaryWithNames:
reader = new RowBinaryWithNamesFormatReader(response.getInputStream(), response.getSettings(), schema,
byteBufferPool);
break;
case RowBinary:
reader = new RowBinaryFormatReader(response.getInputStream(), response.getSettings(), schema,
byteBufferPool);
break;
default:
throw new IllegalArgumentException("Unsupported format: " + response.getFormat());
}
return reader;
}
public ClickHouseBinaryFormatReader newBinaryFormatReader(QueryResponse response) {
return newBinaryFormatReader(response, null);
}
private String registerOperationMetrics() {
String operationId = UUID.randomUUID().toString();
globalClientStats.put(operationId, new ClientStatisticsHolder());
return operationId;
}
private void applyDefaults(QuerySettings settings) {
Map settingsMap = settings.getAllSettings();
String key = ClickHouseClientOption.USE_SERVER_TIME_ZONE.getKey();
if (!settingsMap.containsKey(key) && configuration.containsKey(key)) {
settings.setOption(key, MapUtils.getFlag(configuration, key));
}
key = ClickHouseClientOption.USE_TIME_ZONE.getKey();
if ( !settings.getUseServerTimeZone() && !settingsMap.containsKey(key) && configuration.containsKey(key)) {
settings.setOption(key, TimeZone.getTimeZone(configuration.get(key)));
}
key = ClickHouseClientOption.SERVER_TIME_ZONE.getKey();
if (!settingsMap.containsKey(key) && configuration.containsKey(key)) {
settings.setOption(key, TimeZone.getTimeZone(configuration.get(key)));
}
}
private CompletableFuture runAsyncOperation(Supplier resultSupplier, Map requestSettings) {
boolean isAsync = MapUtils.getFlag(configuration, requestSettings, ClickHouseClientOption.ASYNC.getKey());
return isAsync ? CompletableFuture.supplyAsync(resultSupplier, sharedOperationExecutor) : CompletableFuture.completedFuture(resultSupplier.get());
}
public String toString() {
return "Client{" +
"endpoints=" + endpoints +
'}';
}
/**
* Returns unmodifiable map of configuration options.
* @return - configuration options
*/
public Map getConfiguration() {
return Collections.unmodifiableMap(configuration);
}
/** Returns operation timeout in seconds */
protected int getOperationTimeout() {
return Integer.parseInt(configuration.get(ClickHouseClientOption.MAX_EXECUTION_TIME.getKey()));
}
/**
* Returns unmodifiable set of endpoints.
* @return - set of endpoints
*/
public Set getEndpoints() {
return Collections.unmodifiableSet(endpoints);
}
private ClickHouseNode getNextAliveNode() {
return serverNodes.get(0);
}
public static final String VALUES_LIST_DELIMITER = ",";
}