From 8f21bd297164be526fcd19883b2481898faffe0f Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 2 Mar 2026 14:29:28 -0800 Subject: [PATCH 1/5] added simple client --- tests/clickhouse-client/README.md | 114 ++++++ tests/clickhouse-client/clickhouse | 13 + tests/clickhouse-client/clickhouse-client | 13 + tests/clickhouse-client/pom.xml | 92 +++++ .../java/com/clickhouse/client/cli/Main.java | 351 ++++++++++++++++++ 5 files changed, 583 insertions(+) create mode 100644 tests/clickhouse-client/README.md create mode 100755 tests/clickhouse-client/clickhouse create mode 100755 tests/clickhouse-client/clickhouse-client create mode 100644 tests/clickhouse-client/pom.xml create mode 100644 tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java diff --git a/tests/clickhouse-client/README.md b/tests/clickhouse-client/README.md new file mode 100644 index 000000000..dddc6720a --- /dev/null +++ b/tests/clickhouse-client/README.md @@ -0,0 +1,114 @@ +# clickhouse-client-cli + +A simple CLI tool that mimics `clickhouse-client` for executing SQL queries against a ClickHouse server. +Uses the Java client-v2 API over HTTP, requests `TabSeparated` format, and streams raw output to stdout. + +## Build + +```bash +cd tests/clickhouse-client +mvn package -DskipTests +``` + +This produces an executable fat JAR at `target/clickhouse-client-cli-1.0.0.jar`. + +## Wrapper executable + +A wrapper script named `clickhouse-client` is provided in this directory. + +```bash +cd tests/clickhouse-client +./clickhouse-client --help +``` + +To call it as `clickhouse-client` from anywhere: + +```bash +export PATH="$PATH:/home/schernov/workspace01/clickhouse-java/tests/clickhouse-client" +clickhouse-client --help +``` + +## Usage + +Both `--option value` and `--option=value` formats are supported. + +### Query via `--query` / `-q` + +```bash +./clickhouse-client -q "SELECT uniqExact(number) FROM numbers(1000)" +``` + +### Query via stdin + +Pipe a query: + +```bash +echo "SELECT uniqExact(number) FROM numbers(1000)" | ./clickhouse-client +``` + +Here-string: + +```bash +./clickhouse-client <<< "SELECT 1" +``` + +From a file: + +```bash +./clickhouse-client < query.sql +``` + +If no `--query` is given and nothing is piped, the process blocks waiting for input. +Type your SQL and press `Ctrl+D` (EOF) to execute. + +## Options + +| Option | Default | Description | +|--------------------|-------------|--------------------------| +| `--host`, `-h` | `localhost` | Server host | +| `--port` | `8123` | HTTP port | +| `--user`, `-u` | `default` | Username | +| `--password` | *(empty)* | Password | +| `--database`, `-d` | `default` | Database | +| `--query`, `-q` | | SQL query to execute | +| `--log_comment` | | Comment for query log | +| `--send_logs_level`| | Send server logs level | +| `--max_insert_threads` | | Server setting passthrough | +| `--multiquery` | | Execute `;`-separated SQL statements | +| `--secure`, `-s` | off | Use HTTPS | +| `--multiline`, `-n`| | Ignored (compatibility) | +| `--help` | | Print usage | + +Unknown long options in the form `--name value` / `--name=value` are also accepted and forwarded as ClickHouse server settings. + +## Examples + +```bash +# simple select +./clickhouse-client -q "SELECT 1" + +# connect to a remote server with credentials +./clickhouse-client \ + --host ch.example.com --port 8443 --secure \ + --user admin --password secret \ + --log_comment "sync-job-42" \ + --send_logs_level warning \ + -q "SELECT count() FROM system.tables" + +# multi-line query from stdin +./clickhouse-client <<'EOF' +SELECT + database, + count() AS table_count +FROM system.tables +GROUP BY database +ORDER BY table_count DESC +EOF + +# multiquery from stdin (queries separated by ;) +./clickhouse-client --multiquery <<'EOF' +CREATE TEMPORARY TABLE t (x UInt8); +INSERT INTO t VALUES (1), (2), (3); +SELECT sum(x) FROM t; +EOF +``` diff --git a/tests/clickhouse-client/clickhouse b/tests/clickhouse-client/clickhouse new file mode 100755 index 000000000..f541d5eda --- /dev/null +++ b/tests/clickhouse-client/clickhouse @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +JAR_PATH="${SCRIPT_DIR}/target/clickhouse-client-cli-1.0.0.jar" + +if [[ ! -f "${JAR_PATH}" ]]; then + echo "Jar not found: ${JAR_PATH}" >&2 + echo "Build it first: (cd ${SCRIPT_DIR} && mvn package -DskipTests)" >&2 + exit 1 +fi + +exec java -jar "${JAR_PATH}" "$@" diff --git a/tests/clickhouse-client/clickhouse-client b/tests/clickhouse-client/clickhouse-client new file mode 100755 index 000000000..f541d5eda --- /dev/null +++ b/tests/clickhouse-client/clickhouse-client @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +JAR_PATH="${SCRIPT_DIR}/target/clickhouse-client-cli-1.0.0.jar" + +if [[ ! -f "${JAR_PATH}" ]]; then + echo "Jar not found: ${JAR_PATH}" >&2 + echo "Build it first: (cd ${SCRIPT_DIR} && mvn package -DskipTests)" >&2 + exit 1 +fi + +exec java -jar "${JAR_PATH}" "$@" diff --git a/tests/clickhouse-client/pom.xml b/tests/clickhouse-client/pom.xml new file mode 100644 index 000000000..6c8267e69 --- /dev/null +++ b/tests/clickhouse-client/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + com.clickhouse + clickhouse-client-cli + 1.0.0 + jar + + clickhouse-client-cli + Simple CLI tool that mimics clickhouse-client for executing SQL queries + + + UTF-8 + 17 + 17 + 0.9.6-SNAPSHOT + com.clickhouse.client.cli.Main + + + + + com.clickhouse + client-v2 + ${clickhouse-java.version} + all + + + + org.slf4j + slf4j-api + 2.0.13 + + + + org.slf4j + slf4j-simple + 2.0.13 + runtime + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + ${main.class} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + ${main.class} + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java b/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java new file mode 100644 index 000000000..d4a6d7034 --- /dev/null +++ b/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java @@ -0,0 +1,351 @@ +package com.clickhouse.client.cli; + +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.query.QueryResponse; +import com.clickhouse.client.api.query.QuerySettings; +import com.clickhouse.data.ClickHouseFormat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.concurrent.TimeUnit; + +/** + * Simple CLI tool that mimics clickhouse-client. + * Executes a SQL query against a ClickHouse server and prints results in TSV format. + * + * Usage: + * java -jar clickhouse-client-cli.jar [options] + * + * Options: + * --host, -h Server host (default: localhost) + * --port HTTP port (default: 8123) + * --user, -u Username (default: default) + * --password Password (default: empty) + * --database, -d Database (default: default) + * --query, -q SQL query to execute + * --log_comment Comment for query_log records + * --send_logs_level Server log level to send with result + * --max_insert_threads Max insert threads setting + * --multiquery Execute multiple ';'-separated queries + * --multiline, -n (ignored, accepted for compatibility) + * --help Print usage + * + * If --query is not specified, the query is read from stdin. + */ +public class Main { + + private static final long QUERY_TIMEOUT_SECONDS = 300; + + public static void main(String[] args) { + String host = "localhost"; + int port = 8123; + String user = "default"; + String password = ""; + String database = "default"; + String logComment = null; + String sendLogsLevel = null; + String maxInsertThreads = null; + String query = null; + boolean secure = false; + boolean multiquery = false; + Map extraServerSettings = new LinkedHashMap<>(); + + for (int i = 0; i < args.length; i++) { + ParsedOption option = parseOption(args[i]); + String argName = option.name; + String inlineValue = option.inlineValue; + switch (argName) { + case "--host": + case "-h": + host = valueOrNextArg(args, i, argName, inlineValue); + if (inlineValue == null) { + i++; + } + break; + case "--port": + port = Integer.parseInt(valueOrNextArg(args, i, "--port", inlineValue)); + if (inlineValue == null) { + i++; + } + break; + case "--user": + case "-u": + user = valueOrNextArg(args, i, argName, inlineValue); + if (inlineValue == null) { + i++; + } + break; + case "--password": + password = valueOrNextArg(args, i, "--password", inlineValue); + if (inlineValue == null) { + i++; + } + break; + case "--database": + case "-d": + database = valueOrNextArg(args, i, argName, inlineValue); + if (inlineValue == null) { + i++; + } + break; + case "--query": + case "-q": + query = valueOrNextArg(args, i, argName, inlineValue); + if (inlineValue == null) { + i++; + } + break; + case "--log_comment": + case "--log-comment": + logComment = valueOrNextArg(args, i, argName, inlineValue); + if (inlineValue == null) { + i++; + } + break; + case "--send_logs_level": + case "--send-logs-level": + sendLogsLevel = valueOrNextArg(args, i, argName, inlineValue); + if (inlineValue == null) { + i++; + } + break; + case "--max_insert_threads": + case "--max-insert-threads": + maxInsertThreads = valueOrNextArg(args, i, argName, inlineValue); + if (inlineValue == null) { + i++; + } + break; + case "--secure": + case "-s": + secure = true; + break; + case "--multiline": + case "-n": + break; + case "--multiquery": + case "--multi-query": + multiquery = true; + break; + case "--help": + printUsage(); + System.exit(0); + break; + default: + if (argName.startsWith("--")) { + String settingName = argName.substring(2).replace('-', '_'); + String settingValue = valueOrNextArg(args, i, argName, inlineValue); + extraServerSettings.put(settingName, settingValue); + if (inlineValue == null) { + i++; + } + } else { + System.err.println("Unknown option: " + args[i]); + printUsage(); + System.exit(1); + } + } + } + + if (query == null) { + query = readStdin(); + } + + if (query == null || query.isBlank()) { + System.err.println("No query provided. Use --query or pipe SQL via stdin."); + System.exit(1); + } + List queries = multiquery ? splitQueries(query) : List.of(query); + if (queries.isEmpty()) { + System.err.println("No query provided. Use --query or pipe SQL via stdin."); + System.exit(1); + } + + String endpoint = (secure ? "https://" : "http://") + host + ":" + port; + + try (Client client = new Client.Builder() + .addEndpoint(endpoint) + .setUsername(user) + .setPassword(password) + .setDefaultDatabase(database) + .build()) { + + QuerySettings settings = new QuerySettings() + .setFormat(ClickHouseFormat.TabSeparated); + if (logComment != null && !logComment.isBlank()) { + settings.logComment(logComment); + } + if (sendLogsLevel != null && !sendLogsLevel.isBlank()) { + settings.serverSetting("send_logs_level", sendLogsLevel); + } + if (maxInsertThreads != null && !maxInsertThreads.isBlank()) { + settings.serverSetting("max_insert_threads", maxInsertThreads); + } + for (Map.Entry entry : extraServerSettings.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isBlank()) { + settings.serverSetting(entry.getKey(), entry.getValue()); + } + } + + for (String q : queries) { + try (QueryResponse response = client.query(q, settings) + .get(QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + + try (InputStream is = response.getInputStream()) { + byte[] buf = new byte[8192]; + int n; + while ((n = is.read(buf)) != -1) { + System.out.write(buf, 0, n); + } + System.out.flush(); + } + } + } + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + } + + private static String nextArg(String[] args, int currentIndex, String flag) { + int nextIndex = currentIndex + 1; + if (nextIndex >= args.length) { + System.err.println("Missing value for " + flag); + System.exit(1); + } + return args[nextIndex]; + } + + private static String valueOrNextArg(String[] args, int currentIndex, String flag, String inlineValue) { + if (inlineValue != null) { + return inlineValue; + } + return nextArg(args, currentIndex, flag); + } + + private static ParsedOption parseOption(String arg) { + if (arg.startsWith("-")) { + int eq = arg.indexOf('='); + if (eq > 0) { + return new ParsedOption(arg.substring(0, eq), arg.substring(eq + 1)); + } + } + return new ParsedOption(arg, null); + } + + private static final class ParsedOption { + private final String name; + private final String inlineValue; + + private ParsedOption(String name, String inlineValue) { + this.name = name; + this.inlineValue = inlineValue; + } + } + + private static String readStdin() { + try { + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { + String line; + while ((line = reader.readLine()) != null) { + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append(line); + } + } + return sb.isEmpty() ? null : sb.toString(); + } catch (IOException e) { + return null; + } + } + + private static List splitQueries(String sql) { + List queries = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + + boolean inSingleQuote = false; + boolean inDoubleQuote = false; + boolean inBacktick = false; + boolean escaping = false; + + for (int i = 0; i < sql.length(); i++) { + char ch = sql.charAt(i); + + if (escaping) { + current.append(ch); + escaping = false; + continue; + } + + if ((inSingleQuote || inDoubleQuote) && ch == '\\') { + current.append(ch); + escaping = true; + continue; + } + + if (!inDoubleQuote && !inBacktick && ch == '\'') { + inSingleQuote = !inSingleQuote; + current.append(ch); + continue; + } + if (!inSingleQuote && !inBacktick && ch == '"') { + inDoubleQuote = !inDoubleQuote; + current.append(ch); + continue; + } + if (!inSingleQuote && !inDoubleQuote && ch == '`') { + inBacktick = !inBacktick; + current.append(ch); + continue; + } + + if (!inSingleQuote && !inDoubleQuote && !inBacktick && ch == ';') { + String statement = current.toString().trim(); + if (!statement.isEmpty()) { + queries.add(statement); + } + current.setLength(0); + continue; + } + + current.append(ch); + } + + String trailing = current.toString().trim(); + if (!trailing.isEmpty()) { + queries.add(trailing); + } + + return queries; + } + + private static void printUsage() { + System.err.println("Usage: clickhouse-client [options]"); + System.err.println(); + System.err.println("Options:"); + System.err.println(" --host, -h Server host (default: localhost)"); + System.err.println(" --port HTTP port (default: 8123)"); + System.err.println(" --user, -u Username (default: default)"); + System.err.println(" --password Password (default: empty)"); + System.err.println(" --database, -d Database (default: default)"); + System.err.println(" --query, -q SQL query to execute"); + System.err.println(" --log_comment Comment for query_log records"); + System.err.println(" --send_logs_level Server log level to send with result"); + System.err.println(" --max_insert_threads Max insert threads setting"); + System.err.println(" --multiquery Execute multiple ';'-separated queries"); + System.err.println(" --secure, -s Use HTTPS"); + System.err.println(" --help Print this help"); + System.err.println(); + System.err.println("Both '--option value' and '--option=value' formats are supported."); + System.err.println("Unknown '--name value' options are passed through as server settings."); + System.err.println("If --query is not specified, the query is read from stdin."); + } +} From e7c37d2ba7e2b76ba23596e42066a915dc707682 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 2 Mar 2026 14:59:46 -0800 Subject: [PATCH 2/5] Complete implementation that just works --- tests/clickhouse-client/README.md | 1 + tests/clickhouse-client/clickhouse | 26 ++ .../java/com/clickhouse/client/cli/Main.java | 312 +++++++++++++++++- 3 files changed, 333 insertions(+), 6 deletions(-) diff --git a/tests/clickhouse-client/README.md b/tests/clickhouse-client/README.md index dddc6720a..13c3baceb 100644 --- a/tests/clickhouse-client/README.md +++ b/tests/clickhouse-client/README.md @@ -80,6 +80,7 @@ Type your SQL and press `Ctrl+D` (EOF) to execute. | `--help` | | Print usage | Unknown long options in the form `--name value` / `--name=value` are also accepted and forwarded as ClickHouse server settings. +Compatibility-only options used by tests are accepted but ignored. ## Examples diff --git a/tests/clickhouse-client/clickhouse b/tests/clickhouse-client/clickhouse index f541d5eda..f6c10840d 100755 --- a/tests/clickhouse-client/clickhouse +++ b/tests/clickhouse-client/clickhouse @@ -4,6 +4,32 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" JAR_PATH="${SCRIPT_DIR}/target/clickhouse-client-cli-1.0.0.jar" +if [[ "${1:-}" == "extract-from-config" ]]; then + shift + key="" + while [[ $# -gt 0 ]]; do + case "$1" in + --key) + key="${2:-}" + shift 2 + ;; + --key=*) + key="${1#--key=}" + shift + ;; + *) + shift + ;; + esac + done + + # Minimal compatibility for clickhouse-test usage. + if [[ "${key}" == "listen_host" ]]; then + echo "127.0.0.1" + fi + exit 0 +fi + if [[ ! -f "${JAR_PATH}" ]]; then echo "Jar not found: ${JAR_PATH}" >&2 echo "Build it first: (cd ${SCRIPT_DIR} && mvn package -DskipTests)" >&2 diff --git a/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java b/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java index d4a6d7034..7359fe917 100644 --- a/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java +++ b/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java @@ -9,10 +9,21 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.LinkedHashMap; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -41,6 +52,11 @@ public class Main { private static final long QUERY_TIMEOUT_SECONDS = 300; + private static final String LOG_PATH_ENV = "CLICKHOUSE_CLIENT_CLI_LOG"; + private static final Path DEFAULT_LOG_PATH = Paths.get("/tmp/clickhouse-client-cli.log"); + private static final Path FALLBACK_LOG_PATH = Paths.get("clickhouse-client-cli.log"); + private static final Set CLIENT_ONLY_SETTINGS = createClientOnlySettings(); + private static final Set SERVER_SETTINGS = createServerSettings(); public static void main(String[] args) { String host = "localhost"; @@ -55,6 +71,11 @@ public static void main(String[] args) { boolean secure = false; boolean multiquery = false; Map extraServerSettings = new LinkedHashMap<>(); + Path logPath = resolveLogPath(); + + appendLog(logPath, "=== clickhouse-client invocation ==="); + appendLog(logPath, "timestamp=" + new Date()); + appendLog(logPath, "argv=" + String.join(" ", args)); for (int i = 0; i < args.length; i++) { ParsedOption option = parseOption(args[i]); @@ -140,13 +161,24 @@ public static void main(String[] args) { default: if (argName.startsWith("--")) { String settingName = argName.substring(2).replace('-', '_'); - String settingValue = valueOrNextArg(args, i, argName, inlineValue); - extraServerSettings.put(settingName, settingValue); - if (inlineValue == null) { - i++; + String settingValue = inlineValue; + if (settingValue == null && hasNextValueToken(args, i)) { + settingValue = args[++i]; + } + if (settingValue == null) { + // Keep compatibility with flag-style options that have no explicit value. + settingValue = "1"; + } + SettingScope scope = classifySetting(settingName); + if (scope == SettingScope.SERVER) { + extraServerSettings.put(settingName, settingValue); } + } else if (isDetachedValueToken(argName)) { + // Some test runners may accidentally shift argv tokenization. + // For compatibility, ignore standalone value tokens. } else { System.err.println("Unknown option: " + args[i]); + printArgContext(args, i); printUsage(); System.exit(1); } @@ -168,6 +200,16 @@ public static void main(String[] args) { } String endpoint = (secure ? "https://" : "http://") + host + ":" + port; + appendLog(logPath, "endpoint=" + endpoint); + appendLog(logPath, "database=" + database + ", user=" + user + ", secure=" + secure + ", multiquery=" + multiquery); + appendLog(logPath, "log_comment=" + safeForLog(logComment)); + appendLog(logPath, "send_logs_level=" + safeForLog(sendLogsLevel)); + appendLog(logPath, "max_insert_threads=" + safeForLog(maxInsertThreads)); + appendLog(logPath, "server_settings=" + extraServerSettings); + appendLog(logPath, "queries_count=" + queries.size()); + for (int qi = 0; qi < queries.size(); qi++) { + appendLog(logPath, "query[" + qi + "]=" + queries.get(qi)); + } try (Client client = new Client.Builder() .addEndpoint(endpoint) @@ -194,6 +236,7 @@ public static void main(String[] args) { } for (String q : queries) { + appendLog(logPath, "executing_query=" + q); try (QueryResponse response = client.query(q, settings) .get(QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { @@ -208,6 +251,7 @@ public static void main(String[] args) { } } } catch (Exception e) { + appendLog(logPath, "error=" + e.getMessage()); System.err.println("Error: " + e.getMessage()); System.exit(1); } @@ -217,9 +261,16 @@ private static String nextArg(String[] args, int currentIndex, String flag) { int nextIndex = currentIndex + 1; if (nextIndex >= args.length) { System.err.println("Missing value for " + flag); + printArgContext(args, currentIndex); System.exit(1); } - return args[nextIndex]; + String nextToken = args[nextIndex]; + if (nextToken.startsWith("--")) { + System.err.println("Missing value for " + flag); + printArgContext(args, currentIndex); + System.exit(1); + } + return nextToken; } private static String valueOrNextArg(String[] args, int currentIndex, String flag, String inlineValue) { @@ -239,6 +290,81 @@ private static ParsedOption parseOption(String arg) { return new ParsedOption(arg, null); } + private static boolean hasNextValueToken(String[] args, int currentIndex) { + int nextIndex = currentIndex + 1; + if (nextIndex >= args.length) { + return false; + } + String nextToken = args[nextIndex]; + return !nextToken.startsWith("--"); + } + + private static boolean isDetachedValueToken(String token) { + return token != null && !token.isEmpty() && !token.startsWith("-"); + } + + private static Path resolveLogPath() { + String fromEnv = System.getenv(LOG_PATH_ENV); + if (fromEnv != null && !fromEnv.isBlank()) { + return Paths.get(fromEnv); + } + return DEFAULT_LOG_PATH; + } + + private static void appendLog(Path path, String line) { + String payload = line + System.lineSeparator(); + if (appendLogInternal(path, payload)) { + return; + } + if (!FALLBACK_LOG_PATH.equals(path)) { + appendLogInternal(FALLBACK_LOG_PATH, payload); + } + } + + private static boolean appendLogInternal(Path path, String payload) { + try { + Path parent = path.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + byte[] bytes = payload.getBytes(StandardCharsets.UTF_8); + try (FileChannel channel = FileChannel.open(path, + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.APPEND)) { + channel.write(ByteBuffer.wrap(bytes)); + channel.force(true); + } + return true; + } catch (Exception ignored) { + // Logging must never break CLI behavior in tests. + return false; + } + } + + private static String safeForLog(String value) { + return value == null ? "" : value; + } + + private static void printArgContext(String[] args, int currentIndex) { + int contextSize = 6; + int start = Math.max(0, currentIndex - contextSize); + int end = Math.min(args.length - 1, currentIndex + 1); + StringBuilder sb = new StringBuilder(); + sb.append("Argument context: "); + for (int j = start; j <= end; j++) { + if (j > start) { + sb.append(' '); + } + if (j == currentIndex) { + sb.append(">>").append(args[j]).append("<<"); + } else { + sb.append(args[j]); + } + } + System.err.println(sb.toString()); + } + private static final class ParsedOption { private final String name; private final String inlineValue; @@ -345,7 +471,181 @@ private static void printUsage() { System.err.println(" --help Print this help"); System.err.println(); System.err.println("Both '--option value' and '--option=value' formats are supported."); - System.err.println("Unknown '--name value' options are passed through as server settings."); + System.err.println("Known server settings are forwarded to ClickHouse."); + System.err.println("Client-only and unknown settings are accepted but not sent to server."); System.err.println("If --query is not specified, the query is read from stdin."); } + + private static SettingScope classifySetting(String settingName) { + if (SERVER_SETTINGS.contains(settingName)) { + return SettingScope.SERVER; + } + if (CLIENT_ONLY_SETTINGS.contains(settingName)) { + return SettingScope.CLIENT_ONLY; + } + return SettingScope.UNKNOWN; + } + + private enum SettingScope { + SERVER, + CLIENT_ONLY, + UNKNOWN + } + + private static Set createServerSettings() { + Set settings = new HashSet<>(); + Collections.addAll(settings, + "max_insert_threads", + "send_logs_level"); + return Collections.unmodifiableSet(settings); + } + + private static Set createClientOnlySettings() { + Set settings = new HashSet<>(); + Collections.addAll(settings, + "group_by_two_level_threshold", + "group_by_two_level_threshold_bytes", + "distributed_aggregation_memory_efficient", + "fsync_metadata", + "output_format_parallel_formatting", + "input_format_parallel_parsing", + "min_chunk_bytes_for_parallel_parsing", + "max_read_buffer_size", + "prefer_localhost_replica", + "max_block_size", + "max_joined_block_size_rows", + "joined_block_split_single_row", + "join_output_by_rowlist_perkey_rows_threshold", + "max_threads", + "optimize_append_index", + "use_hedged_requests", + "optimize_if_chain_to_multiif", + "optimize_if_transform_strings_to_enum", + "optimize_read_in_order", + "optimize_or_like_chain", + "optimize_substitute_columns", + "enable_multiple_prewhere_read_steps", + "read_in_order_two_level_merge_threshold", + "optimize_aggregation_in_order", + "aggregation_in_order_max_block_bytes", + "use_uncompressed_cache", + "min_bytes_to_use_direct_io", + "min_bytes_to_use_mmap_io", + "local_filesystem_read_method", + "remote_filesystem_read_method", + "local_filesystem_read_prefetch", + "filesystem_cache_segments_batch_size", + "read_from_filesystem_cache_if_exists_otherwise_bypass_cache", + "throw_on_error_from_cache_on_write_operations", + "remote_filesystem_read_prefetch", + "distributed_cache_discard_connection_if_unread_data", + "distributed_cache_use_clients_cache_for_write", + "distributed_cache_use_clients_cache_for_read", + "allow_prefetched_read_pool_for_remote_filesystem", + "filesystem_prefetch_max_memory_usage", + "filesystem_prefetches_limit", + "filesystem_prefetch_min_bytes_for_single_read_task", + "filesystem_prefetch_step_marks", + "filesystem_prefetch_step_bytes", + "enable_filesystem_cache", + "enable_filesystem_cache_on_write_operations", + "compile_expressions", + "compile_aggregate_expressions", + "compile_sort_description", + "merge_tree_coarse_index_granularity", + "optimize_distinct_in_order", + "max_bytes_before_remerge_sort", + "min_compress_block_size", + "max_compress_block_size", + "merge_tree_compact_parts_min_granules_to_multibuffer_read", + "optimize_sorting_by_input_stream_properties", + "http_response_buffer_size", + "http_wait_end_of_query", + "enable_memory_bound_merging_of_aggregation_results", + "min_count_to_compile_expression", + "min_count_to_compile_aggregate_expression", + "min_count_to_compile_sort_description", + "session_timezone", + "use_page_cache_for_disks_without_file_cache", + "use_page_cache_for_local_disks", + "use_page_cache_for_object_storage", + "page_cache_inject_eviction", + "merge_tree_read_split_ranges_into_intersecting_and_non_intersecting_injection_probability", + "prefer_external_sort_block_bytes", + "cross_join_min_rows_to_compress", + "cross_join_min_bytes_to_compress", + "min_external_table_block_size_bytes", + "max_parsing_threads", + "optimize_functions_to_subcolumns", + "parallel_replicas_local_plan", + "query_plan_join_swap_table", + "enable_vertical_final", + "optimize_extract_common_expressions", + "optimize_syntax_fuse_functions", + "use_async_executor_for_materialized_views", + "use_query_condition_cache", + "secondary_indices_enable_bulk_filtering", + "use_skip_indexes_if_final", + "use_skip_indexes_on_data_read", + "optimize_rewrite_like_perfect_affix", + "input_format_parquet_use_native_reader_v3", + "enable_lazy_columns_replication", + "allow_special_serialization_kinds_in_output_formats", + "short_circuit_function_evaluation_for_nulls_threshold", + "automatic_parallel_replicas_mode", + "temporary_files_buffer_size", + "query_plan_optimize_join_order_algorithm", + "max_bytes_before_external_sort", + "max_bytes_before_external_group_by", + "max_bytes_ratio_before_external_sort", + "max_bytes_ratio_before_external_group_by", + "allow_repeated_settings", + "use_skip_indexes_if_final_exact_mode", + "ratio_of_defaults_for_sparse_serialization", + "prefer_fetch_merged_part_size_threshold", + "vertical_merge_algorithm_min_rows_to_activate", + "vertical_merge_algorithm_min_columns_to_activate", + "allow_vertical_merges_from_compact_to_wide_parts", + "min_merge_bytes_to_use_direct_io", + "index_granularity_bytes", + "merge_max_block_size", + "index_granularity", + "min_bytes_for_wide_part", + "compress_marks", + "compress_primary_key", + "marks_compress_block_size", + "primary_key_compress_block_size", + "replace_long_file_name_to_hash", + "max_file_name_length", + "min_bytes_for_full_part_storage", + "compact_parts_max_bytes_to_buffer", + "compact_parts_max_granules_to_buffer", + "compact_parts_merge_max_bytes_to_prefetch_part", + "cache_populated_by_fetch", + "concurrent_part_removal_threshold", + "old_parts_lifetime", + "prewarm_mark_cache", + "use_const_adaptive_granularity", + "enable_index_granularity_compression", + "enable_block_number_column", + "enable_block_offset_column", + "use_primary_key_cache", + "prewarm_primary_key_cache", + "object_serialization_version", + "object_shared_data_serialization_version", + "object_shared_data_serialization_version_for_zero_level_parts", + "object_shared_data_buckets_for_compact_part", + "object_shared_data_buckets_for_wide_part", + "dynamic_serialization_version", + "auto_statistics_types", + "serialization_info_version", + "string_serialization_version", + "nullable_serialization_version", + "enable_shared_storage_snapshot_in_query", + "min_columns_to_activate_adaptive_write_buffer", + "reduce_blocking_parts_sleep_ms", + "shared_merge_tree_outdated_parts_group_size", + "shared_merge_tree_max_outdated_parts_to_process_at_once"); + return Collections.unmodifiableSet(settings); + } } From 1d8578a81b52ece2005a582147b4bfe1e693fd65 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 8 Mar 2026 14:57:42 -0700 Subject: [PATCH 3/5] moved script to dedicated folder --- tests/clickhouse-client/{ => bin}/clickhouse | 2 +- tests/clickhouse-client/clickhouse-client | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) rename tests/clickhouse-client/{ => bin}/clickhouse (91%) delete mode 100755 tests/clickhouse-client/clickhouse-client diff --git a/tests/clickhouse-client/clickhouse b/tests/clickhouse-client/bin/clickhouse similarity index 91% rename from tests/clickhouse-client/clickhouse rename to tests/clickhouse-client/bin/clickhouse index f6c10840d..c861195a2 100755 --- a/tests/clickhouse-client/clickhouse +++ b/tests/clickhouse-client/bin/clickhouse @@ -2,7 +2,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -JAR_PATH="${SCRIPT_DIR}/target/clickhouse-client-cli-1.0.0.jar" +JAR_PATH="${SCRIPT_DIR}/../target/clickhouse-client-cli-1.0.0.jar" if [[ "${1:-}" == "extract-from-config" ]]; then shift diff --git a/tests/clickhouse-client/clickhouse-client b/tests/clickhouse-client/clickhouse-client deleted file mode 100755 index f541d5eda..000000000 --- a/tests/clickhouse-client/clickhouse-client +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -JAR_PATH="${SCRIPT_DIR}/target/clickhouse-client-cli-1.0.0.jar" - -if [[ ! -f "${JAR_PATH}" ]]; then - echo "Jar not found: ${JAR_PATH}" >&2 - echo "Build it first: (cd ${SCRIPT_DIR} && mvn package -DskipTests)" >&2 - exit 1 -fi - -exec java -jar "${JAR_PATH}" "$@" From 044c20de94bc0e4f63c23d84c3ac66f95aa66cc5 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 8 Mar 2026 15:09:54 -0700 Subject: [PATCH 4/5] Updated readme to have only important information --- tests/clickhouse-client/README.md | 108 ++++-------------------------- 1 file changed, 12 insertions(+), 96 deletions(-) diff --git a/tests/clickhouse-client/README.md b/tests/clickhouse-client/README.md index 13c3baceb..f25613c3c 100644 --- a/tests/clickhouse-client/README.md +++ b/tests/clickhouse-client/README.md @@ -1,9 +1,10 @@ # clickhouse-client-cli A simple CLI tool that mimics `clickhouse-client` for executing SQL queries against a ClickHouse server. -Uses the Java client-v2 API over HTTP, requests `TabSeparated` format, and streams raw output to stdout. +Used to test with ClickHouse test framework designed for `clickhouse-client` (https://github.com/ClickHouse/ClickHouse/blob/master/tests/clickhouse-test). +Note: do not clone ClickHouse repo - it takes a lot of time. Download zip instead. -## Build +## Build Java Application ```bash cd tests/clickhouse-client @@ -14,102 +15,17 @@ This produces an executable fat JAR at `target/clickhouse-client-cli-1.0.0.jar`. ## Wrapper executable -A wrapper script named `clickhouse-client` is provided in this directory. +A wrapper script named `clickhouse-client` is provided in `bin/` directory. It is a simple shell script that calls +java application. It is required because `clickhouse-test` script calls `clickhouse-client` binary found in `PATH` environment variable. +It is recommended to set `PATH` locally in terminal session to not override real `clickhouse-client`. -```bash -cd tests/clickhouse-client -./clickhouse-client --help -``` +Java application accepts `CLICKHOUSE_CLIENT_CLI_LOG` variable to define an log output needed for troubleshooting. +Examples of how to run: -To call it as `clickhouse-client` from anywhere: +```shell -```bash -export PATH="$PATH:/home/schernov/workspace01/clickhouse-java/tests/clickhouse-client" -clickhouse-client --help -``` -## Usage +cd ClickHouse-master +CLICKHOUSE_CLIENT_CLI_LOG=./test-run.log PATH="$PATH:/home/someuser/clickhouse-java/tests/clickhouse-client/bin/" tests/clickhouse-test 01428_hash_set_nan_key -Both `--option value` and `--option=value` formats are supported. - -### Query via `--query` / `-q` - -```bash -./clickhouse-client -q "SELECT uniqExact(number) FROM numbers(1000)" -``` - -### Query via stdin - -Pipe a query: - -```bash -echo "SELECT uniqExact(number) FROM numbers(1000)" | ./clickhouse-client -``` - -Here-string: - -```bash -./clickhouse-client <<< "SELECT 1" -``` - -From a file: - -```bash -./clickhouse-client < query.sql -``` - -If no `--query` is given and nothing is piped, the process blocks waiting for input. -Type your SQL and press `Ctrl+D` (EOF) to execute. - -## Options - -| Option | Default | Description | -|--------------------|-------------|--------------------------| -| `--host`, `-h` | `localhost` | Server host | -| `--port` | `8123` | HTTP port | -| `--user`, `-u` | `default` | Username | -| `--password` | *(empty)* | Password | -| `--database`, `-d` | `default` | Database | -| `--query`, `-q` | | SQL query to execute | -| `--log_comment` | | Comment for query log | -| `--send_logs_level`| | Send server logs level | -| `--max_insert_threads` | | Server setting passthrough | -| `--multiquery` | | Execute `;`-separated SQL statements | -| `--secure`, `-s` | off | Use HTTPS | -| `--multiline`, `-n`| | Ignored (compatibility) | -| `--help` | | Print usage | - -Unknown long options in the form `--name value` / `--name=value` are also accepted and forwarded as ClickHouse server settings. -Compatibility-only options used by tests are accepted but ignored. - -## Examples - -```bash -# simple select -./clickhouse-client -q "SELECT 1" - -# connect to a remote server with credentials -./clickhouse-client \ - --host ch.example.com --port 8443 --secure \ - --user admin --password secret \ - --log_comment "sync-job-42" \ - --send_logs_level warning \ - -q "SELECT count() FROM system.tables" - -# multi-line query from stdin -./clickhouse-client <<'EOF' -SELECT - database, - count() AS table_count -FROM system.tables -GROUP BY database -ORDER BY table_count DESC -EOF - -# multiquery from stdin (queries separated by ;) -./clickhouse-client --multiquery <<'EOF' -CREATE TEMPORARY TABLE t (x UInt8); -INSERT INTO t VALUES (1), (2), (3); -SELECT sum(x) FROM t; -EOF -``` +``` \ No newline at end of file From c290e9d31014b8229e0066d4a44d0517fefa1fb0 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Sun, 8 Mar 2026 15:22:30 -0700 Subject: [PATCH 5/5] Added JDBC cli for tests --- tests/clickhouse-client/README.md | 18 ++- tests/clickhouse-client/pom.xml | 2 +- .../java/com/clickhouse/client/cli/Main.java | 149 +++++++++++++++++- 3 files changed, 158 insertions(+), 11 deletions(-) diff --git a/tests/clickhouse-client/README.md b/tests/clickhouse-client/README.md index f25613c3c..105f8880b 100644 --- a/tests/clickhouse-client/README.md +++ b/tests/clickhouse-client/README.md @@ -19,13 +19,25 @@ A wrapper script named `clickhouse-client` is provided in `bin/` directory. It i java application. It is required because `clickhouse-test` script calls `clickhouse-client` binary found in `PATH` environment variable. It is recommended to set `PATH` locally in terminal session to not override real `clickhouse-client`. -Java application accepts `CLICKHOUSE_CLIENT_CLI_LOG` variable to define an log output needed for troubleshooting. -Examples of how to run: +## Environment variables -```shell +| Variable | Description | +|---|---| +| `CLICKHOUSE_CLIENT_CLI_IMPL` | Backend implementation to use: `client` (default, uses client-v2 API) or `jdbc` (uses ClickHouse JDBC driver) | +| `CLICKHOUSE_CLIENT_CLI_LOG` | Path to log file for troubleshooting | + +## Examples +Run tests using the default client-v2 backend: +```shell cd ClickHouse-master CLICKHOUSE_CLIENT_CLI_LOG=./test-run.log PATH="$PATH:/home/someuser/clickhouse-java/tests/clickhouse-client/bin/" tests/clickhouse-test 01428_hash_set_nan_key +``` +Run tests using the JDBC backend: + +```shell +cd ClickHouse-master +CLICKHOUSE_CLIENT_CLI_IMPL=jdbc CLICKHOUSE_CLIENT_CLI_LOG=./test-run.log PATH="$PATH:/home/someuser/clickhouse-java/tests/clickhouse-client/bin/" tests/clickhouse-test 01428_hash_set_nan_key ``` \ No newline at end of file diff --git a/tests/clickhouse-client/pom.xml b/tests/clickhouse-client/pom.xml index 6c8267e69..e6174a419 100644 --- a/tests/clickhouse-client/pom.xml +++ b/tests/clickhouse-client/pom.xml @@ -23,7 +23,7 @@ com.clickhouse - client-v2 + clickhouse-jdbc ${clickhouse-java.version} all diff --git a/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java b/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java index 7359fe917..4672d899f 100644 --- a/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java +++ b/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java @@ -16,6 +16,11 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Statement; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -23,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.LinkedHashMap; +import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -30,6 +36,13 @@ * Simple CLI tool that mimics clickhouse-client. * Executes a SQL query against a ClickHouse server and prints results in TSV format. * + *

Supports two backend implementations selected via the {@code CLICKHOUSE_CLIENT_CLI_IMPL} + * environment variable: + *

    + *
  • {@code client} (default) – uses the ClickHouse client-v2 API
  • + *
  • {@code jdbc} – uses the ClickHouse JDBC driver
  • + *
+ * * Usage: * java -jar clickhouse-client-cli.jar [options] * @@ -53,6 +66,9 @@ public class Main { private static final long QUERY_TIMEOUT_SECONDS = 300; private static final String LOG_PATH_ENV = "CLICKHOUSE_CLIENT_CLI_LOG"; + private static final String IMPL_ENV = "CLICKHOUSE_CLIENT_CLI_IMPL"; + private static final String IMPL_CLIENT = "client"; + private static final String IMPL_JDBC = "jdbc"; private static final Path DEFAULT_LOG_PATH = Paths.get("/tmp/clickhouse-client-cli.log"); private static final Path FALLBACK_LOG_PATH = Paths.get("clickhouse-client-cli.log"); private static final Set CLIENT_ONLY_SETTINGS = createClientOnlySettings(); @@ -199,8 +215,12 @@ public static void main(String[] args) { System.exit(1); } - String endpoint = (secure ? "https://" : "http://") + host + ":" + port; - appendLog(logPath, "endpoint=" + endpoint); + String impl = System.getenv(IMPL_ENV); + if (impl == null || impl.isBlank()) { + impl = IMPL_CLIENT; + } + + appendLog(logPath, "impl=" + impl); appendLog(logPath, "database=" + database + ", user=" + user + ", secure=" + secure + ", multiquery=" + multiquery); appendLog(logPath, "log_comment=" + safeForLog(logComment)); appendLog(logPath, "send_logs_level=" + safeForLog(sendLogsLevel)); @@ -211,6 +231,38 @@ public static void main(String[] args) { appendLog(logPath, "query[" + qi + "]=" + queries.get(qi)); } + try { + switch (impl) { + case IMPL_CLIENT: + executeWithClient(host, port, user, password, database, secure, + logComment, sendLogsLevel, maxInsertThreads, + extraServerSettings, queries, logPath); + break; + case IMPL_JDBC: + executeWithJdbc(host, port, user, password, database, secure, + logComment, sendLogsLevel, maxInsertThreads, + extraServerSettings, queries, logPath); + break; + default: + System.err.println("Unknown " + IMPL_ENV + " value: " + impl + + ". Supported: " + IMPL_CLIENT + ", " + IMPL_JDBC); + System.exit(1); + } + } catch (Exception e) { + appendLog(logPath, "error=" + e.getMessage()); + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + } + + private static void executeWithClient(String host, int port, String user, String password, + String database, boolean secure, + String logComment, String sendLogsLevel, String maxInsertThreads, + Map extraServerSettings, + List queries, Path logPath) throws Exception { + String endpoint = (secure ? "https://" : "http://") + host + ":" + port; + appendLog(logPath, "endpoint=" + endpoint); + try (Client client = new Client.Builder() .addEndpoint(endpoint) .setUsername(user) @@ -239,7 +291,6 @@ public static void main(String[] args) { appendLog(logPath, "executing_query=" + q); try (QueryResponse response = client.query(q, settings) .get(QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - try (InputStream is = response.getInputStream()) { byte[] buf = new byte[8192]; int n; @@ -250,10 +301,90 @@ public static void main(String[] args) { } } } - } catch (Exception e) { - appendLog(logPath, "error=" + e.getMessage()); - System.err.println("Error: " + e.getMessage()); - System.exit(1); + } + } + + private static void executeWithJdbc(String host, int port, String user, String password, + String database, boolean secure, + String logComment, String sendLogsLevel, String maxInsertThreads, + Map extraServerSettings, + List queries, Path logPath) throws Exception { + String protocol = secure ? "https" : "http"; + String jdbcUrl = "jdbc:clickhouse:" + protocol + "://" + host + ":" + port + "/" + database; + appendLog(logPath, "jdbc_url=" + jdbcUrl); + + Properties props = new Properties(); + props.setProperty("user", user); + props.setProperty("password", password); + + addServerSetting(props, "log_comment", logComment); + addServerSetting(props, "send_logs_level", sendLogsLevel); + addServerSetting(props, "max_insert_threads", maxInsertThreads); + for (Map.Entry entry : extraServerSettings.entrySet()) { + addServerSetting(props, entry.getKey(), entry.getValue()); + } + + try (Connection conn = DriverManager.getConnection(jdbcUrl, props); + Statement stmt = conn.createStatement()) { + + for (String q : queries) { + appendLog(logPath, "executing_query=" + q); + boolean hasResultSet = stmt.execute(q); + if (hasResultSet) { + try (ResultSet rs = stmt.getResultSet()) { + writeResultSetAsTsv(rs); + } + } + } + } + } + + private static void addServerSetting(Properties props, String name, String value) { + if (value != null && !value.isBlank()) { + props.setProperty("clickhouse_setting_" + name, value); + } + } + + private static void writeResultSetAsTsv(ResultSet rs) throws Exception { + ResultSetMetaData meta = rs.getMetaData(); + int columnCount = meta.getColumnCount(); + StringBuilder line = new StringBuilder(); + + while (rs.next()) { + line.setLength(0); + for (int i = 1; i <= columnCount; i++) { + if (i > 1) { + line.append('\t'); + } + String val = rs.getString(i); + if (val == null) { + line.append("\\N"); + } else { + escapeTsv(val, line); + } + } + line.append('\n'); + System.out.print(line); + } + System.out.flush(); + } + + private static void escapeTsv(String value, StringBuilder out) { + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + switch (ch) { + case '\\': + out.append('\\').append('\\'); + break; + case '\t': + out.append('\\').append('t'); + break; + case '\n': + out.append('\\').append('n'); + break; + default: + out.append(ch); + } } } @@ -474,6 +605,10 @@ private static void printUsage() { System.err.println("Known server settings are forwarded to ClickHouse."); System.err.println("Client-only and unknown settings are accepted but not sent to server."); System.err.println("If --query is not specified, the query is read from stdin."); + System.err.println(); + System.err.println("Environment variables:"); + System.err.println(" CLICKHOUSE_CLIENT_CLI_IMPL Backend implementation: 'client' (default) or 'jdbc'"); + System.err.println(" CLICKHOUSE_CLIENT_CLI_LOG Path to log file for troubleshooting"); } private static SettingScope classifySetting(String settingName) {