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

io.trino.cli.Console Maven / Gradle / Ivy

There is a newer version: 464
Show newest version
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.trino.cli;

import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import io.airlift.units.Duration;
import io.trino.cli.ClientOptions.OutputFormat;
import io.trino.cli.ClientOptions.PropertyMapping;
import io.trino.cli.Trino.VersionProvider;
import io.trino.cli.lexer.StatementSplitter;
import io.trino.client.ClientSelectedRole;
import io.trino.client.ClientSession;
import io.trino.client.uri.PropertyName;
import io.trino.client.uri.TrinoUri;
import org.jline.reader.EndOfFileException;
import org.jline.reader.History;
import org.jline.reader.UserInterruptException;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.InfoCmp;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

import static com.google.common.base.CharMatcher.whitespace;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.io.Files.asCharSource;
import static com.google.common.util.concurrent.Uninterruptibles.awaitUninterruptibly;
import static io.trino.cli.Completion.commandCompleter;
import static io.trino.cli.Help.getHelpText;
import static io.trino.cli.QueryPreprocessor.preprocessQuery;
import static io.trino.cli.TerminalUtils.closeTerminal;
import static io.trino.cli.TerminalUtils.getTerminal;
import static io.trino.cli.TerminalUtils.isRealTerminal;
import static io.trino.cli.TerminalUtils.terminalEncoding;
import static io.trino.cli.Trino.formatCliErrorMessage;
import static io.trino.cli.lexer.StatementSplitter.Statement;
import static io.trino.cli.lexer.StatementSplitter.isEmptyStatement;
import static io.trino.client.ClientSession.stripTransactionId;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Locale.ENGLISH;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.jline.utils.AttributedStyle.CYAN;
import static org.jline.utils.AttributedStyle.DEFAULT;

@Command(
        name = "trino",
        header = "Trino command line interface",
        synopsisHeading = "%nUSAGE:%n%n",
        optionListHeading = "%nOPTIONS:%n",
        usageHelpAutoWidth = true,
        versionProvider = VersionProvider.class)
public class Console
        implements Callable
{
    public static final Set STATEMENT_DELIMITERS = ImmutableSet.of(";", "\\G");

    private static final String PROMPT_NAME = "trino";
    private static final Duration EXIT_DELAY = new Duration(3, SECONDS);

    @CommandLine.Spec
    CommandLine.Model.CommandSpec spec;

    @Option(names = {"-h", "--help"}, usageHelp = true, description = "Show this help message and exit")
    public boolean usageHelpRequested;

    @Option(names = "--version", versionHelp = true, description = "Print version information and exit")
    public boolean versionInfoRequested;

    @Mixin
    public ClientOptions clientOptions;

    @Override
    public Integer call()
    {
        return run() ? 0 : 1;
    }

    public boolean run()
    {
        CommandLine.ParseResult parseResult = spec.commandLine().getParseResult();

        Map restrictedOptions = spec.options().stream()
                .filter(parseResult::hasMatchedOption)
                .map(option -> getMapping(option.userObject())
                        .map(value -> new AbstractMap.SimpleEntry<>(value, option.longestName())))
                .filter(Optional::isPresent)
                .map(Optional::get)
                .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));

        TrinoUri uri = clientOptions.getTrinoUri(restrictedOptions);
        ClientSession session = clientOptions.toClientSession(uri);
        boolean hasQuery = clientOptions.execute != null;
        boolean isFromFile = !isNullOrEmpty(clientOptions.file);

        String query = clientOptions.execute;
        if (hasQuery) {
            query += ";";
        }

        if (isFromFile) {
            if (hasQuery) {
                throw new RuntimeException("both --execute and --file specified");
            }
            try {
                query = asCharSource(new File(clientOptions.file), UTF_8).read();
                hasQuery = true;
            }
            catch (IOException e) {
                throw new RuntimeException(format("Error reading from file %s: %s", clientOptions.file, e.getMessage()));
            }
        }

        // Read queries from stdin
        if (!hasQuery && !isRealTerminal()) {
            try {
                if (System.in.available() > 0) {
                    query = new String(ByteStreams.toByteArray(System.in), terminalEncoding()) + ";";

                    if (query.length() > 1) {
                        hasQuery = true;
                    }
                }
            }
            catch (IOException e) {
                // ignored
            }
        }

        // abort any running query if the CLI is terminated
        AtomicBoolean exiting = new AtomicBoolean();
        ThreadInterruptor interruptor = new ThreadInterruptor();
        CountDownLatch exited = new CountDownLatch(1);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            exiting.set(true);
            interruptor.interrupt();
            @SuppressWarnings("CheckReturnValue")
            boolean ignored = awaitUninterruptibly(exited, EXIT_DELAY.toMillis(), MILLISECONDS);
            // Terminal closing restores terminal settings and releases underlying system resources
            closeTerminal();
        }));

        try (QueryRunner queryRunner = new QueryRunner(
                uri,
                session,
                clientOptions.debug)) {
            if (hasQuery) {
                return executeCommand(
                        queryRunner,
                        exiting,
                        query,
                        clientOptions.outputFormat,
                        clientOptions.ignoreErrors,
                        clientOptions.progress.orElse(false),
                        clientOptions.decimalDataSize);
            }

            Optional pager = clientOptions.pager;
            runConsole(
                    queryRunner,
                    exiting,
                    clientOptions.outputFormatInteractive,
                    clientOptions.editingMode,
                    getHistoryFile(clientOptions.historyFile),
                    pager,
                    clientOptions.progress.orElse(true),
                    clientOptions.disableAutoSuggestion,
                    clientOptions.decimalDataSize);
            return true;
        }
        finally {
            exited.countDown();
            interruptor.close();
        }
    }

    private static Optional getMapping(Object userObject)
    {
        if (userObject instanceof Field) {
            return Optional.ofNullable(((Field) userObject).getAnnotation(PropertyMapping.class))
                    .map(PropertyMapping::value);
        }

        return Optional.empty();
    }

    private static void runConsole(
            QueryRunner queryRunner,
            AtomicBoolean exiting,
            OutputFormat outputFormat,
            ClientOptions.EditingMode editingMode,
            Optional historyFile,
            Optional pager,
            boolean progress,
            boolean disableAutoSuggestion,
            boolean decimalDataSize)
    {
        try (TableNameCompleter tableNameCompleter = new TableNameCompleter(queryRunner);
                InputReader reader = new InputReader(editingMode, historyFile, disableAutoSuggestion, commandCompleter(), tableNameCompleter)) {
            tableNameCompleter.populateCache();
            String remaining = "";
            while (!exiting.get()) {
                // setup prompt
                String prompt = PROMPT_NAME;
                Optional schema = queryRunner.getSession().getSchema();
                prompt += schema.map(value -> ":" + value.replace("%", "%%"))
                        .orElse("");
                String commandPrompt = prompt + "> ";

                // read a line of input from user
                String line;
                try {
                    line = reader.readLine(commandPrompt, remaining);
                }
                catch (UserInterruptException e) {
                    if (!e.getPartialLine().isEmpty()) {
                        reader.getHistory().add(e.getPartialLine());
                    }
                    remaining = "";
                    continue;
                }
                catch (EndOfFileException e) {
                    System.out.println();
                    return;
                }

                // check for special commands -- must match InputParser
                String command = CharMatcher.is(';').or(whitespace()).trimTrailingFrom(line);
                switch (command.toLowerCase(ENGLISH)) {
                    case "exit":
                    case "quit":
                        return;
                    case "clear":
                        Terminal terminal = reader.getTerminal();
                        terminal.puts(InfoCmp.Capability.clear_screen);
                        terminal.flush();
                        continue;
                    case "history":
                        for (History.Entry entry : reader.getHistory()) {
                            System.out.println(new AttributedStringBuilder()
                                    .style(DEFAULT.foreground(CYAN))
                                    .append(format("%5d", entry.index() + 1))
                                    .style(DEFAULT)
                                    .append("  ")
                                    .append(entry.line())
                                    .toAnsi(reader.getTerminal()));
                        }
                        continue;
                    case "help":
                        System.out.println();
                        System.out.println(getHelpText());
                        continue;
                }

                // execute any complete statements
                StatementSplitter splitter = new StatementSplitter(line, STATEMENT_DELIMITERS);
                for (Statement split : splitter.getCompleteStatements()) {
                    OutputFormat currentOutputFormat = outputFormat;
                    if (split.terminator().equals("\\G")) {
                        currentOutputFormat = OutputFormat.VERTICAL;
                    }

                    process(queryRunner, split.statement(), currentOutputFormat, tableNameCompleter::populateCache, pager, progress, decimalDataSize, reader.getTerminal(), System.out, System.out);
                }

                // replace remaining with trailing partial statement
                remaining = whitespace().trimTrailingFrom(splitter.getPartialStatement());
            }
        }
        catch (IOException e) {
            e.printStackTrace(System.err);
        }
    }

    private static boolean executeCommand(
            QueryRunner queryRunner,
            AtomicBoolean exiting,
            String query,
            OutputFormat outputFormat,
            boolean ignoreErrors,
            boolean showProgress,
            boolean decimalDataSize)
    {
        boolean success = true;
        StatementSplitter splitter = new StatementSplitter(query);
        for (Statement split : splitter.getCompleteStatements()) {
            if (!isEmptyStatement(split.statement())) {
                if (!process(queryRunner, split.statement(), outputFormat, () -> {}, Optional.of(""), showProgress, decimalDataSize, getTerminal(), System.out, System.err)) {
                    if (!ignoreErrors) {
                        return false;
                    }
                    success = false;
                }
            }
            if (exiting.get()) {
                return success;
            }
        }
        if (!isEmptyStatement(splitter.getPartialStatement())) {
            System.err.println("Non-terminated statement: " + splitter.getPartialStatement());
            return false;
        }
        return success;
    }

    private static boolean process(
            QueryRunner queryRunner,
            String sql,
            OutputFormat outputFormat,
            Runnable schemaChanged,
            Optional pager,
            boolean showProgress,
            boolean decimalDataSize,
            Terminal terminal,
            PrintStream out,
            PrintStream errorChannel)
    {
        String finalSql;
        try {
            finalSql = preprocessQuery(
                    terminal,
                    queryRunner.getSession().getCatalog(),
                    queryRunner.getSession().getSchema(),
                    sql);
        }
        catch (QueryPreprocessorException e) {
            System.err.println(e.getMessage());
            if (queryRunner.isDebug()) {
                e.printStackTrace(System.err);
            }
            return false;
        }

        try (Query query = queryRunner.startQuery(finalSql)) {
            boolean success = query.renderOutput(terminal, out, errorChannel, outputFormat, pager, showProgress, decimalDataSize);

            ClientSession session = queryRunner.getSession();

            // update catalog and schema if present
            if (query.getSetCatalog().isPresent() || query.getSetSchema().isPresent()) {
                ClientSession.Builder builder = ClientSession.builder(session);
                query.getSetCatalog().ifPresent(builder::catalog);
                query.getSetSchema().ifPresent(builder::schema);
                session = builder.build();
            }

            // update transaction ID if necessary
            if (query.isClearTransactionId()) {
                session = stripTransactionId(session);
            }

            ClientSession.Builder builder = ClientSession.builder(session);

            if (query.getStartedTransactionId() != null) {
                builder = builder.transactionId(query.getStartedTransactionId());
            }

            // update path if present
            if (query.getSetPath().isPresent()) {
                builder = builder.path(query.getSetPath().get());
            }

            // update authorization user if present
            if (query.getSetAuthorizationUser().isPresent()) {
                builder = builder.authorizationUser(query.getSetAuthorizationUser());
                builder = builder.roles(ImmutableMap.of());
            }

            if (query.isResetAuthorizationUser()) {
                builder = builder.authorizationUser(Optional.empty());
                builder = builder.roles(ImmutableMap.of());
            }

            // update session properties if present
            if (!query.getSetSessionProperties().isEmpty() || !query.getResetSessionProperties().isEmpty()) {
                Map sessionProperties = new HashMap<>(session.getProperties());
                sessionProperties.putAll(query.getSetSessionProperties());
                sessionProperties.keySet().removeAll(query.getResetSessionProperties());
                builder = builder.properties(sessionProperties);
            }

            // update session roles
            if (!query.getSetRoles().isEmpty()) {
                Map roles = new HashMap<>(session.getRoles());
                roles.putAll(query.getSetRoles());
                builder = builder.roles(roles);
            }

            // update prepared statements if present
            if (!query.getAddedPreparedStatements().isEmpty() || !query.getDeallocatedPreparedStatements().isEmpty()) {
                Map preparedStatements = new HashMap<>(session.getPreparedStatements());
                preparedStatements.putAll(query.getAddedPreparedStatements());
                preparedStatements.keySet().removeAll(query.getDeallocatedPreparedStatements());
                builder = builder.preparedStatements(preparedStatements);
            }

            session = builder.build();
            queryRunner.setSession(session);

            if (query.getSetCatalog().isPresent() || query.getSetSchema().isPresent()) {
                schemaChanged.run();
            }

            return success;
        }
        catch (RuntimeException e) {
            System.err.println(formatCliErrorMessage(e, queryRunner.isDebug()));
            return false;
        }
    }

    private static Optional getHistoryFile(String path)
    {
        if (isNullOrEmpty(path)) {
            return Optional.empty();
        }
        return Optional.of(Paths.get(path));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy