Skip to content

Commit

Permalink
Merge pull request #1613 from ClickHouse/feature_query_settings
Browse files Browse the repository at this point in the history
Added all query settings and implemented simple validation
  • Loading branch information
chernser authored Apr 18, 2024
2 parents cb6a298 + e5d453c commit fdb8205
Show file tree
Hide file tree
Showing 9 changed files with 679 additions and 77 deletions.
24 changes: 17 additions & 7 deletions client-v2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,25 @@
<artifactId>clickhouse-http-client</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
<version>${apache.httpclient.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>5.2.1</version>
<version>${apache.httpclient.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
<version>5.2.1</version>
<version>${apache.httpclient.version}</version>
</dependency>
<dependency>
<groupId>com.github.luben</groupId>
Expand Down Expand Up @@ -76,6 +81,7 @@
<scope>compile</scope>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>${project.parent.groupId}</groupId>
<artifactId>clickhouse-client</artifactId>
Expand All @@ -86,11 +92,13 @@
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand All @@ -101,13 +109,15 @@
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
<groupId>${project.parent.groupId}</groupId>
<artifactId>clickhouse-client</artifactId>
<version>${revision}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>

Expand Down
61 changes: 55 additions & 6 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.clickhouse.client.api;

import com.clickhouse.client.*;
import com.clickhouse.client.api.internal.SettingsConverter;
import com.clickhouse.client.api.internal.ValidationUtils;
import com.clickhouse.data.ClickHouseColumn;

import java.io.InputStream;
Expand All @@ -13,6 +15,10 @@
import com.clickhouse.client.api.query.QueryResponse;
import com.clickhouse.client.api.query.QuerySettings;
import com.clickhouse.data.ClickHouseFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.slf4j.helpers.BasicMDCAdapter;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
Expand All @@ -24,6 +30,8 @@ public class Client {
private Set<String> endpoints;
private Map<String, String> configuration;
private List<ClickHouseNode> serverNodes = new ArrayList<>();
private static final Logger LOG = LoggerFactory.getLogger(Client.class);

private Client(Set<String> endpoints, Map<String,String> configuration) {
this.endpoints = endpoints;
this.configuration = configuration;
Expand Down Expand Up @@ -189,16 +197,22 @@ public Future<InsertResponse> insert(String tableName,
* @return
*/
public Future<QueryResponse> query(String sqlQuery, Map<String, Object> qparams, QuerySettings settings) {
ClickHouseClient clientQuery = ClickHouseClient.newInstance(ClickHouseProtocol.HTTP);
ClickHouseRequest request = clientQuery.read(getServerNode());
ClickHouseClient client = createClient();
ClickHouseRequest<?> request = client.read(getServerNode());
request.options(SettingsConverter.toRequestOptions(settings.getAllSettings()));
request.settings(SettingsConverter.toRequestSettings(settings.getAllSettings()));
request.query(sqlQuery, settings.getQueryID());
// TODO: convert qparams to map[string, string]
request.params(qparams);
return CompletableFuture.completedFuture(new QueryResponse(clientQuery.execute(request)));
request.format(ClickHouseFormat.valueOf(settings.getFormat()));
if (qparams != null && !qparams.isEmpty()) {
request.params(qparams);
}
MDC.put("queryId", settings.getQueryID());
LOG.debug("Executing request: {}", request);
return CompletableFuture.completedFuture(new QueryResponse(client, request.execute()));
}

public TableSchema getTableSchema(String table, String database) {
try (ClickHouseClient clientQuery = ClickHouseClient.newInstance(ClickHouseProtocol.HTTP)) {
try (ClickHouseClient clientQuery = createClient()) {
ClickHouseRequest request = clientQuery.read(getServerNode());
// XML - because java has a built-in XML parser. Will consider CSV later.
request.query("DESCRIBE TABLE " + table + " FORMAT " + ClickHouseFormat.TSKV.name());
Expand All @@ -210,4 +224,39 @@ public TableSchema getTableSchema(String table, String database) {
}
}
}


private ClickHouseClient createClient() {
ClickHouseConfig clientConfig = new ClickHouseConfig();
return ClickHouseClient.builder().config(clientConfig)
.nodeSelector(ClickHouseNodeSelector.of(ClickHouseProtocol.HTTP))
.build();
}

private static final Set<String> COMPRESS_ALGORITHMS = ValidationUtils.whiteList("LZ4", "LZ4HC", "ZSTD", "ZSTDHC", "NONE");

public static Set<String> getCompressAlgorithms() {
return COMPRESS_ALGORITHMS;
}

private static final Set<String> OUTPUT_FORMATS = createFormatWhitelist("output");

private static final Set<String> INPUT_FORMATS = createFormatWhitelist("input");

public static Set<String> getOutputFormats() {
return OUTPUT_FORMATS;
}

private static Set<String> createFormatWhitelist(String shouldSupport) {
Set<String> formats = new HashSet<>();
boolean supportOutput = "output".equals(shouldSupport);
boolean supportInput = "input".equals(shouldSupport);
boolean supportBoth = "both".equals(shouldSupport);
for (ClickHouseFormat format : ClickHouseFormat.values()) {
if ((supportOutput && format.supportsOutput()) || (supportInput && format.supportsInput()) || (supportBoth)) {
formats.add(format.name());
}
}
return Collections.unmodifiableSet(formats);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package com.clickhouse.client.api.internal;

import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.config.ClickHouseOption;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

public class SettingsConverter {

public static Map<String, Serializable> toRequestSettings(Map<String, Object> settings) {
Map<String, Serializable> requestSettings = new HashMap<>();

for (Map.Entry<String, Object> entry : settings.entrySet()) {
if (!REQUEST_SETTINGS.contains(entry.getKey())) {
continue;
}

if (entry.getValue() instanceof Map<?,?>) {
Map<String, String> map = (Map<String, String>) entry.getValue();
requestSettings.put(entry.getKey(), convertMapToStringValue(map));
} else if (entry.getValue() instanceof Collection<?>) {
Collection<?> collection = (Collection<?>) entry.getValue();
requestSettings.put(entry.getKey(), convertCollectionToStringValue(collection));
} else {
requestSettings.put(entry.getKey(), (Serializable) entry.getValue());
}
}

return requestSettings;
}

public static Map<ClickHouseOption, Serializable> toRequestOptions(Map<String, Object> settings) {
Map<ClickHouseOption, Serializable> requestOptions = new HashMap<>();

for (Map.Entry<String, Object> entry : settings.entrySet()) {
if (!REQUEST_OPTIONS.containsKey(entry.getKey())) {
continue;
}

ClickHouseOption option = REQUEST_OPTIONS.get(entry.getKey());
if (entry.getValue() instanceof Map<?,?>) {
Map<String, String> map = (Map<String, String>) entry.getValue();
requestOptions.put(option, convertMapToStringValue(map));
} else if (entry.getValue() instanceof Collection<?>) {
Collection<?> collection = (Collection<?>) entry.getValue();
requestOptions.put(option, convertCollectionToStringValue(collection));
} else {
requestOptions.put(option, (Serializable) entry.getValue());
}
}

return requestOptions;
}

private static String convertMapToStringValue(Map<String, String> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : map.entrySet()) {
sb.append(escape(e.getKey())).append('=').append(escape(e.getValue())).append(',');
}
sb.setLength(sb.length() - 1);
return sb.toString();
}

private static String convertCollectionToStringValue(Collection<?> collection) {
StringBuilder sb = new StringBuilder();
for (Object value : collection) {
sb.append(escape(value.toString())).append(',');
}
sb.setLength(sb.length() - 1);
return sb.toString();
}
private static final Pattern ESCAPE_PATTERN = Pattern.compile("[,'\\\"=\\t\\n]{1}");

public static String escape(String value) {
return ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0");
}

private static final Map<String, ClickHouseOption> REQUEST_OPTIONS = createMapOfRequestOptions();
private static final Set<String> REQUEST_SETTINGS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
ClickHouseClientOption.MAX_EXECUTION_TIME.getKey(),
ClickHouseClientOption.MAX_RESULT_ROWS.getKey(),
"extremes",
"role"
)));

public static Map<String, ClickHouseOption> createMapOfRequestOptions() {
Map<String, ClickHouseOption> map = new HashMap<>();

Arrays.asList(ClickHouseClientOption.FORMAT,
ClickHouseClientOption.MAX_EXECUTION_TIME,
ClickHouseHttpOption.CUSTOM_PARAMS,
ClickHouseClientOption.AUTO_DISCOVERY,
ClickHouseClientOption.CUSTOM_SETTINGS,
ClickHouseClientOption.CUSTOM_SOCKET_FACTORY,
ClickHouseClientOption.CUSTOM_SOCKET_FACTORY_OPTIONS,
ClickHouseClientOption.CLIENT_NAME,
ClickHouseClientOption.DECOMPRESS,
ClickHouseClientOption.DECOMPRESS_ALGORITHM,
ClickHouseClientOption.DECOMPRESS_LEVEL,
ClickHouseClientOption.COMPRESS,
ClickHouseClientOption.COMPRESS_ALGORITHM,
ClickHouseClientOption.COMPRESS_LEVEL,
ClickHouseClientOption.CONNECTION_TIMEOUT,
ClickHouseClientOption.DATABASE,
ClickHouseClientOption.MAX_BUFFER_SIZE,
ClickHouseClientOption.BUFFER_SIZE,
ClickHouseClientOption.BUFFER_QUEUE_VARIATION,
ClickHouseClientOption.READ_BUFFER_SIZE,
ClickHouseClientOption.WRITE_BUFFER_SIZE,
ClickHouseClientOption.REQUEST_CHUNK_SIZE,
ClickHouseClientOption.REQUEST_BUFFERING,
ClickHouseClientOption.RESPONSE_BUFFERING,
ClickHouseClientOption.MAX_MAPPER_CACHE,
ClickHouseClientOption.MAX_QUEUED_BUFFERS,
ClickHouseClientOption.MAX_QUEUED_REQUESTS,
ClickHouseClientOption.MAX_RESULT_ROWS,
ClickHouseClientOption.MAX_THREADS_PER_CLIENT,
ClickHouseClientOption.PRODUCT_NAME,
ClickHouseClientOption.NODE_CHECK_INTERVAL,
ClickHouseClientOption.FAILOVER,
ClickHouseClientOption.RETRY,
ClickHouseClientOption.REPEAT_ON_SESSION_LOCK,
ClickHouseClientOption.REUSE_VALUE_WRAPPER,
ClickHouseClientOption.SERVER_TIME_ZONE,
ClickHouseClientOption.SERVER_VERSION,
ClickHouseClientOption.SESSION_TIMEOUT,
ClickHouseClientOption.SESSION_CHECK,
ClickHouseClientOption.SOCKET_TIMEOUT,
ClickHouseClientOption.SSL,
ClickHouseClientOption.SSL_MODE,
ClickHouseClientOption.SSL_ROOT_CERTIFICATE,
ClickHouseClientOption.SSL_CERTIFICATE,
ClickHouseClientOption.SSL_KEY,
ClickHouseClientOption.KEY_STORE_TYPE,
ClickHouseClientOption.TRUST_STORE,
ClickHouseClientOption.KEY_STORE_PASSWORD,
ClickHouseClientOption.TRANSACTION_TIMEOUT,
ClickHouseClientOption.WIDEN_UNSIGNED_TYPES,
ClickHouseClientOption.USE_BINARY_STRING,
ClickHouseClientOption.USE_BLOCKING_QUEUE,
ClickHouseClientOption.USE_COMPILATION,
ClickHouseClientOption.USE_OBJECTS_IN_ARRAYS,
ClickHouseClientOption.USE_SERVER_TIME_ZONE,
ClickHouseClientOption.USE_SERVER_TIME_ZONE_FOR_DATES,
ClickHouseClientOption.USE_TIME_ZONE)
.forEach(option -> map.put(option.getKey(), option));

return Collections.unmodifiableMap(map);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.clickhouse.client.api.internal;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class ValidationUtils {

public static void checkRange(int value, int min, int max, String name) {
if (value < min || value > max) {
throw new SettingsValidationException(name, "\"" + name + "\" must be in range [" + min + ", " + max + "]");
}
}

public static void checkPositive(int value, String name) {
if (value <= 0) {
throw new SettingsValidationException(name, "\"" + name + "\" must be positive");
}
}

public static void checkNonBlank(String value, String name) {
if (value == null || value.isEmpty()) {
throw new SettingsValidationException(name, "\"" + name + "\" must be non-null and non-empty");
}
}

public static void checkNotNull(Object value, String name) {
if (value == null) {
throw new SettingsValidationException(name, "\"" + name + "\" must be non-null");
}
}

public static void checkValueFromSet(Object value, String name, Set<?> validValues) {
if (!validValues.contains(value)) {
throw new SettingsValidationException(name, "\"" + name + "\" must be one of " + validValues);
}
}

/**
* Creates a unmodifiable set from the given values.
* @param values
* @return
* @param <T>
*/
public static <T> Set<T> whiteList(T... values) {
return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(values)));
}

public static class SettingsValidationException extends IllegalArgumentException {
private static final long serialVersionUID = 1L;

private final String key;

public SettingsValidationException(String key, String message) {
super(message);
this.key = key;
}
}
}
Loading

0 comments on commit fdb8205

Please sign in to comment.