From 8f21bd297164be526fcd19883b2481898faffe0f Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 2 Mar 2026 14:29:28 -0800 Subject: [PATCH 1/2] 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/2] 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); + } }