diff --git a/include/MySQL_Thread.h b/include/MySQL_Thread.h index a3dd6aef7..4afa51740 100644 --- a/include/MySQL_Thread.h +++ b/include/MySQL_Thread.h @@ -535,6 +535,7 @@ class MySQL_Threads_Handler bool client_session_track_gtid; bool enable_client_deprecate_eof; bool enable_server_deprecate_eof; + bool enable_load_data_local_infile; bool log_mysql_warnings_enabled; } variables; struct { diff --git a/include/proxysql_structs.h b/include/proxysql_structs.h index ca53c1319..57a47eb81 100644 --- a/include/proxysql_structs.h +++ b/include/proxysql_structs.h @@ -798,6 +798,7 @@ __thread int mysql_thread___query_digests_grouping_limit; __thread bool mysql_thread___enable_client_deprecate_eof; __thread bool mysql_thread___enable_server_deprecate_eof; __thread bool mysql_thread___log_mysql_warnings_enabled; +__thread bool mysql_thread___enable_load_data_local_infile; /* variables used for Query Cache */ __thread int mysql_thread___query_cache_size_MB; @@ -949,6 +950,7 @@ extern __thread int mysql_thread___query_digests_grouping_limit; extern __thread bool mysql_thread___enable_client_deprecate_eof; extern __thread bool mysql_thread___enable_server_deprecate_eof; extern __thread bool mysql_thread___log_mysql_warnings_enabled; +extern __thread bool mysql_thread___enable_load_data_local_infile; /* variables used for Query Cache */ extern __thread int mysql_thread___query_cache_size_MB; diff --git a/lib/MySQL_Session.cpp b/lib/MySQL_Session.cpp index 69ec4c5b0..9a9f583ea 100644 --- a/lib/MySQL_Session.cpp +++ b/lib/MySQL_Session.cpp @@ -1334,15 +1334,29 @@ bool MySQL_Session::handler_special_queries(PtrSize_t *pkt) { } // 'LOAD DATA LOCAL INFILE' is unsupported. We report an specific error to inform clients about this fact. For more context see #833. if ( (pkt->size >= 22 + 5) && (strncasecmp((char *)"LOAD DATA LOCAL INFILE",(char *)pkt->ptr+5, 22)==0) ) { - client_myds->DSS=STATE_QUERY_SENT_NET; - client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,1,1047,(char *)"HY000",(char *)"Unsupported 'LOAD DATA LOCAL INFILE' command",true); - client_myds->DSS=STATE_SLEEP; - status=WAITING_CLIENT_DATA; - if (mirror==false) { - RequestEnd(NULL); + if (mysql_thread___enable_load_data_local_infile == false) { + client_myds->DSS=STATE_QUERY_SENT_NET; + client_myds->myprot.generate_pkt_ERR(true,NULL,NULL,1,1047,(char *)"HY000",(char *)"Unsupported 'LOAD DATA LOCAL INFILE' command",true); + client_myds->DSS=STATE_SLEEP; + status=WAITING_CLIENT_DATA; + if (mirror==false) { + RequestEnd(NULL); + } + l_free(pkt->size,pkt->ptr); + return true; + } else { + if (mysql_thread___verbose_query_error) { + proxy_warning( + "Command '%.*s' refers to file in ProxySQL instance, NOT on client side!\n", + pkt->size - sizeof(mysql_hdr) - 1, + static_cast(pkt->ptr) + 5 + ); + } else { + proxy_warning( + "Command 'LOAD DATA LOCAL INFILE' refers to file in ProxySQL instance, NOT on client side!\n" + ); + } } - l_free(pkt->size,pkt->ptr); - return true; } return false; diff --git a/lib/MySQL_Thread.cpp b/lib/MySQL_Thread.cpp index 1c29278ba..946942294 100644 --- a/lib/MySQL_Thread.cpp +++ b/lib/MySQL_Thread.cpp @@ -421,6 +421,7 @@ static char * mysql_thread_variables_names[]= { (char *)"connect_timeout_server_max", (char *)"enable_client_deprecate_eof", (char *)"enable_server_deprecate_eof", + (char *)"enable_load_data_local_infile", (char *)"eventslog_filename", (char *)"eventslog_filesize", (char *)"eventslog_default_log", @@ -1158,6 +1159,7 @@ MySQL_Threads_Handler::MySQL_Threads_Handler() { variables.query_digests_grouping_limit = 3; variables.enable_client_deprecate_eof=true; variables.enable_server_deprecate_eof=true; + variables.enable_load_data_local_infile=false; variables.log_mysql_warnings_enabled=false; // status variables status_variables.mirror_sessions_current=0; @@ -1970,6 +1972,7 @@ char ** MySQL_Threads_Handler::get_variables_list() { VariablesPointers_bool["default_reconnect"] = make_tuple(&variables.default_reconnect, false); VariablesPointers_bool["enable_client_deprecate_eof"] = make_tuple(&variables.enable_client_deprecate_eof, false); VariablesPointers_bool["enable_server_deprecate_eof"] = make_tuple(&variables.enable_server_deprecate_eof, false); + VariablesPointers_bool["enable_load_data_local_infile"] = make_tuple(&variables.enable_load_data_local_infile, false); VariablesPointers_bool["enforce_autocommit_on_reads"] = make_tuple(&variables.enforce_autocommit_on_reads, false); VariablesPointers_bool["firewall_whitelist_enabled"] = make_tuple(&variables.firewall_whitelist_enabled, false); VariablesPointers_bool["kill_backend_connection_when_disconnect"] = make_tuple(&variables.kill_backend_connection_when_disconnect, false); @@ -3655,6 +3658,7 @@ void MySQL_Thread::refresh_variables() { mysql_thread___default_reconnect=(bool)GloMTH->get_variable_int((char *)"default_reconnect"); mysql_thread___enable_client_deprecate_eof=(bool)GloMTH->get_variable_int((char *)"enable_client_deprecate_eof"); mysql_thread___enable_server_deprecate_eof=(bool)GloMTH->get_variable_int((char *)"enable_server_deprecate_eof"); + mysql_thread___enable_load_data_local_infile=(bool)GloMTH->get_variable_int((char *)"enable_load_data_local_infile"); mysql_thread___log_mysql_warnings_enabled=(bool)GloMTH->get_variable_int((char *)"log_mysql_warnings_enabled"); #ifdef DEBUG mysql_thread___session_debug=(bool)GloMTH->get_variable_int((char *)"session_debug"); diff --git a/lib/mysql_data_stream.cpp b/lib/mysql_data_stream.cpp index 46b39b473..91a6d28ab 100644 --- a/lib/mysql_data_stream.cpp +++ b/lib/mysql_data_stream.cpp @@ -1352,9 +1352,16 @@ void MySQL_Data_Stream::return_MySQL_Connection_To_Pool() { unsigned long long intv = mysql_thread___connection_max_age_ms; intv *= 1000; if ( - ( (intv) && (mc->last_time_used > mc->creation_time + intv) ) + (( (intv) && (mc->last_time_used > mc->creation_time + intv) ) || - ( mc->local_stmts->get_num_backend_stmts() > (unsigned int)GloMTH->variables.max_stmts_per_connection ) + ( mc->local_stmts->get_num_backend_stmts() > (unsigned int)GloMTH->variables.max_stmts_per_connection )) + && + // NOTE: If the current session if in 'PINGING_SERVER' status, there is + // no need to reset the session. The destruction and creation of a new + // session in case this session has exceeded the time specified by + // 'connection_max_age_ms' will be deferred to the next time the session + // is used outside 'PINGING_SERVER' operation. For more context see #3502. + sess->status != PINGING_SERVER ) { if (mysql_thread___reset_connection_algorithm == 2) { sess->create_new_session_and_reset_connection(this); diff --git a/src/SQLite3_Server.cpp b/src/SQLite3_Server.cpp index 7723d2cac..eee8817b5 100644 --- a/src/SQLite3_Server.cpp +++ b/src/SQLite3_Server.cpp @@ -765,6 +765,14 @@ static void * sqlite3server_main_loop(void *arg) } fds[i].revents=0; } + // NOTE: In case the address imposed by 'sqliteserver-mysql_ifaces' isn't avaible, + // a infinite loop could take place if 'POLLNVAL' is not checked here. + // This means that trying to set a 'mysql_ifaces' to an address that is + // already taken will result into an 'assert' in ProxySQL side. + if (nfds == 1 && fds[0].revents == POLLNVAL) { + proxy_error("revents==POLLNVAL for FD=%d, events=%d\n", fds[i].fd, fds[i].events); + assert(fds[0].revents != POLLNVAL); + } __end_while_pool: if (S_amll.get_version()!=version) { S_amll.wrlock(); diff --git a/test/tap/tap/utils.cpp b/test/tap/tap/utils.cpp index 9edcb7280..f107c06eb 100644 --- a/test/tap/tap/utils.cpp +++ b/test/tap/tap/utils.cpp @@ -470,3 +470,25 @@ int exec(const std::string& cmd, std::string& result) { } return err; } + +std::vector extract_mysql_rows(MYSQL_RES* my_res) { + if (my_res == nullptr) { return {}; } + + std::vector result {}; + MYSQL_ROW row = nullptr; + uint32_t num_fields = mysql_num_fields(my_res); + + while ((row = mysql_fetch_row(my_res))) { + mysql_res_row row_values {}; + uint64_t *lengths = mysql_fetch_lengths(my_res); + + for (uint32_t i = 0; i < num_fields; i++) { + std::string field_val(row[i], lengths[i]); + row_values.push_back(field_val); + } + + result.push_back(row_values); + } + + return result; +}; diff --git a/test/tap/tap/utils.h b/test/tap/tap/utils.h index 062f2e8f1..19f859669 100644 --- a/test/tap/tap/utils.h +++ b/test/tap/tap/utils.h @@ -70,8 +70,19 @@ int execvp(const std::string& file, const std::vector& argv, std::s int exec(const std::string& cmd, std::string& result); + // create table test.sbtest1 with num_rows rows int create_table_test_sbtest1(int num_rows, MYSQL *mysql); +using mysql_res_row = std::vector; + +/** + * @brief Function that extracts the provided 'MYSQL_RES' into a vector of vector of + * strings. + * @param my_res The 'MYSQL_RES' for which to extract the values. In case of + * being NULL an empty vector is returned. + * @return The extracted values of all the rows present in the resultset. + */ +std::vector extract_mysql_rows(MYSQL_RES* my_res); #endif // #define UTILS_H diff --git a/test/tap/tests/load_data_local_datadir/insert_data.txt b/test/tap/tests/load_data_local_datadir/insert_data.txt new file mode 100644 index 000000000..22556d0e1 --- /dev/null +++ b/test/tap/tests/load_data_local_datadir/insert_data.txt @@ -0,0 +1,4 @@ +1,"a string","100.20" +2,"a string containing a , comma","102.20" +3,"a string containing a \" quote","102.20" +4,"a string containing a \", quote and comma","102.20" diff --git a/test/tap/tests/test_sqlite3_server-t.cpp b/test/tap/tests/test_sqlite3_server-t.cpp new file mode 100644 index 000000000..860e86091 --- /dev/null +++ b/test/tap/tests/test_sqlite3_server-t.cpp @@ -0,0 +1,473 @@ +/** + * @file test_sqlite3_server-t.cpp + * @brief Test to perform multiple operations over ProxySQL SQLite3 server. + * @details It performs the following operations: + * - Connects to sqlite3 with a wrong username. + * - Connects to sqlite3 with a right username but wrong password. + * - Successfully connects to sqlite3 and runs: + * + SHOW SCHEMAS + * + SHOW DATABASES + * + SELECT DATABASE() + * + select DATABASE(), USER() limit 1 + * + select @@version_comment limit 1 + * + select @@character_set_client, @@character_set_connection, @@character_set_server, @@character_set_database limit 1 + * - Successfully connects to sqlite3 and runs a variety of queries: + * + CREATE TABLE, SHOW CREATE TABLE, INSERT, SELECT, DROP TABLE... + * + Queries that induce errors: syntax error, duplicate keys, etc... + * - Changes 'sqliteserver-mysql_ifaces' and tries to connect to the new interface. + * - Connects to ProxySQL Admin and performs the following operations: + * + LOAD|SAVE SQLITESERVER TO|FROM RUNTIME|MEMORY|DISK + * + * NOTE: 'sqliteserver-read_only' is completely omitted from this test because + * it's **currently unused**. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +using query_spec = std::tuple; + +const int sqlite3_port = 0; + +/** + * @brief Extract the current 'sqliteserver-mysql_ifaces' from ProxySQL config. + * @param proxysql_admin An already opened connection to ProxySQL Admin. + * @return EXIT_SUCCESS, or one of the following error codes: + * - EINVAL if supplied 'proxysql_admin' is NULL. + * - '-1' in case of ProxySQL returns an 'NULL' row for the query selecting + * the variable 'sqliteserver-read_only'. + * - EXIT_FAILURE in case other operation failed. + */ +int get_sqlite3_ifaces(MYSQL* proxysql_admin, std::string& sqlite3_ifaces) { + if (proxysql_admin == NULL) { + return EINVAL; + } + + int res = EXIT_FAILURE; + + MYSQL_QUERY( + proxysql_admin, + "SELECT * FROM global_variables WHERE Variable_name='sqliteserver-mysql_ifaces'" + ); + + MYSQL_RES* admin_res = mysql_store_result(proxysql_admin); + if (!admin_res) { + diag("'mysql_store_result' at line %d failed: %s", __LINE__, mysql_error(proxysql_admin)); + goto cleanup; + } + + { + MYSQL_ROW row = mysql_fetch_row(admin_res); + if (!row || row[0] == nullptr || row[1] == nullptr) { + diag("'mysql_fetch_row' at line %d returned 'NULL'", __LINE__); + res = -1; + goto cleanup; + } + + std::string _sqlite3_ifaces { row[1] }; + sqlite3_ifaces = _sqlite3_ifaces; + res = EXIT_SUCCESS; + } + +cleanup: + + return res; +} + +void fetch_and_discard_results(MYSQL_RES* result, bool verbose=false) { + MYSQL_ROW row = nullptr; + unsigned int num_fields = 0; + unsigned int i = 0; + unsigned int j = 0; + + num_fields = mysql_num_fields(result); + while ((row = mysql_fetch_row(result))) { + unsigned long *lengths = mysql_fetch_lengths(result); + + if (verbose) { + printf("# RowNum_%d: ", j); + } + + for(i = 0; i < num_fields; i++) { + if (verbose) { + printf("[%.*s] ", (int) lengths[i], row[i] ? row[i] : "NULL"); + } + } + + if (verbose) { + printf("\n"); + } + + j++; + } +} + +/** + * @brief Execute the supplied queries and check that the return codes are the + * ones specified. + * + * @param proxysql_sqlite3 An already opened MYSQL connection to ProxySQL + * SQLite3 server. + * @param queries The queries to be performed and check. + */ +void execute_and_check_queries(MYSQL* proxysql_sqlite3, const std::vector& queries) { + for (const auto& supp_query : queries) { + const std::string query = std::get<0>(supp_query); + const int exp_err_code = std::get<1>(supp_query); + + int query_err = mysql_query(proxysql_sqlite3, query.c_str()); + MYSQL_RES* result = mysql_store_result(proxysql_sqlite3); + if (result) { + fetch_and_discard_results(result, true); + mysql_free_result(result); + } + + int m_errno = mysql_errno(proxysql_sqlite3); + const char* m_error = mysql_error(proxysql_sqlite3); + + if (exp_err_code == 0) { + ok( + exp_err_code == m_errno, + "Query '%s' should succeed. Error code: (Expected:'%d' == Actual:'%d')", + query.c_str(), exp_err_code, m_errno + ); + } else { + ok( + exp_err_code == m_errno, + "Query '%s' should fail. Error code: (Expected:'%d' == Actual:'%d'), Err: '%s'", + query.c_str(), exp_err_code, m_errno, m_error + ); + } + } +} + +int extract_sqlite3_host_port(MYSQL* proxysql_admin, std::pair& host_port) { + if (proxysql_admin == nullptr) { return EINVAL; } + int res = EXIT_SUCCESS; + + std::string sqlite3_ifaces {}; + int ifaces_err = get_sqlite3_ifaces(proxysql_admin, sqlite3_ifaces); + + // ProxySQL is likely to have been launched without "--sqlite3-server" flag + if (ifaces_err == -1) { + diag("ProxySQL was launched without '--sqlite3-server' flag"); + res = EXIT_FAILURE; + return res; + } + + // Extract the correct port to connect to SQLite server + std::string::size_type colon_pos = sqlite3_ifaces.find(":"); + if (colon_pos == std::string::npos) { + diag("ProxySQL returned a malformed 'sqliteserver-mysql_ifaces': %s", sqlite3_ifaces.c_str()); + res = EXIT_FAILURE; + return res; + } + + std::string sqlite3_host { sqlite3_ifaces.substr(0, colon_pos) }; + std::string sqlite3_port { sqlite3_ifaces.substr(colon_pos + 1) }; + + // Check that port has valid conversion + char* end_pos = nullptr; + int i_sqlite3_port = std::strtol(sqlite3_port.c_str(), &end_pos, 10); + + if (errno == ERANGE || (end_pos != &sqlite3_port.back() + 1)) { + diag( + "ProxySQL returned a invalid port number within 'sqliteserver-mysql_ifaces': %s", + sqlite3_ifaces.c_str() + ); + res = EXIT_FAILURE; + return res; + } + + if (res == EXIT_SUCCESS) { + host_port = { sqlite3_host, i_sqlite3_port }; + } + + return res; +} + +/** + * @brief List of the pairs holding a series of queries that should be + * successfully performed against ProxySQL SQLite3 server. + */ +std::vector successful_queries { + std::make_tuple("SHOW SCHEMAS", 0), + std::make_tuple("SHOW DATABASES", 0), + std::make_tuple("SELECT DATABASE()", 0), + std::make_tuple("SELECT DATABASE(), USER() LIMIT 1", 0), + std::make_tuple("SELECT @@version_comment LIMIT 1", 0), + std::make_tuple( + "SELECT @@character_set_client, @@character_set_connection," + " @@character_set_server, @@character_set_database LIMIT 1", + 0 + ), + std::make_tuple( + "CREATE TABLE IF NOT EXISTS test_sqlite3_server_p0712(" + " c1 INTEGER PRIMARY KEY AUTOINCREMENT," + " c2 VARCHAR(100)," + " c3 VARCHAR(100)" + ")", + 0 + ), + std::make_tuple("SHOW CREATE TABLE test_sqlite3_server_p0712", 0), + std::make_tuple("SHOW TABLES", 0), + std::make_tuple( + "INSERT INTO test_sqlite3_server_p0712" + " (c2, c3) VALUES ('1234', '1234')", + 0 + ), + std::make_tuple( + "INSERT INTO test_sqlite3_server_p0712" + " (c2, c3) VALUES ('123555555', '12355555')", + 0 + ), + std::make_tuple( + "DELETE FROM test_sqlite3_server_p0712", + 0 + ), + std::make_tuple("DROP TABLE test_sqlite3_server_p0712", 0), + std::make_tuple("SHOW TABLES", 0), +}; + +/** + * @brief List of the pairs holding a series of queries in which *some* + * should fail when executed against ProxySQL SQLite3 server. + */ +std::vector unsuccessful_queries { + std::make_tuple("SHOW CHEMAS", 1045), + std::make_tuple("SHOW DAABASES", 1045), + std::make_tuple("SELECT DAABASE()", 1045), + std::make_tuple("SELECT DAABASE(), USER() LIMIT 1", 1045), + std::make_tuple("SHOW CREATE TABLE test_sqlite3_server_p0712", 0), + std::make_tuple( + "CREATE TABLE IF NOT EXISTS test_sqlite3_server_p0712(" + " c1 INTEGER PRIMARY KEY AUTOINCREMENT," + " c2 VARCHAR(100)," + " c3 VARCHAR(100)" + ")", + 0 + ), + std::make_tuple( + "INSERT INTO test_sqlite3_server_p0712" + " (c2, c3) VALUES ('1234', '1234')", + 0 + ), + std::make_tuple( + "INSERT INTO test_sqlite3_server_p0712" + " (c1, c2, c3) VALUES (1, '1235', '1235')", + 1045 + ), + std::make_tuple( + "USE foobar", + 1045 + ), + std::make_tuple( + "DROP TABLE test_sqlite3_server_p0712_non_existent", + 1045 + ), + std::make_tuple( + "DROP TABLE test_sqlite3_server_p0712", + 0 + ), +}; + +/** + * @brief Perform several admin queries to exercise more paths. + */ +std::vector admin_queries { + "LOAD SQLITESERVER VARIABLES FROM DISK", + "LOAD SQLITESERVER VARIABLES TO RUNTIME", + "SAVE SQLITESERVER VARIABLES FROM RUNTIME", + "SAVE SQLITESERVER VARIABLES TO DISK" +}; + +/** + * @brief Perform several admin queries to exercise more paths. + */ +std::vector sqlite_intf_queries { + "SET sqliteserver-mysql_ifaces='127.0.0.1:6035'", + "LOAD SQLITESERVER VARIABLES TO RUNTIME" +}; + +int main(int argc, char** argv) { + CommandLine cl; + + // plan as many tests as queries + plan( + 2 /* Fail to connect with wrong username and password */ + successful_queries.size() + + unsuccessful_queries.size() + admin_queries.size() + sqlite_intf_queries.size() + + 1 /* Connect to new setup interface */ + ); + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + MYSQL* proxysql_admin = mysql_init(NULL); + + // Connect to ProxySQL Admin and check current SQLite3 configuration + if ( + !mysql_real_connect( + proxysql_admin, cl.host, cl.admin_username, cl.admin_password, + NULL, cl.admin_port, NULL, 0 + ) + ) { + fprintf( + stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, + mysql_error(proxysql_admin) + ); + return EXIT_FAILURE; + } + + { + std::pair host_port {}; + int host_port_err = extract_sqlite3_host_port(proxysql_admin, host_port); + if (host_port_err) { + diag("Failed to get and parse 'sqliteserver-mysql_ifaces' at line '%d'", __LINE__); + goto cleanup; + } + + MYSQL* proxysql_sqlite3 = mysql_init(NULL); + + // Connect with invalid username + std::string inv_user_err {}; + bool failed_to_connect = false; + if ( + !mysql_real_connect( + proxysql_sqlite3, host_port.first.c_str(), "foobar_user", cl.password, + NULL, host_port.second, NULL, 0 + ) + ) { + inv_user_err = mysql_error(proxysql_sqlite3); + failed_to_connect = true; + } + + ok( + failed_to_connect, + "An invalid user should fail to connect to SQLite3 server, error was: %s", + inv_user_err.c_str() + ); + + // Reinitialize MYSQL handle + mysql_close(proxysql_sqlite3); + proxysql_sqlite3 = mysql_init(NULL); + + // Connect with invalid password + std::string inv_pass_err {}; + failed_to_connect = false; + if ( + !mysql_real_connect( + proxysql_sqlite3, host_port.first.c_str(), cl.username, "foobar_pass", + NULL, host_port.second, NULL, 0 + ) + ) { + inv_pass_err = mysql_error(proxysql_sqlite3); + failed_to_connect = true; + } + + ok( + failed_to_connect, + "An invalid user should fail to connect to SQLite3 server, error was: %s", + inv_pass_err.c_str() + ); + + // Reinitialize MYSQL handle + mysql_close(proxysql_sqlite3); + proxysql_sqlite3 = mysql_init(NULL); + + // Correctly connect to SQLite3 server + if ( + !mysql_real_connect( + proxysql_sqlite3, host_port.first.c_str(), cl.username, cl.password, + NULL, host_port.second, NULL, 0 + ) + ) { + fprintf( + stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, + mysql_error(proxysql_sqlite3) + ); + goto cleanup; + } + + diag("Started performing successful queries"); + execute_and_check_queries(proxysql_sqlite3, successful_queries); + + diag("Started performing failing queries"); + execute_and_check_queries(proxysql_sqlite3, unsuccessful_queries); + + // Reinitialize MYSQL handle + mysql_close(proxysql_sqlite3); + proxysql_sqlite3 = mysql_init(NULL); + + // Change SQLite interface and connect to new port + for (const auto& admin_query : sqlite_intf_queries) { + int query_err = mysql_query(proxysql_admin, admin_query.c_str()); + ok( + query_err == 0, "Admin query '%s' should succeed. Line: %d, Err: '%s'", + admin_query.c_str(), __LINE__, mysql_error(proxysql_admin) + ); + } + + // NOTE: Wait for ProxySQL to reconfigure, changing SQLite3 interface. + // Trying to perform a connection immediately after changing the + // interface could lead to 'EADDRINUSE' in ProxySQL side. + sleep(1); + + // Connect to the new interface + std::pair new_host_port {}; + int ext_intf_err = extract_sqlite3_host_port(proxysql_admin, new_host_port); + if (ext_intf_err) { + diag("Failed to get and parse 'sqliteserver-mysql_ifaces' at line '%d'", __LINE__); + goto cleanup; + } + + // Connect with invalid username + bool success_to_connect = true; + std::string new_intf_conn_err {}; + if ( + !mysql_real_connect( + proxysql_sqlite3, new_host_port.first.c_str(), cl.username, cl.password, + NULL, new_host_port.second, NULL, 0 + ) + ) { + new_intf_conn_err = mysql_error(proxysql_sqlite3); + success_to_connect = false; + } + + ok( + success_to_connect, + "A connection to the new selected interface should success, error was: '%s'", + new_intf_conn_err.c_str() + ); + + mysql_close(proxysql_sqlite3); + + // Perform the final Admin queries + for (const auto& admin_query : admin_queries) { + int query_err = mysql_query(proxysql_admin, admin_query.c_str()); + ok( + query_err == 0, "Admin query '%s' should succeed. Line: %d, Err: '%s'", + admin_query.c_str(), __LINE__, mysql_error(proxysql_admin) + ); + } + } + +cleanup: + + mysql_close(proxysql_admin); + + return exit_status(); +} diff --git a/test/tap/tests/test_unsupported_queries-t.cpp b/test/tap/tests/test_unsupported_queries-t.cpp index 3bf9dd3d7..2c02045ef 100644 --- a/test/tap/tests/test_unsupported_queries-t.cpp +++ b/test/tap/tests/test_unsupported_queries-t.cpp @@ -1,9 +1,12 @@ /** * @file test_unsupported_queries-t.cpp - * @brief Simple test to check that unsupported queries by ProxySQL return the expected error codes. + * @brief Test to check that unsupported queries, and queries that can be + * enabled or disabled via configuration variables, return the expected error + * codes, and perform correctly when enabled. */ #include +#include #include #include #include @@ -12,8 +15,10 @@ #include #include -#include "tap.h" #include "command_line.h" +#include "json.hpp" +#include "proxysql_utils.h" +#include "tap.h" #include "utils.h" /** @@ -21,16 +26,538 @@ * together with the error code that they should return. */ std::vector> unsupported_queries { - std::make_tuple("LOAD DATA LOCAL INFILE", 1047, "Unsupported 'LOAD DATA LOCAL INFILE' command"), - std::make_tuple("LOAD DATA LOCAL INFILE 'data.txt' INTO TABLE db.test_table", 1047, "Unsupported 'LOAD DATA LOCAL INFILE' command"), - std::make_tuple("LOAD DATA LOCAL INFILE '/tmp/test.txt' INTO TABLE test IGNORE 1 LINES", 1047, "Unsupported 'LOAD DATA LOCAL INFILE' command"), + std::make_tuple( + "LOAD DATA LOCAL INFILE", + 1047, + "Unsupported 'LOAD DATA LOCAL INFILE' command" + ), + std::make_tuple( + "LOAD DATA LOCAL INFILE 'data.txt' INTO TABLE db.test_table", + 1047, + "Unsupported 'LOAD DATA LOCAL INFILE' command" + ), + std::make_tuple( + "LOAD DATA LOCAL INFILE '/tmp/test.txt' INTO TABLE test IGNORE 1 LINES", + 1047, + "Unsupported 'LOAD DATA LOCAL INFILE' command" + ), +}; + +/** + * @brief Type holding the required information for identifying, enabling and + * disabling a query which support can be enabled and disabled by ProxySQL. + */ +using query_test_info = + std::tuple< + // Query to be tested + std::string, + // Variable name enabling / disabling the query + std::string, + // Value for enabling the query + std::string, + // Value for diabling the query + std::string, + // Expected error code in case of failure + int, + // Function performing an internal 'ok' test checking that the + // enabled / disabled query responds as expected + std::function + >; + +// "SET mysql-enable_load_data_local_infile='true'", + + +/** + * @brief Extract the current value for a given 'variable_name' from + * ProxySQL current configuration, either MEMORY or RUNTIME. + * @param proxysql_admin An already opened connection to ProxySQL Admin. + * @param variable_name The name of the variable to be retrieved from ProxySQL + * config. + * @param variable_value Reference to string acting as output parameter which + * will content the value of the specified variable. + * @return EXIT_SUCCESS, or one of the following error codes: + * - EINVAL if supplied 'proxysql_admin' is NULL. + * - '-1' in case of ProxySQL returns an 'NULL' row for the query selecting + * the variable 'sqliteserver-read_only'. + * - EXIT_FAILURE in case other operation failed. + */ +int get_variable_value( + MYSQL* proxysql_admin, const std::string& variable_name, + std::string& variable_value, bool runtime=false +) { + if (proxysql_admin == NULL) { + return EINVAL; + } + + int res = EXIT_FAILURE; + + const std::string t_select_var_query { + "SELECT * FROM %sglobal_variables WHERE Variable_name='%s'" + }; + std::string select_var_query {}; + + if (runtime) { + string_format(t_select_var_query, select_var_query, "runtime_", variable_name.c_str()); + } else { + string_format(t_select_var_query, select_var_query, "", variable_name.c_str()); + } + + MYSQL_QUERY(proxysql_admin, select_var_query.c_str()); + + MYSQL_RES* admin_res = mysql_store_result(proxysql_admin); + if (!admin_res) { + diag("'mysql_store_result' at line %d failed: %s", __LINE__, mysql_error(proxysql_admin)); + goto cleanup; + } + + { + MYSQL_ROW row = mysql_fetch_row(admin_res); + if (!row || row[0] == nullptr || row[1] == nullptr) { + diag("'mysql_fetch_row' at line %d returned 'NULL'", __LINE__); + res = -1; + goto cleanup; + } + + // Extract the result + std::string _variable_value { row[1] }; + variable_value = _variable_value; + + res = EXIT_SUCCESS; + } + +cleanup: + + return res; +} + +/** + * @brief Enable the query based using the information supplied in the + * 'query_info' parameter, and verifies that the value of the query has properly + * change at runtime. + * + * @param proxysql_admin An already oppened connection to ProxySQL Admin. + * @param query_info Information about the query to be enabled. + * + * @return True if the query was properly enabled, false if not. + */ +bool enable_query(MYSQL* proxysql_admin, const query_test_info& query_info, bool enable=true) { + std::string exp_var_value {}; + + // In case of false, we choose the value for disabling the variable + if (enable == true) { + exp_var_value = std::get<2>(query_info); + } else { + exp_var_value = std::get<3>(query_info); + } + + std::vector enabling_queries { + "SET " + std::get<1>(query_info) + " = " + exp_var_value, + "LOAD MYSQL VARIABLES TO RUNTIME" + }; + + bool query_enabling_succeed = true; + + for (const auto& query : enabling_queries) { + int query_res = mysql_query(proxysql_admin, query.c_str()); + if (query_res) { + diag( + "Query '%s' for enabling query '%s' enabling at line '%d', with error: '%s'", + query.c_str(), std::get<0>(query_info).c_str(), __LINE__, + mysql_error(proxysql_admin) + ); + query_enabling_succeed = false; + goto exit; + } + } + + { + std::string variable_value {}; + int var_err = get_variable_value( + proxysql_admin, std::get<1>(query_info), variable_value, true + ); + + if (var_err) { + diag( + "Getting value for variable '%s', failed with error: '%d'", + std::get<1>(query_info).c_str(), var_err + ); + query_enabling_succeed = false; + goto exit; + } + + // perform a final conversion in case it's required for the exp value + std::string f_exp_var_value {}; + if (exp_var_value == "'true'") { + f_exp_var_value = "true"; + } else if (exp_var_value == "'false'") { + f_exp_var_value = "false"; + } else { + f_exp_var_value = exp_var_value; + } + + if (variable_value != f_exp_var_value) { + query_enabling_succeed = false; + diag( + "Variable value doesn't match expected: (Exp: '%s', Act: '%s')", + exp_var_value.c_str(), variable_value.c_str() + ); + goto exit; + } + } + +exit: + + return query_enabling_succeed; +} + +// ******************* QUERIES TESTING FUNCTIONS ******************** // + +const std::vector prepare_table_queries { + "CREATE DATABASE IF NOT EXISTS test", + "DROP TABLE IF EXISTS test.load_data_local", + "CREATE TABLE IF NOT EXISTS test.load_data_local (" + " c1 INT NOT NULL AUTO_INCREMENT PRIMARY KEY, c2 VARCHAR(100), c3 VARCHAR(100))", +}; + +using mysql_res_row = std::vector; + +/** + * @brief Helper function that performs the actual check for 'test_load_data_local_infile'. + * + * @param cl CommandLine parameters required for the test. + * @param proxysql An already oppened connection to ProxySQL. + * @param exp_err The expected error code in case we are testing for failure, + * '0' by default. + * @param test_for_success Select the operation mode of the test, 'true' for + * testing for success, 'false' for failure. It's 'true' by default. + */ +void helper_test_load_data_local_infile( + const CommandLine& cl, MYSQL* proxysql, int exp_err=0, bool test_for_success=true +) { + std::string datafile { + std::string { cl.workdir } + "load_data_local_datadir/insert_data.txt" + }; + + bool table_prep_success = true; + + for (const auto& query : prepare_table_queries) { + int query_res = mysql_query(proxysql, query.c_str()); + if (query_res) { + diag( + "Query '%s' for table preparation failed at line '%d', with error: '%s'", + query.c_str(), __LINE__, mysql_error(proxysql) + ); + table_prep_success = false; + break; + } + } + + if (table_prep_success) { + std::string t_load_data_command { + "LOAD DATA LOCAL INFILE \"%s\" INTO TABLE test.load_data_local" + " FIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\\n'" + }; + std::string load_data_command {}; + string_format(t_load_data_command, load_data_command, datafile.c_str()); + + int load_data_res = + mysql_query(proxysql, load_data_command.c_str()); + + if (test_for_success) { + if (load_data_res) { + diag( + load_data_command.c_str(), __LINE__, mysql_error(proxysql) + ); + } + + if (load_data_res == EXIT_SUCCESS) { + diag( + "Supplied query '%s' succeeded, performing check on data...", + load_data_command.c_str() + ); + } else { + diag( + "Supplied query '%s' failed, check not going to be performed. Error was: '%s'.", + load_data_command.c_str(), mysql_error(proxysql) + ); + } + + // Check that the data has actually been loaded to the database + // NOTE: Specifically target 'hostgroup=0' to avoid replication lag. + int myerr = mysql_query(proxysql, "SELECT * /*+ ;hostgroup=0 */ FROM test.load_data_local"); + if (myerr) { + diag( + "Query 'SELECT * FROM test.load_data_local' for table preparation failed" + " at line '%d', with error: '%s'", __LINE__, mysql_error(proxysql) + ); + } else { + MYSQL_RES* result = mysql_store_result(proxysql); + std::vector rows_res { extract_mysql_rows(result) }; + std::vector exp_rows { + { "1","a string","100.20" }, + { "2","a string containing a , comma","102.20" }, + { "3","a string containing a \" quote","102.20" }, + { "4","a string containing a \", quote and comma","102.20" } + }; + + std::string exp_rows_str { "{\n" }; + for (const auto& exp_row : exp_rows) { + std::string exp_row_str { nlohmann::json(exp_row).dump() }; + exp_rows_str += " " + exp_row_str + ",\n"; + } + exp_rows_str += "}\n"; + + diag("Expected values for rows were: \n%s", exp_rows_str.c_str()); + + std::string act_rows_str { "{\n" }; + for (const auto& act_row : rows_res) { + std::string act_row_str { nlohmann::json(act_row).dump() }; + act_rows_str += " " + act_row_str + ",\n"; + } + act_rows_str += "}\n"; + + diag("Actual values for found rows were: \n%s", act_rows_str.c_str()); + + bool equal = false; + if (!rows_res.empty()) { + equal = std::equal(exp_rows.begin(), exp_rows.end(), rows_res.begin()); + } + + ok(equal, "The selected ROWS were equal to the expected ones"); + } + } else { + if (load_data_res) { + diag( + load_data_command.c_str(), __LINE__, mysql_error(proxysql) + ); + } + + int my_errno = mysql_errno(proxysql); + ok( + my_errno == exp_err, + "Query '%s' should fail. ErrCode: '%d', and error: '%s'", + load_data_command.c_str(), my_errno, mysql_error(proxysql) + ); + } + } +} + +/** + * @brief Perform the same test as 'test_load_data_local_infile', but with + * 'mysql-verbose_query_error' set to 'true'. This test only purpose is + * to exercise the code performing the additional extra logging. + */ +void test_verbose_error_load_data_local_infile( + const CommandLine& cl, MYSQL* proxysql, int exp_err=0, bool test_for_success=true +) { + MYSQL* proxysql_admin = mysql_init(NULL); + + if ( + !mysql_real_connect( + proxysql_admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0 + ) + ) { + diag("File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin)); + return; + } + + std::vector verbose_query_error_true { + "SET mysql-verbose_query_error='true'", + "LOAD MYSQL VARIABLES TO RUNTIME" + }; + for (const auto& query : verbose_query_error_true) { + int query_err = mysql_query(proxysql_admin, query.c_str()); + if (query_err) { + diag("File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin)); + return; + } + } + + helper_test_load_data_local_infile(cl, proxysql, exp_err, test_for_success); + + std::vector verbose_query_error_false { + "SET mysql-verbose_query_error='false'", + "LOAD MYSQL VARIABLES TO RUNTIME" + }; + for (const auto& query : verbose_query_error_false) { + int query_err = mysql_query(proxysql_admin, query.c_str()); + if (query_err) { + diag("File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin)); + return; + } + } + + mysql_close(proxysql_admin); +} + +/** + * @brief Test that the query 'LOAD DATA LOCAL INFILE' performs correctly when + * enabled, and returns the proper error code when disabled. Performs one + * 'ok()' call in case everything went as expected, and several 'diag()' call + * in case of errors. + * + * @param cl CommandLine parameters required for the test. + * @param proxysql An already oppened connection to ProxySQL. + * @param exp_err The expected error code in case we are testing for failure, + * '0' by default. + * @param test_for_success Select the operation mode of the test, 'true' for + * testing for success, 'false' for failure. It's 'true' by default. + */ +void test_load_data_local_infile( + const CommandLine& cl, MYSQL* proxysql, int exp_err=0, bool test_for_success=true +) { + helper_test_load_data_local_infile(cl, proxysql, exp_err, test_for_success); +} + +/** + * @brief Analogous function to 'test_load_data_local_infile' but it + * deliberately provides a non-existing file as an argument to make the query + * fail. + * + * @details This way we make sure that ProxySQL is exhibiting proper behavior + * for this unsupported query that can be misused. + * @param cl CommandLine parameters required for the test. + * @param proxysql An already oppened connection to ProxySQL. + * @param exp_err The expected error code in case we are testing for failure, + * '0' by default. + * @param test_for_success Select the operation mode of the test, 'true' for + * testing for success, 'false' for failure. It's 'true' by default. + */ +void test_failing_load_data_local_infile( + const CommandLine& cl, MYSQL* proxysql, int exp_err=0, bool test_for_success=true +) { + // Supply an invalid file + std::string datafile { + std::string { cl.workdir } + "load_data_local_datadir/non_existing_file.txt" + }; + + bool table_prep_success = true; + + for (const auto& query : prepare_table_queries) { + int query_res = mysql_query(proxysql, query.c_str()); + if (query_res) { + diag( + "Query '%s' for table preparation failed at line '%d', with error: '%s'", + query.c_str(), __LINE__, mysql_error(proxysql) + ); + table_prep_success = false; + break; + } + } + + if (table_prep_success) { + std::string t_load_data_command { + "LOAD DATA LOCAL INFILE \"%s\" INTO TABLE test.load_data_local" + }; + std::string load_data_command {}; + string_format(t_load_data_command, load_data_command, datafile.c_str()); + + int load_data_res = + mysql_query(proxysql, load_data_command.c_str()); + + if (test_for_success) { + if (load_data_res) { + diag( + load_data_command.c_str(), __LINE__, mysql_error(proxysql) + ); + } + + int my_errno = mysql_errno(proxysql); + ok( + (load_data_res != EXIT_SUCCESS) && my_errno == 2, + "Query '%s' should fail. ErrCode: '%d', and error: '%s'", + load_data_command.c_str(), mysql_errno(proxysql), mysql_error(proxysql) + ); + } else { + if (load_data_res) { + diag( + load_data_command.c_str(), __LINE__, mysql_error(proxysql) + ); + } + + int my_errno = mysql_errno(proxysql); + ok( + my_errno == exp_err, + "Query '%s' should fail. ErrCode: '%d', and error: '%s'", + load_data_command.c_str(), my_errno, mysql_error(proxysql) + ); + } + } +} + +// ****************************************************************** // + + +// ********************* QUERIES TESTS INFO ************************ // + +/** + * @brief List of queries which need to be check before performing the + * 'unsupported' checks. + */ +std::vector queries_tests_info { + std::make_tuple< + std::string, std::string, std::string, std::string, int, + std::function + >( + // Query to be tested + "LOAD DATA LOCAL INFILE", + // Variable name enabling / disabling the query + "mysql-enable_load_data_local_infile", + // Value for enabling the query + "'true'", + // Value for diabling the query + "'false'", + // Expected error code in case of failure + 1047, + // Function performing an internal 'ok' test checking that the + // enabled / disabled query responds as expected + test_load_data_local_infile + ), + std::make_tuple< + std::string, std::string, std::string, std::string, int, + std::function + >( + // Query to be tested + "LOAD DATA LOCAL INFILE", + // Variable name enabling / disabling the query + "mysql-enable_load_data_local_infile", + // Value for enabling the query + "'true'", + // Value for diabling the query + "'false'", + // Expected error code in case of failure + 1047, + // Function performing an internal 'ok' test checking that the + // enabled / disabled query responds as expected + test_failing_load_data_local_infile + ), + std::make_tuple< + std::string, std::string, std::string, std::string, int, + std::function + >( + // Query to be tested + "LOAD DATA LOCAL INFILE", + // Variable name enabling / disabling the query + "mysql-enable_load_data_local_infile", + // Value for enabling the query + "'true'", + // Value for diabling the query + "'false'", + // Expected error code in case of failure + 1047, + // Function performing an internal 'ok' test checking that the + // enabled / disabled query responds as expected + test_verbose_error_load_data_local_infile + ), }; +// ****************************************************************** // + int main(int argc, char** argv) { CommandLine cl; // plan as many tests as queries - plan(unsupported_queries.size()); + plan(unsupported_queries.size() + 4 * queries_tests_info.size()); if (cl.getEnv()) { diag("Failed to get the required environmental variables."); @@ -48,7 +575,7 @@ int main(int argc, char** argv) { if (!mysql_real_connect(proxysql_mysql, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_mysql)); - return -1; + return EXIT_FAILURE; } int query_err = mysql_query(proxysql_mysql, query.c_str()); @@ -68,5 +595,56 @@ int main(int argc, char** argv) { mysql_close(proxysql_mysql); } + // Create required connection to ProxySQL admin required to perform the + // tests for conditionally enabled queries. + MYSQL* proxysql_admin = mysql_init(NULL); + + if ( + !mysql_real_connect( + proxysql_admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0 + ) + ) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin)); + return EXIT_FAILURE; + } + + // Enable and test the queries that can be conditionally enabled + for (const auto& query_test_info : queries_tests_info) { + MYSQL* proxysql_mysql = mysql_init(NULL); + + // extract the tuple elements + const std::string query = std::get<0>(query_test_info); + const std::string variable_name = std::get<1>(query_test_info); + int exp_err = std::get<4>(query_test_info); + const auto& testing_fn = std::get<5>(query_test_info); + + if (!mysql_real_connect(proxysql_mysql, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_mysql)); + return EXIT_FAILURE; + } + + bool query_enabling_succeed = enable_query(proxysql_admin, query_test_info, true); + ok( + query_enabling_succeed, "Enabling query '%s' should succeed.", + std::get<0>(query_test_info).c_str() + ); + + // Check that the query is now properly supported + testing_fn(cl, proxysql_mysql, 0, true); + + bool query_disabling_succeed = enable_query(proxysql_admin, query_test_info, false); + ok( + query_disabling_succeed, "Disabling query '%s' should succeed.", + std::get<0>(query_test_info).c_str() + ); + + // Check that the query is now failing + testing_fn(cl, proxysql_mysql, exp_err, false); + + mysql_close(proxysql_mysql); + } + + mysql_close(proxysql_admin); + return exit_status(); }