All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.clickhouse.client.cli.ClickHouseCommandLine Maven / Gradle / Ivy

Go to download

Wrapper of ClickHouse native command-line client planed to be deprecated from version 0.6.0 and removed at 0.7.0

There is a newer version: 0.6.4
Show newest version
package com.clickhouse.client.cli;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.net.ConnectException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;

import com.clickhouse.client.ClickHouseConfig;
import com.clickhouse.client.ClickHouseCredentials;
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.ClickHouseRequest;
import com.clickhouse.client.cli.config.ClickHouseCommandLineOption;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.config.ClickHouseSslMode;
import com.clickhouse.data.ClickHouseChecker;
import com.clickhouse.data.ClickHouseCompression;
import com.clickhouse.data.ClickHouseDataConfig;
import com.clickhouse.data.ClickHouseExternalTable;
import com.clickhouse.data.ClickHouseFile;
import com.clickhouse.data.ClickHouseInputStream;
import com.clickhouse.data.ClickHouseOutputStream;
import com.clickhouse.data.ClickHousePassThruStream;
import com.clickhouse.data.ClickHouseUtils;
import com.clickhouse.logging.Logger;
import com.clickhouse.logging.LoggerFactory;
// deprecate from version 0.6.0
@Deprecated
public class ClickHouseCommandLine implements AutoCloseable {
    private static final Logger log = LoggerFactory.getLogger(ClickHouseCommandLine.class);

    public static final String DEFAULT_CLI_ARG_VERSION = "--version";
    public static final String DEFAULT_CLICKHOUSE_CLI_PATH = "clickhouse";
    public static final String DEFAULT_CLIENT_OPTION = "client";
    public static final String DEFAULT_LOCAL_OPTION = "local";
    public static final String DEFAULT_DOCKER_CLI_PATH = "docker";
    public static final String DEFAULT_DOCKER_IMAGE = "clickhouse/clickhouse-server";

    public static final boolean DEFAULT_CLI_IS_AVAILALBE;
    public static final boolean DEFAULT_DOCKER_IS_AVAILALBE;

    private static final Map cache = Collections.synchronizedMap(new WeakHashMap<>(8));

    static {
        String option = DEFAULT_CLIENT_OPTION;
        int timeout = (int) ClickHouseClientOption.CONNECTION_TIMEOUT.getEffectiveDefaultValue();

        DEFAULT_CLI_IS_AVAILALBE = check(timeout, DEFAULT_CLICKHOUSE_CLI_PATH, option, DEFAULT_CLI_ARG_VERSION);
        DEFAULT_DOCKER_IS_AVAILALBE = check(timeout, DEFAULT_DOCKER_CLI_PATH, option, DEFAULT_CLI_ARG_VERSION);
    }

    static String getCommandLine(ClickHouseConfig config, String option) {
        int timeout = config.getConnectionTimeout();
        String cli = config.getStrOption(ClickHouseCommandLineOption.CLICKHOUSE_CLI_PATH);
        if (ClickHouseChecker.isNullOrBlank(cli)) {
            cli = DEFAULT_CLI_IS_AVAILALBE ? DEFAULT_CLICKHOUSE_CLI_PATH : null;
        } else if (!check(timeout, cli, option, DEFAULT_CLI_ARG_VERSION)) {
            cli = null;
        }

        if (cli == null) {
            cli = config.getStrOption(ClickHouseCommandLineOption.DOCKER_CLI_PATH);
            if (ClickHouseChecker.isNullOrBlank(cli)) {
                cli = DEFAULT_DOCKER_IS_AVAILALBE ? DEFAULT_DOCKER_CLI_PATH : null;
            } else if (!check(timeout, cli, option, DEFAULT_CLI_ARG_VERSION)) {
                cli = null;
            }
        }
        return cli;
    }

    static boolean check(int timeout, String command, String... args) {
        if (ClickHouseChecker.isNullOrBlank(command) || args == null) {
            throw new IllegalArgumentException("Non-blank command and non-null arguments are required");
        }

        StringBuilder builder = new StringBuilder(command);
        for (String str : args) {
            builder.append(' ').append(str);
        }
        String commandLine = builder.toString();
        Boolean value = cache.get(commandLine);
        if (value == null) {
            value = Boolean.FALSE;

            List list = new ArrayList<>(args.length + 1);
            list.add(command);
            Collections.addAll(list, args);
            Process process = null;
            try {
                process = new ProcessBuilder(list).start();
                process.getOutputStream().close();
                if (process.waitFor(timeout, TimeUnit.MILLISECONDS)) {
                    int exitValue = process.exitValue();
                    if (exitValue != 0) {
                        log.trace("Command %s exited with value %d", list, exitValue);
                    }
                    value = exitValue == 0;
                } else {
                    log.trace("Timed out after waiting %d ms for command %s to complete", timeout, list);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                log.trace("Failed to check command %s due to: %s", list, e.getMessage());
            } finally {
                if (process != null && process.isAlive()) {
                    process.destroyForcibly();
                }
                process = null;
            }

            if (value) { // no negative cache
                cache.put(commandLine, value);
            }
        }

        return Boolean.TRUE.equals(value);
    }

    static void dockerCommand(ClickHouseConfig config, String hostDir, String containerDir, int timeout,
            List commands) {
        String cli = config.getStrOption(ClickHouseCommandLineOption.DOCKER_CLI_PATH);
        boolean useDocker = false;
        if (ClickHouseChecker.isNullOrBlank(cli)) {
            if (DEFAULT_DOCKER_IS_AVAILALBE) {
                cli = DEFAULT_DOCKER_CLI_PATH;
                useDocker = true;
            }
        } else if (check(timeout, cli, DEFAULT_CLI_ARG_VERSION)) {
            useDocker = true;
        }
        if (useDocker) {
            commands.add(cli);
        } else {
            throw new UncheckedIOException(new ConnectException("Docker command-line is not available: " + cli));
        }

        String img = config.getStrOption(ClickHouseCommandLineOption.CLICKHOUSE_DOCKER_IMAGE);
        if (ClickHouseChecker.isNullOrBlank(img)) {
            img = DEFAULT_DOCKER_IMAGE;
        }
        String str = config.getStrOption(ClickHouseCommandLineOption.CLI_CONTAINER_ID);
        if (!ClickHouseChecker.isNullOrBlank(str)) {
            if (!check(timeout, cli, "exec", str, DEFAULT_CLICKHOUSE_CLI_PATH, DEFAULT_CLIENT_OPTION,
                    DEFAULT_CLI_ARG_VERSION)) {
                synchronized (ClickHouseCommandLine.class) {
                    if (!check(timeout, cli, "exec", str, DEFAULT_CLICKHOUSE_CLI_PATH, DEFAULT_CLIENT_OPTION,
                            DEFAULT_CLI_ARG_VERSION)
                            && !check(timeout, cli, "run", "--rm", "--name", str, "-v", hostDir + ':' + containerDir,
                                    "-d", img, "tail", "-f", "/dev/null")) {
                        throw new UncheckedIOException(new ConnectException("Failed to start new container: " + str));
                    }
                }
            }
            // reuse the existing container
            commands.add("exec");
            commands.add("-i");
            commands.add(str);
        } else { // create new container for each query
            if (!check(timeout, cli, "run", "--rm", img, DEFAULT_CLICKHOUSE_CLI_PATH, DEFAULT_CLIENT_OPTION,
                    DEFAULT_CLI_ARG_VERSION)) {
                throw new UncheckedIOException(new ConnectException("Invalid ClickHouse docker image: " + img));
            }
            commands.add("run");
            commands.add("--rm");
            commands.add("-i");
            commands.add("-v");
            commands.add(hostDir + ':' + containerDir);
            commands.add(img);
        }

        commands.add(DEFAULT_CLICKHOUSE_CLI_PATH);
    }

    static Process startProcess(ClickHouseRequest request) {
        final ClickHouseConfig config = request.getConfig();
        final ClickHouseNode server = request.getServer();
        final int timeout = config.getSocketTimeout();

        String hostDir = config.getStrOption(ClickHouseCommandLineOption.CLI_WORK_DIRECTORY);
        hostDir = ClickHouseUtils.normalizeDirectory(
                ClickHouseChecker.isNullOrBlank(hostDir) ? System.getProperty("java.io.tmpdir") : hostDir);
        String containerDir = config.getStrOption(ClickHouseCommandLineOption.CLI_CONTAINER_DIRECTORY);
        if (ClickHouseChecker.isNullOrBlank(containerDir)) {
            containerDir = "/tmp/";
        } else {
            containerDir = ClickHouseUtils.normalizeDirectory(containerDir);
        }

        List commands = new LinkedList<>();
        String cli = config.getStrOption(ClickHouseCommandLineOption.CLICKHOUSE_CLI_PATH);
        boolean useCli = false;
        if (ClickHouseChecker.isNullOrBlank(cli)) {
            if (DEFAULT_CLI_IS_AVAILALBE) {
                cli = DEFAULT_CLICKHOUSE_CLI_PATH;
                useCli = true;
            }
        } else if (check(timeout, cli, DEFAULT_CLIENT_OPTION, DEFAULT_CLI_ARG_VERSION)) {
            useCli = true;
        }
        if (useCli) {
            commands.add(cli);
            containerDir = hostDir;
        } else {
            // fallback to docker
            dockerCommand(config, hostDir, containerDir, timeout, commands);
        }
        commands.add(DEFAULT_CLIENT_OPTION);

        if (config.isSsl()) {
            commands.add("--secure");

            if (config.getSslMode() == ClickHouseSslMode.NONE) {
                commands.add("--accept-invalid-certificate");
            }
        }
        if (config.isResponseCompressed()) {
            commands.add("--compression=1");
            switch (config.getResponseCompressAlgorithm()) {
                case LZ4:
                    commands.add("--network_compression_method=lz4");
                    break;
                case ZSTD:
                    commands.add("--network_compression_method=zstd");
                    if (config.getResponseCompressLevel() >= 0 && config.getResponseCompressLevel() <= 22) {
                        commands.add("----network_zstd_compression_level=" + config.getResponseCompressLevel());
                    }
                    break;
                default:
                    break;
            }
        } else {
            commands.add("--compression=0");
        }

        commands.add("--host=".concat(server.getHost()));
        commands.add("--port=".concat(Integer.toString(server.getPort())));

        String str = server.getDatabase(config);
        if (!ClickHouseChecker.isNullOrBlank(str)) {
            commands.add("--database=".concat(str));
        }
        str = config.getStrOption(ClickHouseCommandLineOption.CLI_CONFIG_FILE);
        if (config.getBoolOption(ClickHouseCommandLineOption.USE_CLI_CONFIG)
                && !ClickHouseChecker.isNullOrBlank(str) && Files.exists(Paths.get(str))) {
            commands.add("--config-file=".concat(str));
        } else {
            ClickHouseCredentials credentials = server.getCredentials(config);
            str = credentials.getUserName();
            if (!ClickHouseChecker.isNullOrBlank(str)) {
                commands.add("--user=".concat(str));
            }
            str = credentials.getPassword();
            if (!ClickHouseChecker.isNullOrBlank(str)) {
                commands.add("--password=".concat(str));
            }
        }
        commands.add("--format=".concat(config.getFormat().name()));

        str = request.getQueryId().orElse("");
        if (!ClickHouseChecker.isNullOrBlank(str)) {
            commands.add("--query_id=".concat(str));
        }
        commands.add("--query=".concat(str = request.getStatements(false).get(0)));

        for (ClickHouseExternalTable table : request.getExternalTables()) {
            ClickHouseFile tableFile = ClickHouseFile.of(table.getContent(), table.getCompression(),
                    table.getCompressionLevel(), table.getFormat());
            commands.add("--external");
            String filePath;
            if (!tableFile.hasOutput() || !tableFile.getFile().getAbsolutePath().startsWith(hostDir)) {
                // creating a hard link is faster but it's not platform-independent
                File f = ClickHouseInputStream.save(
                        Paths.get(hostDir, "chc_".concat(request.getManager().createUniqueId())).toFile(),
                        table.getContent(), config.getWriteBufferSize(), config.getSocketTimeout(), true);
                filePath = containerDir.concat(f.getName());
            } else {
                filePath = tableFile.getFile().getAbsolutePath();
                if (!hostDir.equals(containerDir)) {
                    filePath = Paths.get(containerDir, filePath.substring(hostDir.length())).toFile().getAbsolutePath();
                }
            }
            commands.add("--file=" + filePath);
            if (!ClickHouseChecker.isNullOrEmpty(table.getName())) {
                commands.add("--name=".concat(table.getName()));
            }
            if (table.getFormat() != null) {
                commands.add("--format=".concat(table.getFormat().name()));
            }
            commands.add("--structure=".concat(table.getStructure()));
        }

        Map settings = request.getSettings();
        Object value = settings.get("max_result_rows");
        if (value instanceof Number) {
            long maxRows = ((Number) value).longValue();
            if (maxRows > 0L) {
                commands.add("--limit=".concat(Long.toString(maxRows)));
            }
        }
        value = settings.get("result_overflow_mode");
        if (value != null) {
            commands.add("--result_overflow_mode=".concat(value.toString()));
        }
        value = settings.get("readonly");
        if (value != null) {
            commands.add("--readonly=".concat(value.toString()));
        }
        if (config.getBoolOption(ClickHouseCommandLineOption.USE_PROFILE_EVENTS)) {
            commands.add("--print-profile-events");
            commands.add("--profile-events-delay-ms=-1");
        }

        log.debug("Query: %s", str);
        ProcessBuilder builder = new ProcessBuilder(commands);
        String workDirectory = config.getStrOption(
                ClickHouseCommandLineOption.CLI_WORK_DIRECTORY);
        if (!ClickHouseChecker.isNullOrBlank(workDirectory)) {
            Path p = Paths.get(workDirectory);
            if (Files.isDirectory(p)) {
                builder.directory(p.toFile());
            }
        }

        if (request.hasOutputStream()) {
            final ClickHouseOutputStream chOutput = request.getOutputStream().get(); // NOSONAR

            if (chOutput.hasUnderlyingStream()) {
                final ClickHousePassThruStream customStream = chOutput.getUnderlyingStream();
                File f = customStream instanceof ClickHouseFile ? ((ClickHouseFile) customStream).getFile() : null;
                if (f == null) {
                    throw new UncheckedIOException(new IOException("Output file not found in " + customStream));
                }
                if (hostDir.equals(containerDir)) {
                    // builder.redirectOutput(f);
                } else if (f.getAbsolutePath().startsWith(hostDir)) {
                    String relativePath = f.getAbsolutePath().substring(hostDir.length());
                    // FIXME overrided by below
                    builder.redirectOutput(new File(containerDir.concat(relativePath)));
                } else {
                    String fileName = f.getName();
                    int len = fileName.length();
                    int index = fileName.indexOf('.', 1);
                    String uuid = request.getManager().createUniqueId();
                    if (index > 0 && index + 1 < len) {
                        fileName = new StringBuilder(len + uuid.length() + 1).append(fileName.substring(0, index))
                                .append('_').append(uuid).append(fileName.substring(index)).toString();
                    } else {
                        fileName = new StringBuilder(len + uuid.length() + 1).append(fileName).append('_')
                                .append(request.getManager().createUniqueId()).toString();
                    }
                    Path newPath = Paths.get(hostDir, fileName);
                    try {
                        f = Files.createLink(newPath, f.toPath()).toFile();
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    } catch (UnsupportedOperationException e) {
                        try {
                            f = ClickHouseInputStream.save(newPath.toFile(), new FileInputStream(f),
                                    config.getWriteBufferSize(), timeout, true);
                        } catch (FileNotFoundException exp) {
                            throw new UncheckedIOException(exp);
                        }
                    }
                }
                builder.redirectOutput(f);
            }
        }

        final Optional in = request.getInputStream();
        try {
            final Process process;
            if (in.isPresent()) {
                final ClickHouseInputStream chInput = in.get();
                final File inputFile;
                if (chInput.hasUnderlyingStream()) {
                    final ClickHousePassThruStream customStream = chInput.getUnderlyingStream();
                    inputFile = customStream instanceof ClickHouseFile ? ((ClickHouseFile) customStream).getFile()
                            : ClickHouseFile.of(customStream.getInputStream(), config.getRequestCompressAlgorithm(),
                                    config.getRequestCompressLevel(), config.getFormat()).getFile();
                } else {
                    inputFile = ClickHouseFile.of(chInput, config.getRequestCompressAlgorithm(),
                            config.getRequestCompressLevel(), config.getFormat()).getFile();
                }
                process = builder.redirectInput(inputFile).start();
            } else {
                process = builder.start();
                process.getOutputStream().close();
            }
            return process;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private final ClickHouseRequest request;

    private String error;

    private final Process process;

    public ClickHouseCommandLine(ClickHouseRequest request) {
        this.request = request;

        this.error = null;

        this.process = startProcess(request);
    }

    public ClickHouseInputStream getInputStream() throws IOException {
        ClickHouseConfig c = request.getConfig();
        ClickHouseOutputStream out = request.getOutputStream().orElse(null);
        Runnable postCloseAction = () -> {
            IOException exp = getError();
            if (exp != null) {
                throw new UncheckedIOException(exp);
            }
        };
        if (out != null && !out.hasUnderlyingStream()) {
            try (OutputStream o = out) {
                ClickHouseInputStream.pipe(process.getInputStream(), o, c.getWriteBufferSize());
            }
            return ClickHouseInputStream.wrap(null, ClickHouseInputStream.empty(), c.getReadBufferSize(),
                    ClickHouseCompression.NONE, ClickHouseDataConfig.DEFAULT_READ_COMPRESS_LEVEL, postCloseAction);
        } else {
            return ClickHouseInputStream.of(process.getInputStream(), c.getReadBufferSize(), postCloseAction);
        }
    }

    IOException getError() {
        if (error == null) {
            int bufferSize = (int) ClickHouseClientOption.BUFFER_SIZE.getDefaultValue();
            try (ByteArrayOutputStream output = new ByteArrayOutputStream(bufferSize)) {
                ClickHouseInputStream.pipe(process.getErrorStream(), output, bufferSize);
                error = new String(output.toByteArray(), StandardCharsets.UTF_8);
            } catch (IOException e) {
                error = "";
            }
            try {
                int exitValue = process.waitFor();
                if (exitValue != 0) {
                    if (error.isEmpty()) {
                        error = ClickHouseUtils.format("Command exited with value %d", exitValue);
                    } else {
                        int index = error.trim().indexOf('\n');
                        error = index > 0 ? error.substring(index + 1) : error;
                    }
                } else {
                    if (!error.isEmpty()) {
                        // TODO update response summary
                        log.trace(() -> {
                            for (String line : error.split("\n")) {
                                log.trace(line);
                            }
                            return "";
                        });
                    }
                    error = "";
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                process.destroyForcibly();
                throw new CompletionException(e);
            }
        }
        return !ClickHouseChecker.isNullOrBlank(error) ? new IOException(error) : null;
    }

    @Override
    public void close() {
        if (process.isAlive()) {
            process.destroyForcibly();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy