diff --git a/tests/clickhouse-client/README.md b/tests/clickhouse-client/README.md
new file mode 100644
index 000000000..105f8880b
--- /dev/null
+++ b/tests/clickhouse-client/README.md
@@ -0,0 +1,43 @@
+# clickhouse-client-cli
+
+A simple CLI tool that mimics `clickhouse-client` for executing SQL queries against a ClickHouse server.
+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 Java Application
+
+```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 `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`.
+
+## Environment variables
+
+| 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/bin/clickhouse b/tests/clickhouse-client/bin/clickhouse
new file mode 100755
index 000000000..c861195a2
--- /dev/null
+++ b/tests/clickhouse-client/bin/clickhouse
@@ -0,0 +1,39 @@
+#!/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 [[ "${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
+ 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..b5ec62024
--- /dev/null
+++ b/tests/clickhouse-client/pom.xml
@@ -0,0 +1,104 @@
+
+
+ 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
+ clickhouse-jdbc
+ ${clickhouse-java.version}
+ all
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.13
+
+
+
+ org.apache.commons
+ commons-csv
+ 1.14.1
+
+
+
+ commons-cli
+ commons-cli
+ 1.11.0
+
+
+
+ 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..3c55ddba7
--- /dev/null
+++ b/tests/clickhouse-client/src/main/java/com/clickhouse/client/cli/Main.java
@@ -0,0 +1,710 @@
+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 org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+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.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;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+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;
+
+/**
+ * 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]
+ *
+ * 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;
+ 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 CSVFormat CLICKHOUSE_TSV_FORMAT = CSVFormat.TDF.builder()
+ .setQuote(null)
+ .setEscape('\\')
+ .setRecordSeparator('\n')
+ .setNullString("\\N")
+ .build();
+ private static final String USAGE_HEADER = System.lineSeparator()
+ + "Known server settings are forwarded to ClickHouse."
+ + System.lineSeparator()
+ + "Client-only and unknown settings are accepted but not sent to server."
+ + System.lineSeparator()
+ + "If --query is not specified, the query is read from stdin."
+ + System.lineSeparator()
+ + System.lineSeparator()
+ + "Environment variables:"
+ + System.lineSeparator()
+ + " CLICKHOUSE_CLIENT_CLI_IMPL Backend implementation: 'client' (default) or 'jdbc'"
+ + System.lineSeparator()
+ + " CLICKHOUSE_CLIENT_CLI_LOG Path to log file for troubleshooting";
+ private static final Set CLIENT_ONLY_SETTINGS = createClientOnlySettings();
+ private static final Set SERVER_SETTINGS = createServerSettings();
+
+ 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<>();
+ Path logPath = resolveLogPath();
+ Options options = createBaseOptions();
+ Set knownLongOptions = collectLongOptionNames(options);
+
+ appendLog(logPath, "=== clickhouse-client invocation ===");
+ appendLog(logPath, "timestamp=" + new Date());
+ appendLog(logPath, "argv=" + String.join(" ", args));
+
+ registerDynamicLongOptions(options, knownLongOptions, args);
+
+ CommandLineParser parser = DefaultParser.builder()
+ .setAllowPartialMatching(false)
+ .build();
+ CommandLine cmd;
+ try {
+ cmd = parser.parse(options, args);
+ } catch (ParseException e) {
+ System.err.println("Error: " + e.getMessage());
+ printUsage(options);
+ System.exit(1);
+ return;
+ }
+
+ if (cmd.hasOption("help")) {
+ printUsage(options);
+ System.exit(0);
+ }
+
+ host = cmd.getOptionValue("host", host);
+ String portValue = cmd.getOptionValue("port");
+ if (portValue != null) {
+ port = Integer.parseInt(portValue);
+ }
+ user = cmd.getOptionValue("user", user);
+ password = cmd.getOptionValue("password", password);
+ database = cmd.getOptionValue("database", database);
+ query = cmd.getOptionValue("query", query);
+ logComment = firstNonNullOptionValue(cmd, "log_comment", "log-comment");
+ sendLogsLevel = firstNonNullOptionValue(cmd, "send_logs_level", "send-logs-level");
+ maxInsertThreads = firstNonNullOptionValue(cmd, "max_insert_threads", "max-insert-threads");
+ secure = cmd.hasOption("secure");
+ multiquery = cmd.hasOption("multiquery") || cmd.hasOption("multi-query");
+
+ for (Option option : cmd.getOptions()) {
+ String longOpt = option.getLongOpt();
+ if (longOpt == null || knownLongOptions.contains(longOpt)) {
+ continue;
+ }
+ String settingName = longOpt.replace('-', '_');
+ String settingValue = option.hasArg() ? option.getValue() : null;
+ if (settingValue == null) {
+ settingValue = "1";
+ }
+ if (classifySetting(settingName) == SettingScope.SERVER) {
+ extraServerSettings.put(settingName, settingValue);
+ }
+ }
+
+ 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 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));
+ 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 {
+ 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)
+ .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) {
+ 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;
+ while ((n = is.read(buf)) != -1) {
+ System.out.write(buf, 0, n);
+ }
+ System.out.flush();
+ }
+ }
+ }
+ }
+ }
+
+ 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();
+ CSVPrinter printer = new CSVPrinter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8), CLICKHOUSE_TSV_FORMAT);
+ while (rs.next()) {
+ for (int i = 1; i <= columnCount; i++) {
+ printer.print(rs.getString(i));
+ }
+ printer.println();
+ }
+ printer.flush();
+ }
+
+ private static Options createBaseOptions() {
+ Options options = new Options();
+ options.addOption(Option.builder("h").longOpt("host").hasArg().argName("HOST")
+ .desc("Server host (default: localhost)").build());
+ options.addOption(Option.builder().longOpt("port").hasArg().argName("PORT")
+ .desc("HTTP port (default: 8123)").build());
+ options.addOption(Option.builder("u").longOpt("user").hasArg().argName("USER")
+ .desc("Username (default: default)").build());
+ options.addOption(Option.builder().longOpt("password").hasArg().argName("PASSWORD")
+ .desc("Password (default: empty)").build());
+ options.addOption(Option.builder("d").longOpt("database").hasArg().argName("DB")
+ .desc("Database (default: default)").build());
+ options.addOption(Option.builder("q").longOpt("query").hasArg().argName("SQL")
+ .desc("SQL query to execute").build());
+ options.addOption(Option.builder().longOpt("log_comment").hasArg().argName("VALUE")
+ .desc("Comment for query_log records").build());
+ options.addOption(Option.builder().longOpt("log-comment").hasArg().argName("VALUE").build());
+ options.addOption(Option.builder().longOpt("send_logs_level").hasArg().argName("VALUE")
+ .desc("Server log level to send with result").build());
+ options.addOption(Option.builder().longOpt("send-logs-level").hasArg().argName("VALUE").build());
+ options.addOption(Option.builder().longOpt("max_insert_threads").hasArg().argName("VALUE")
+ .desc("Max insert threads setting").build());
+ options.addOption(Option.builder().longOpt("max-insert-threads").hasArg().argName("VALUE").build());
+ options.addOption(Option.builder("s").longOpt("secure")
+ .desc("Use HTTPS").build());
+ options.addOption(Option.builder("n").longOpt("multiline")
+ .desc("(ignored, accepted for compatibility)").build());
+ options.addOption(Option.builder().longOpt("multiquery")
+ .desc("Execute multiple ';'-separated queries").build());
+ options.addOption(Option.builder().longOpt("multi-query").build());
+ options.addOption(Option.builder().longOpt("help")
+ .desc("Print this help").build());
+ return options;
+ }
+
+ private static Set collectLongOptionNames(Options options) {
+ Set names = new HashSet<>();
+ for (Option option : options.getOptions()) {
+ if (option.getLongOpt() != null) {
+ names.add(option.getLongOpt());
+ }
+ }
+ return names;
+ }
+
+ private static void registerDynamicLongOptions(Options options, Set knownLongOptions, String[] args) {
+ Set added = new LinkedHashSet<>();
+ for (String arg : args) {
+ if (arg == null || !arg.startsWith("--") || "--".equals(arg)) {
+ continue;
+ }
+ int eq = arg.indexOf('=');
+ String longOpt = eq >= 0 ? arg.substring(2, eq) : arg.substring(2);
+ if (longOpt.isBlank() || knownLongOptions.contains(longOpt) || added.contains(longOpt)) {
+ continue;
+ }
+ options.addOption(Option.builder()
+ .longOpt(longOpt)
+ .hasArg()
+ .optionalArg(true)
+ .argName("VALUE")
+ .build());
+ added.add(longOpt);
+ }
+ }
+
+ private static String firstNonNullOptionValue(CommandLine cmd, String... optionNames) {
+ for (String optionName : optionNames) {
+ String value = cmd.getOptionValue(optionName);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ 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 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(Options options) {
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.setOptionComparator(null);
+ formatter.printHelp("clickhouse-client [options]", USAGE_HEADER, options, "", true);
+ }
+
+ 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);
+ }
+}