mysqlshdk/shellcore/shell_sql.cc (369 lines of code) (raw):

/* * Copyright (c) 2014, 2024, Oracle and/or its affiliates. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, * as designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "shellcore/shell_sql.h" #include <array> #include <fstream> #include <functional> #include "mysqlshdk/include/shellcore/console.h" #include "mysqlshdk/include/shellcore/utils_help.h" #include "mysqlshdk/libs/db/mysql/session.h" #include "mysqlshdk/libs/db/mysqlx/session.h" #include "mysqlshdk/libs/db/replay/setup.h" #include "mysqlshdk/libs/utils/fault_injection.h" #include "mysqlshdk/libs/utils/profiling.h" #include "shellcore/base_session.h" #include "shellcore/interrupt_handler.h" #include "shellcore/shell_options.h" #include "utils/utils_general.h" #include "utils/utils_lexing.h" #include "utils/utils_string.h" REGISTER_HELP(CMD_G_UC_BRIEF, "Send command to mysql server, display result vertically."); REGISTER_HELP(CMD_G_UC_SYNTAX, "<statement>\\G"); REGISTER_HELP(CMD_G_UC_DETAIL, "Execute the statement in the MySQL server and display results " "in a vertical format, one field and value per line."); REGISTER_HELP( CMD_G_UC_DETAIL1, "Useful for results that are too wide to fit the screen horizontally."); REGISTER_HELP(CMD_G_LC_BRIEF, "Send command to mysql server."); REGISTER_HELP(CMD_G_LC_SYNTAX, "<statement>\\g"); REGISTER_HELP(CMD_G_LC_DETAIL, "Execute the statement in the MySQL server and display results."); REGISTER_HELP(CMD_G_LC_DETAIL1, "Same as executing with the current delimiter (default ;)."); using mysqlshdk::utils::Sql_splitter; namespace shcore { namespace { const std::initializer_list<std::string_view> k_keyword_commands = {"source", "use"}; bool ansi_quotes_enabled( const std::shared_ptr<mysqlshdk::db::ISession> &session) { if (!session) return false; FI_SUPPRESS(mysql); FI_SUPPRESS(mysqlx); return session->ansi_quotes_enabled(); } bool no_backslash_escapes_enabled( const std::shared_ptr<mysqlshdk::db::ISession> &session) { if (!session) return false; FI_SUPPRESS(mysql); FI_SUPPRESS(mysqlx); return session->no_backslash_escapes_enabled(); } } // namespace // How many bytes at a time to process when executing large SQL scripts static constexpr auto k_sql_chunk_size = 64 * 1024; Shell_sql::Context::Context(Shell_sql *parent_) : parent(parent_), splitter( [parent_](std::string_view s, bool bol, size_t /*lnum*/) { return parent_->handle_command(s.data(), s.size(), bol); }, [](std::string_view err) { mysqlsh::current_console()->print_error(std::string{err}); }, k_keyword_commands), old_buffer(parent->m_buffer), old_splitter(parent->m_splitter) { parent->m_buffer = &buffer; parent->m_splitter = &splitter; last_handled.swap(parent->_last_handled); } Shell_sql::Context::~Context() { parent->m_buffer = old_buffer; parent->m_splitter = old_splitter; last_handled.swap(parent->_last_handled); } Shell_sql::Shell_sql(IShell_core *owner) : Shell_language(owner), m_base_context(this) { assert(m_splitter != nullptr); assert(m_buffer != nullptr); // Inject help for statement commands. Actual handling of these // commands is done in a way different from other commands SET_CUSTOM_SHELL_COMMAND("\\G", "CMD_G_UC", Shell_command_function(), true, IShell_core::Mode_mask(IShell_core::Mode::SQL)); SET_CUSTOM_SHELL_COMMAND("\\g", "CMD_G_LC", Shell_command_function(), true, IShell_core::Mode_mask(IShell_core::Mode::SQL)); } void Shell_sql::kill_query(uint64_t conn_id, const mysqlshdk::db::Connection_options &conn_opts) { try { std::shared_ptr<mysqlshdk::db::ISession> kill_session; if (conn_opts.get_scheme() == "mysqlx") kill_session = mysqlshdk::db::mysqlx::Session::create(); else kill_session = mysqlshdk::db::mysql::Session::create(); kill_session->connect(conn_opts); kill_session->executef("kill query ?", conn_id); mysqlsh::current_console()->print("-- query aborted\n"); kill_session->close(); } catch (const std::exception &e) { mysqlsh::current_console()->print(std::string("-- error aborting query: ") + e.what() + "\n"); } } bool Shell_sql::process_sql(std::string_view query, std::string_view delimiter, size_t line_num, std::shared_ptr<mysqlshdk::db::ISession> session, mysqlshdk::utils::Sql_splitter *splitter) { bool ret_val = false; std::optional<bool> server_state_changed; if (session) { try { std::shared_ptr<mysqlshdk::db::IResult> result; Sql_result_info info; if (delimiter == "\\G") info.show_vertical = true; try { mysqlshdk::utils::Profile_timer timer; timer.stage_begin("query"); // Install kill query as ^C handler uint64_t conn_id = session->get_connection_id(); const auto &conn_opts = session->get_connection_options(); { shcore::Interrupt_handler interrupt([this, conn_id, conn_opts]() { kill_query(conn_id, conn_opts); return true; }); result = session->querys(query.data(), query.size()); if ((mysqlshdk::db::replay::g_replay_mode == mysqlshdk::db::replay::Mode::Direct) && _owner->get_dev_session()->is_sql_mode_tracking_enabled()) { server_state_changed = session->get_server_status() & SERVER_SESSION_STATE_CHANGED; } } timer.stage_end(); info.elapsed_seconds = timer.total_seconds_elapsed(); } catch (const mysqlshdk::db::Error &e) { auto exc = shcore::Exception::mysql_error_with_code_and_state( e.what(), e.code(), e.sqlstate()); if (line_num > 0) exc.set_file_context("", line_num); throw exc; } catch (...) { throw; } _result_processor(result, info); ret_val = true; } catch (const mysqlshdk::db::Error &exc) { print_exception(shcore::Exception::mysql_error_with_code_and_state( exc.what(), exc.code(), exc.sqlstate())); ret_val = false; } catch (const shcore::Exception &exc) { print_exception(exc); ret_val = false; } } _last_handled.append(query).append(delimiter); // check if the value of sql_mode have changed - statement can either begin // with 'SET' or, because our splitter does not strip them, with a C style // comment e.g.: /*...*/ set sql_mode... if (ret_val) { if (server_state_changed) { if (*server_state_changed) { session->refresh_sql_mode(); } splitter->set_ansi_quotes(session->ansi_quotes_enabled()); splitter->set_no_backslash_escapes( session->no_backslash_escapes_enabled()); } else if (query.size() > 12 && (query[2] == 't' || query[2] == 'T' || query[1] == '*')) { mysqlshdk::utils::SQL_iterator it(_last_handled); auto next = it.next_token(); if (shcore::str_caseeq(next, "SET")) { constexpr std::array<std::string_view, 4> mods = {"GLOBAL", "PERSIST", "SESSION", "LOCAL"}; next = it.next_token(); for (auto mod : mods) { if (shcore::str_caseeq(next, mod)) { next = it.next_token(); break; } } while (next == "@") next = it.next_token(); if (shcore::str_upper(next).find("SQL_MODE") != std::string::npos) { session->refresh_sql_mode(); splitter->set_ansi_quotes(session->ansi_quotes_enabled()); splitter->set_no_backslash_escapes( session->no_backslash_escapes_enabled()); } } } } return ret_val; } bool Shell_sql::handle_input_stream(std::istream *istream) { std::shared_ptr<mysqlshdk::db::ISession> session; { auto s = _owner->get_dev_session(); if (!s) print_exception(shcore::Exception::logic_error("Not connected.")); else session = s->get_core_session(); } mysqlshdk::utils::Sql_splitter *splitter = nullptr; if (!mysqlshdk::utils::iterate_sql_stream( istream, k_sql_chunk_size, [&](std::string_view s, std::string_view delim, size_t lnum, size_t) { std::string_view file; if (shcore::str_beginswith(s, "source")) file = s.substr(6); else if (shcore::str_beginswith(s, "\\.")) file = s.substr(2); bool ret = false; if (!file.empty()) ret = _owner->handle_shell_command("\\source " + std::string{file}); else if (!s.empty()) ret = process_sql(s, delim, lnum, session, splitter); return ret ? ret : mysqlsh::current_shell_options()->get().force; }, [](std::string_view err) { mysqlsh::current_console()->print_error(std::string{err}); }, ansi_quotes_enabled(session), no_backslash_escapes_enabled(session), nullptr, &splitter)) { // signal error during input processing _result_processor(nullptr, {}); return false; } return true; } std::shared_ptr<mysqlshdk::db::ISession> Shell_sql::get_session() { const auto s = _owner->get_dev_session(); std::shared_ptr<mysqlshdk::db::ISession> session; if (s) { session = s->get_core_session(); } if (!s || !session || !session->is_open()) { print_exception(shcore::Exception::logic_error("Not connected.")); } return session; } void Shell_sql::handle_input(std::string &code, Input_state &state) { // TODO(kolesink) this is a temporary solution and should be removed when // splitter is adjusted to be able to restart parsing from previous state if (state == Input_state::ContinuedSingle) { bool statement_complete = false; for (const auto s : {m_splitter->delimiter().c_str(), "\\G", "\\g"}) if (code.find(s) != std::string::npos) { statement_complete = true; break; } // no need to re-parse code until statement is complete if (!statement_complete) { return; } } auto session = get_session(); m_splitter->set_ansi_quotes(ansi_quotes_enabled(session)); m_splitter->set_no_backslash_escapes(no_backslash_escapes_enabled(session)); _last_handled.clear(); // the whole code to be executed is in the `code` variable *m_buffer = std::move(code); code.clear(); // Reinitializes the input state, will be changed back to the Continued state // if no complete statement is found m_input_state = Input_state::Ok; handle_input(session, false); state = m_input_state; // code could have been executed partially ('statement; incomplete statement') // store the incomplete statement back in the input buffer code = *m_buffer; } void Shell_sql::flush_input(const std::string &code) { auto session = get_session(); *m_buffer = code; handle_input(session, true); } void Shell_sql::handle_input(std::shared_ptr<mysqlshdk::db::ISession> session, bool flush) { bool got_error = false; if (!m_buffer->empty()) { // Parses the input string to identify individual statements in it. // Will return a range for every statement that ends with the delimiter, if // there is additional code after the last delimiter, a range for it will be // included too. m_splitter->feed_chunk(&m_buffer->at(0), m_buffer->size()); mysqlshdk::utils::Sql_splitter::Range range; std::string delim; for (;;) { // If there's no more input, the code in the buffer should be executed no // matter if it is a complete statement or not, any generated error will // bubble up if (m_splitter->next_range(&range, &delim) || flush) { if (!process_sql({&m_buffer->at(range.offset), range.length}, delim, 0, session, m_splitter)) { got_error = true; } if (flush) { break; } } else { m_splitter->pack_buffer(m_buffer, range); if (m_buffer->size() > 0 && range.length > 0) { m_input_state = m_splitter->context() == Sql_splitter::Context::kNone ? Input_state::Ok : Input_state::ContinuedSingle; } else { m_buffer->clear(); } break; } } } // signal error during input processing if (got_error) { _result_processor(nullptr, {}); } if (flush) { clear_input(); } } void Shell_sql::clear_input() { m_buffer->clear(); m_splitter->reset(); } std::string Shell_sql::get_continued_input_context() { return to_string(m_splitter->context()); } std::pair<size_t, bool> Shell_sql::handle_command(const char *p, size_t len, bool bol) { // whitelist of inline commands that can appear in SQL static const char k_allowed_inline_commands[] = "wW"; // handle single-letter commands if (len >= 2) { if (p[1] == 'g' || p[1] == 'G') return {2, true}; if (strchr(k_allowed_inline_commands, p[1])) { size_t skip = _owner->handle_inline_shell_command(std::string(p, len)); if (skip > 0) return {skip, false}; } } std::string cmd(p, len); const auto handle_sql_style_command = [&](std::string_view kwd) -> std::pair<size_t, bool> { { std::unique_ptr<Context> ctx; if (kwd == "source") ctx = std::make_unique<Context>(this); if (!_owner->handle_shell_command("\\" + std::string{kwd} + cmd.substr(kwd.size()))) return std::make_pair(0, false); } _last_handled.append(cmd); if (strncmp(p + len, m_splitter->delimiter().c_str(), m_splitter->delimiter().length()) == 0) _last_handled.append(m_splitter->delimiter()); return std::make_pair(len, false); }; for (const auto &c : k_keyword_commands) if (shcore::str_ibeginswith(cmd, c)) return handle_sql_style_command(c); if (bol && memchr(p, '\n', len)) { // handle as full-line command std::unique_ptr<Context> ctx; if (shcore::str_ibeginswith(cmd, "\\source") || shcore::str_beginswith(cmd, "\\.")) ctx = std::make_unique<Context>(this); if (_owner->handle_shell_command(cmd)) return std::make_pair(len, false); } mysqlsh::current_console()->print_error("Unknown command '" + std::string(p, 2) + "'"); // consume the command return std::make_pair(2, false); } void Shell_sql::execute(std::string_view sql) { std::shared_ptr<mysqlshdk::db::ISession> session; const auto s = _owner->get_dev_session(); if (s) { session = s->get_core_session(); } if (!s || !session || !session->is_open()) { print_exception(shcore::Exception::logic_error("Not connected.")); } else { auto check_delim = [this, &sql, &session](std::string_view delim) { if (!shcore::str_endswith(sql, delim)) return false; process_sql(sql.substr(0, sql.length() - delim.length()), delim, 0, session, m_splitter); return true; }; if (check_delim(";")) return; if (check_delim("\\G")) return; if (check_delim("\\g")) return; if (check_delim(get_main_delimiter())) return; process_sql(sql, get_main_delimiter(), 0, session, m_splitter); } } void Shell_sql::print_exception(const shcore::Exception &e) { // Sends a description of the exception data to the error handler which will // define the final format. shcore::Value exception(e.error()); mysqlsh::current_console()->print_value(exception, "error"); } } // namespace shcore