modules/mod_mysql.cc (342 lines of code) (raw):

/* * Copyright (c) 2016, 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 "modules/mod_mysql.h" #include <memory> #include <utility> #include <vector> #include "modules/mod_mysql_constants.h" #include "modules/mod_mysql_session.h" #include "modules/mod_utils.h" #include "modules/mysqlxtest_utils.h" #include "mysqlshdk/include/scripting/type_info/custom.h" #include "mysqlshdk/include/scripting/type_info/generic.h" #include "mysqlshdk/include/shellcore/utils_help.h" #include "mysqlshdk/libs/db/utils/utils.h" #include "mysqlshdk/libs/parser/mysql_parser_utils.h" #include "mysqlshdk/libs/utils/utils_mysql_parsing.h" #include "errmsg.h" // NOLINT namespace mysqlsh { namespace mysql { struct Mysqld_ername { const char *name; int code; const char *text; const char *sqlstate; const char *dummy1; int dummy2; }; #if !defined(DOXYGEN_JS) && !defined(DOXYGEN_PY) static const Mysqld_ername k_error_names[] = { #include "mysqld_ername.h" // NOLINT }; #endif struct Mysqlclient_ername { const char *name; int code; }; static const Mysqlclient_ername k_client_error_names[] = { {"CR_MIN_ERROR", CR_MIN_ERROR}, {"CR_UNKNOWN_ERROR", CR_UNKNOWN_ERROR}, {"CR_SOCKET_CREATE_ERROR", CR_SOCKET_CREATE_ERROR}, {"CR_CONNECTION_ERROR", CR_CONNECTION_ERROR}, {"CR_CONN_HOST_ERROR", CR_CONN_HOST_ERROR}, {"CR_IPSOCK_ERROR", CR_IPSOCK_ERROR}, {"CR_UNKNOWN_HOST", CR_UNKNOWN_HOST}, {"CR_SERVER_GONE_ERROR", CR_SERVER_GONE_ERROR}, {"CR_VERSION_ERROR", CR_VERSION_ERROR}, {"CR_OUT_OF_MEMORY", CR_OUT_OF_MEMORY}, {"CR_WRONG_HOST_INFO", CR_WRONG_HOST_INFO}, {"CR_LOCALHOST_CONNECTION", CR_LOCALHOST_CONNECTION}, {"CR_TCP_CONNECTION", CR_TCP_CONNECTION}, {"CR_SERVER_HANDSHAKE_ERR", CR_SERVER_HANDSHAKE_ERR}, {"CR_SERVER_LOST", CR_SERVER_LOST}, {"CR_COMMANDS_OUT_OF_SYNC", CR_COMMANDS_OUT_OF_SYNC}, {"CR_NAMEDPIPE_CONNECTION", CR_NAMEDPIPE_CONNECTION}, {"CR_NAMEDPIPEWAIT_ERROR", CR_NAMEDPIPEWAIT_ERROR}, {"CR_NAMEDPIPEOPEN_ERROR", CR_NAMEDPIPEOPEN_ERROR}, {"CR_NAMEDPIPESETSTATE_ERROR", CR_NAMEDPIPESETSTATE_ERROR}, {"CR_CANT_READ_CHARSET", CR_CANT_READ_CHARSET}, {"CR_NET_PACKET_TOO_LARGE", CR_NET_PACKET_TOO_LARGE}, {"CR_EMBEDDED_CONNECTION", CR_EMBEDDED_CONNECTION}, #ifdef CR_PROBE_SLAVE_STATUS {"CR_PROBE_SLAVE_STATUS", CR_PROBE_SLAVE_STATUS}, {"CR_PROBE_SLAVE_HOSTS", CR_PROBE_SLAVE_HOSTS}, {"CR_PROBE_SLAVE_CONNECT", CR_PROBE_SLAVE_CONNECT}, {"CR_PROBE_MASTER_CONNECT", CR_PROBE_MASTER_CONNECT}, {"CR_PROBE_REPLICA_STATUS", CR_PROBE_SLAVE_STATUS}, {"CR_PROBE_REPLICA_HOSTS", CR_PROBE_SLAVE_HOSTS}, {"CR_PROBE_REPLICA_CONNECT", CR_PROBE_SLAVE_CONNECT}, {"CR_PROBE_SOURCE_CONNECT", CR_PROBE_MASTER_CONNECT}, #else {"CR_PROBE_REPLICA_STATUS", CR_PROBE_REPLICA_STATUS}, {"CR_PROBE_REPLICA_HOSTS", CR_PROBE_REPLICA_HOSTS}, {"CR_PROBE_REPLICA_CONNECT", CR_PROBE_REPLICA_CONNECT}, {"CR_PROBE_SOURCE_CONNECT", CR_PROBE_SOURCE_CONNECT}, #endif {"CR_SSL_CONNECTION_ERROR", CR_SSL_CONNECTION_ERROR}, {"CR_MALFORMED_PACKET", CR_MALFORMED_PACKET}, {"CR_WRONG_LICENSE", CR_WRONG_LICENSE}, {"CR_NULL_POINTER", CR_NULL_POINTER}, {"CR_NO_PREPARE_STMT", CR_NO_PREPARE_STMT}, {"CR_PARAMS_NOT_BOUND", CR_PARAMS_NOT_BOUND}, {"CR_DATA_TRUNCATED", CR_DATA_TRUNCATED}, {"CR_NO_PARAMETERS_EXISTS", CR_NO_PARAMETERS_EXISTS}, {"CR_INVALID_PARAMETER_NO", CR_INVALID_PARAMETER_NO}, {"CR_INVALID_BUFFER_USE", CR_INVALID_BUFFER_USE}, {"CR_UNSUPPORTED_PARAM_TYPE", CR_UNSUPPORTED_PARAM_TYPE}, {"CR_SHARED_MEMORY_CONNECTION", CR_SHARED_MEMORY_CONNECTION}, {"CR_SHARED_MEMORY_CONNECT_REQUEST_ERROR", CR_SHARED_MEMORY_CONNECT_REQUEST_ERROR}, {"CR_SHARED_MEMORY_CONNECT_ANSWER_ERROR", CR_SHARED_MEMORY_CONNECT_ANSWER_ERROR}, {"CR_SHARED_MEMORY_CONNECT_FILE_MAP_ERROR", CR_SHARED_MEMORY_CONNECT_FILE_MAP_ERROR}, {"CR_SHARED_MEMORY_CONNECT_MAP_ERROR", CR_SHARED_MEMORY_CONNECT_MAP_ERROR}, {"CR_SHARED_MEMORY_FILE_MAP_ERROR", CR_SHARED_MEMORY_FILE_MAP_ERROR}, {"CR_SHARED_MEMORY_MAP_ERROR", CR_SHARED_MEMORY_MAP_ERROR}, {"CR_SHARED_MEMORY_EVENT_ERROR", CR_SHARED_MEMORY_EVENT_ERROR}, {"CR_SHARED_MEMORY_CONNECT_ABANDONED_ERROR", CR_SHARED_MEMORY_CONNECT_ABANDONED_ERROR}, {"CR_SHARED_MEMORY_CONNECT_SET_ERROR", CR_SHARED_MEMORY_CONNECT_SET_ERROR}, {"CR_CONN_UNKNOW_PROTOCOL", CR_CONN_UNKNOW_PROTOCOL}, {"CR_INVALID_CONN_HANDLE", CR_INVALID_CONN_HANDLE}, {"CR_UNUSED_1", CR_UNUSED_1}, {"CR_FETCH_CANCELED", CR_FETCH_CANCELED}, {"CR_NO_DATA", CR_NO_DATA}, {"CR_NO_STMT_METADATA", CR_NO_STMT_METADATA}, {"CR_NO_RESULT_SET", CR_NO_RESULT_SET}, {"CR_NOT_IMPLEMENTED", CR_NOT_IMPLEMENTED}, {"CR_SERVER_LOST_EXTENDED", CR_SERVER_LOST_EXTENDED}, {"CR_STMT_CLOSED", CR_STMT_CLOSED}, {"CR_NEW_STMT_METADATA", CR_NEW_STMT_METADATA}, {"CR_ALREADY_CONNECTED", CR_ALREADY_CONNECTED}, {"CR_AUTH_PLUGIN_CANNOT_LOAD", CR_AUTH_PLUGIN_CANNOT_LOAD}, {"CR_DUPLICATE_CONNECTION_ATTR", CR_DUPLICATE_CONNECTION_ATTR}, {"CR_AUTH_PLUGIN_ERR", CR_AUTH_PLUGIN_ERR}, {"CR_INSECURE_API_ERR", CR_INSECURE_API_ERR}, {"CR_FILE_NAME_TOO_LONG", CR_FILE_NAME_TOO_LONG}, {"CR_SSL_FIPS_MODE_ERR", CR_SSL_FIPS_MODE_ERR}, {"CR_DEPRECATED_COMPRESSION_NOT_SUPPORTED", CR_DEPRECATED_COMPRESSION_NOT_SUPPORTED}, {"CR_COMPRESSION_WRONGLY_CONFIGURED", CR_COMPRESSION_WRONGLY_CONFIGURED}, {"CR_KERBEROS_USER_NOT_FOUND", CR_KERBEROS_USER_NOT_FOUND}, {"CR_LOAD_DATA_LOCAL_INFILE_REJECTED", CR_LOAD_DATA_LOCAL_INFILE_REJECTED}, {"CR_LOAD_DATA_LOCAL_INFILE_REALPATH_FAIL", CR_LOAD_DATA_LOCAL_INFILE_REALPATH_FAIL}, {"CR_DNS_SRV_LOOKUP_FAILED", CR_DNS_SRV_LOOKUP_FAILED}, {"CR_MANDATORY_TRACKER_NOT_FOUND", CR_MANDATORY_TRACKER_NOT_FOUND}, {"CR_INVALID_FACTOR_NO", CR_INVALID_FACTOR_NO}, {"CR_CANT_GET_SESSION_DATA", CR_CANT_GET_SESSION_DATA}, {"CR_INVALID_CLIENT_CHARSET", CR_INVALID_CLIENT_CHARSET}, {"CR_ERROR_LAST", CR_ERROR_LAST}, {"CR_MAX_ERROR", CR_MAX_ERROR}}; // If this assert fails, the list of client error names has to be updated static_assert(CR_ERROR_LAST <= 2074); static shcore::Value g_error_code; REGISTER_HELP_MODULE(mysql, shellapi); REGISTER_HELP(MYSQL_GLOBAL_BRIEF, "Support for connecting to MySQL servers using the classic MySQL " "protocol."); REGISTER_HELP(MYSQL_BRIEF, "Encloses the functions and classes available to interact with a " "MySQL Server using the traditional " "MySQL Protocol."); REGISTER_HELP(MYSQL_DETAIL, "Use this module to create a session using the traditional MySQL " "Protocol, for example for MySQL Servers where " "the X Protocol is not available."); REGISTER_HELP(MYSQL_DETAIL1, "Note that the API interface on this module is very limited, " "even you can load schemas, tables and views as " "objects there are no operations available on them."); REGISTER_HELP(MYSQL_DETAIL2, "The purpose of this module is to allow SQL Execution on MySQL " "Servers where the X Protocol is not enabled."); REGISTER_HELP(MYSQL_DETAIL3, "To use the properties and functions available on this module " "you first need to import it."); REGISTER_HELP(MYSQL_DETAIL4, "When running the shell in interactive mode, this module is " "automatically imported."); REGISTER_HELP_CONSTANTS(ErrorCode, mysql); REGISTER_HELP(ERRORCODE_BRIEF, "MySQL server error codes."); REGISTER_HELP(ERRORCODE_DETAIL, "Constants with the error code values that can be received from " "the MySQL server."); REGISTER_MODULE(Mysql, mysql) { add_property("ErrorCode|ErrorCode"); expose("getClassicSession", &Mysql::get_session, "connectionData", "?password"); expose("getSession", &Mysql::get_session, "connectionData", "?password"); expose("splitScript", &Mysql::split_script, "script"); expose("parseStatementAst", &Mysql::parse_statement_ast, "statement"); expose("quoteIdentifier", &Mysql::quote_identifier, "string"); expose("unquoteIdentifier", &Mysql::unquote_identifier, "string"); _type.reset(new Type()); } // We need to hide this from doxygen to avoid warnings #if !defined(DOXYGEN_JS) && !defined(DOXYGEN_PY) shcore::Value Mysql::get_member(const std::string &prop) const { shcore::Value ret_val; if (prop == "Type") { ret_val = shcore::Value(_type); } else if (prop == "ErrorCode") { if (!g_error_code) { auto dict = shcore::make_dict(); for (const auto &e : k_client_error_names) { dict->set(e.name, shcore::Value(e.code)); } for (const auto &e : k_error_names) { dict->set(e.name, shcore::Value(e.code)); } g_error_code = shcore::Value(dict); } ret_val = g_error_code; } else { ret_val = Cpp_object_bridge::get_member(prop); } return ret_val; } #endif REGISTER_HELP_FUNCTION(getClassicSession, mysql); REGISTER_HELP(MYSQL_GETCLASSICSESSION_BRIEF, "Opens a classic MySQL protocol session to a MySQL server."); REGISTER_HELP(MYSQL_GETCLASSICSESSION_PARAM, "@param connectionData The connection data for the session"); REGISTER_HELP(MYSQL_GETCLASSICSESSION_PARAM1, "@param password Optional password for the session"); REGISTER_HELP(MYSQL_GETCLASSICSESSION_RETURNS, "@returns A ClassicSession"); REGISTER_HELP(MYSQL_GETCLASSICSESSION_DETAIL, "A ClassicSession object uses the traditional MySQL Protocol to " "allow executing operations on the " "connected MySQL Server."); REGISTER_HELP(MYSQL_GETCLASSICSESSION_DETAIL1, "${TOPIC_CONNECTION_DATA}"); /** * \ingroup mysql * $(MYSQL_GETCLASSICSESSION_BRIEF) * * $(MYSQL_GETCLASSICSESSION_PARAM) * $(MYSQL_GETCLASSICSESSION_PARAM1) * * $(MYSQL_GETCLASSICSESSION_RETURNS) * * $(MYSQL_GETCLASSICSESSION_DETAIL) * * \copydoc connection_options * * Detailed description of the connection data format is available at \ref * connection_data * */ #if DOXYGEN_JS ClassicSession getClassicSession(ConnectionData connectionData, String password) {} #elif DOXYGEN_PY ClassicSession get_classic_session(ConnectionData connectionData, str password) {} #endif REGISTER_HELP_FUNCTION(getSession, mysql); REGISTER_HELP(MYSQL_GETSESSION_BRIEF, "Opens a classic MySQL protocol session to a MySQL server."); REGISTER_HELP(MYSQL_GETSESSION_PARAM, "@param connectionData The connection data for the session"); REGISTER_HELP(MYSQL_GETSESSION_PARAM1, "@param password Optional password for the session"); REGISTER_HELP(MYSQL_GETSESSION_RETURNS, "@returns A ClassicSession"); REGISTER_HELP(MYSQL_GETSESSION_DETAIL, "A ClassicSession object uses the traditional MySQL Protocol to " "allow executing operations on the " "connected MySQL Server."); REGISTER_HELP(MYSQL_GETSESSION_DETAIL1, "${TOPIC_CONNECTION_DATA}"); // clang-format on /** * \ingroup mysql * $(MYSQL_GETSESSION_BRIEF) * * $(MYSQL_GETSESSION_PARAM) * $(MYSQL_GETSESSION_PARAM1) * * $(MYSQL_GETSESSION_RETURNS) * * $(MYSQL_GETSESSION_DETAIL) * * \copydoc connection_options * * Detailed description of the connection data format is available at \ref * connection_data * */ #if DOXYGEN_JS ClassicSession getSession(ConnectionData connectionData, String password) {} #elif DOXYGEN_PY ClassicSession get_session(ConnectionData connectionData, str password) {} #endif #if !defined(DOXYGEN_JS) && !defined(DOXYGEN_PY) std::shared_ptr<shcore::Object_bridge> Mysql::get_session( const mysqlshdk::db::Connection_options &co_, const char *password) { auto co = co_; set_password_from_string(&co, password); co.set_scheme("mysql"); return ClassicSession::create(co); } #endif REGISTER_HELP_FUNCTION(splitScript, mysql); REGISTER_HELP_FUNCTION_TEXT(MYSQL_SPLITSCRIPT, R"*( Split a SQL script into individual statements. @param script A SQL script as a string containing multiple statements @returns A list of statements The default statement delimiter is `;` but it can be changed with the DELIMITER keyword, which must be followed by the delimiter character(s) and a newline. )*"); /** * \ingroup mysql * $(MYSQL_SPLITSCRIPT_BRIEF) * * $(MYSQL_SPLITSCRIPT) */ #if DOXYGEN_JS Array splitScript(String script) {} #elif DOXYGEN_PY list split_script(str script) {} #endif shcore::Value Mysql::split_script(const std::string &script) const { shcore::Array_t array = shcore::make_array(); for (auto s : mysqlshdk::utils::split_sql(script)) array->emplace_back(std::move(s)); return shcore::Value(array); } REGISTER_HELP_FUNCTION(parseStatementAst, mysql); REGISTER_HELP_FUNCTION_TEXT(MYSQL_PARSESTATEMENTAST, R"*( Parse MySQL statements and return its AST representation. @param statements SQL statements to be parsed @returns AST encoded as a JSON structure )*"); /** * \ingroup mysql * $(MYSQL_PARSESTATEMENTAST_BRIEF) * * $(MYSQL_PARSESTATEMENTAST) */ #if DOXYGEN_JS Dictionary parseStatementAst(String statements) {} #elif DOXYGEN_PY dict parse_statement_ast(str statements) {} #endif shcore::Value Mysql::parse_statement_ast(const std::string &sql) const { auto array = shcore::make_array(); int errors = 0; shcore::Value root = shcore::Value(shcore::make_dict("children", shcore::Value(array))); mysqlshdk::parser::traverse_script_ast<shcore::Value>( sql, {}, false, false, &root, [](const std::string &, size_t, bool) {}, [](const mysqlshdk::parser::AST_rule_node &node, bool enter, shcore::Value *parent) -> shcore::Value * { if (enter) { auto parent_list = parent->as_map()->get_array("children"); parent_list->emplace_back(shcore::make_dict( "rule", node.name, "children", shcore::make_array())); return &parent_list->back(); } return nullptr; }, [](const mysqlshdk::parser::AST_terminal_node &node, shcore::Value *parent) { auto parent_list = parent->as_map()->get_array("children"); parent_list->emplace_back( shcore::make_dict("symbol", node.name, "text", node.text)); return &parent_list->back(); }, [&errors](const mysqlshdk::parser::AST_error_node &node, shcore::Value *parent) { auto parent_list = parent->as_map()->get_array("children"); ++errors; parent_list->emplace_back(shcore::make_dict( "symbol", node.name, "text", node.text, "line", node.line, "offset", node.offset, "error", shcore::Value(1))); return &parent_list->back(); }); // this is only supposed to return 1 node, so remove the parent assert(array->size() <= 1); if (array->size() == 0) return {}; return array->at(0); } REGISTER_HELP_FUNCTION(quoteIdentifier, mysql); REGISTER_HELP_FUNCTION_TEXT(MYSQL_QUOTEIDENTIFIER, R"*( Quote a string as a MySQL identifier, escaping characters when needed. @param s the identifier name to be quoted @returns Quoted identifier )*"); /** * \ingroup mysql * $(MYSQL_QUOTEIDENTIFIER_BRIEF) * * $(MYSQL_QUOTEIDENTIFIER) */ #if DOXYGEN_JS String quoteIdentifier(String s) {} #elif DOXYGEN_PY str quote_identifier(str s) {} #endif std::string Mysql::quote_identifier(const std::string &s) const { return shcore::quote_identifier(s); } REGISTER_HELP_FUNCTION(unquoteIdentifier, mysql); REGISTER_HELP_FUNCTION_TEXT(MYSQL_UNQUOTEIDENTIFIER, R"*( Unquote a MySQL identifier. @param s String to unquote @returns Unquoted string. An exception is thrown if the input string is not quoted with backticks. )*"); /** * \ingroup mysql * $(MYSQL_UNQUOTEIDENTIFIER_BRIEF) * * $(MYSQL_UNQUOTEIDENTIFIER) */ #if DOXYGEN_JS String unquoteIdentifier(String s) {} #elif DOXYGEN_PY str unquote_identifier(str s) {} #endif std::string Mysql::unquote_identifier(const std::string &s) const { return shcore::unquote_identifier(s); } } // namespace mysql } // namespace mysqlsh