diff --git a/test/tap/tests/Makefile b/test/tap/tests/Makefile index 9305d3bd7..482186a85 100644 --- a/test/tap/tests/Makefile +++ b/test/tap/tests/Makefile @@ -115,3 +115,9 @@ create_connection_annotation: test_connection_annotation-t.cpp test_set_collation-t: test_set_collation-t.cpp $(TAP_LIBDIR)/libtap.a g++ test_set_collation-t.cpp $(INCLUDEDIRS) $(LDIRS) $(OPT) -std=c++11 $(MYLIBS) -ltap -Wl,--no-as-needed -ldl -lpthread -o test_set_collation-t -DGITVERSION=\"$(GIT_VERSION)\" + +reg_test_1574-stmt_metadata-t: reg_test_1574-stmt_metadata-t.cpp $(TAP_LIBDIR)/libtap.a + g++ -DTEST_AURORA -DDEBUG reg_test_1574-stmt_metadata-t.cpp $(INCLUDEDIRS) $(LDIRS) $(OPT) -std=c++11 $(MYLIBS) -ltap -Wl,--no-as-needed -ldl -lpthread -o reg_test_1574-stmt_metadata-t -DGITVERSION=\"$(GIT_VERSION)\" + +reg_test_1574-mariadb_read_stmt_execute_response-t: reg_test_1574-mariadb_read_stmt_execute_response-t.cpp $(TAP_LIBDIR)/libtap.a + g++ -DTEST_AURORA -DDEBUG reg_test_1574-mariadb_read_stmt_execute_response-t.cpp $(INCLUDEDIRS) $(LDIRS) $(OPT) -std=c++11 $(MYLIBS) -ltap -Wl,--no-as-needed -ldl -lpthread -o reg_test_1574-mariadb_read_stmt_execute_response-t -DGITVERSION=\"$(GIT_VERSION)\" diff --git a/test/tap/tests/reg_test_1574-mariadb_read_stmt_execute_response-t.cpp b/test/tap/tests/reg_test_1574-mariadb_read_stmt_execute_response-t.cpp new file mode 100644 index 000000000..120d0d236 --- /dev/null +++ b/test/tap/tests/reg_test_1574-mariadb_read_stmt_execute_response-t.cpp @@ -0,0 +1,131 @@ +/** + * @file reg_test_1574-mariadb_read_stmt_execute_response-t.cpp + * @brief This test is a regression test for issue #1574. In the fix for this issue some changes were + * introduced into 'read_stmt_execute_response'. These modifications prevent 'mariadb client library' + * from returning CR_NEW_STMT_METADATA in case 'stmt' and 'mysql' fields count doesn't match, instead, + * replaces current stmt fields with the ones returned with the resulset. + * + * @details For checking that this behavior is correct, the test creates a fixed number of tables, + * and prepares a query for all of them. Later the tables are altered, changing their number of + * columns. Finally the statements are executed and the number of fields are check after the execute; + * none error should take place during the executes and after each execute the number of fields + * should match the new (altered) version of the table. + * + * @date 2021-02-15 + */ + +#include +#include +#include +#include +#include + +#include + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +using std::string; + +const int STRING_SIZE=32; +const int NUM_TEST_TABLES = 50; + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return -1; + } + + plan(50); + + MYSQL* mysql = mysql_init(NULL); + if (!mysql) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(mysql)); + return exit_status(); + } + + if (!mysql_real_connect(mysql, cl.host, cl.username, cl.password, NULL, 13306, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(mysql)); + return exit_status(); + } + + MYSQL_QUERY(mysql, "CREATE DATABASE IF NOT EXISTS test"); + std::string create_table_query_t = + "CREATE TEMPORARY TABLE IF NOT EXISTS test.reg_test_read_execute_response_1574_%d (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `c2` varchar(32))"; + + for (int i = 0; i < NUM_TEST_TABLES; i++) { + std::string create_table_query (static_cast(create_table_query_t.size() + 10), '\0'); + snprintf(&create_table_query[0], create_table_query.size(), create_table_query_t.c_str(), i); + MYSQL_QUERY(mysql, create_table_query.c_str()); + } + + std::string select_query_t = "SELECT * FROM test.reg_test_read_execute_response_1574_%d"; + std::vector stmts {}; + + // Initialize and prepare all the statements + for (int i = 0; i < NUM_TEST_TABLES; i++) { + MYSQL_STMT* stmt = mysql_stmt_init(mysql); + if (!stmt) { + ok(false, "mysql_stmt_init(), out of memory\n"); + return exit_status(); + } + + std::string select_query (static_cast(select_query_t.size() + 10), '\0'); + snprintf(&select_query[0], select_query.size(), select_query_t.c_str(), i); + + // Prepare all the statements + if (mysql_stmt_prepare(stmt, select_query.c_str(), strlen(select_query.c_str()))) { + diag("select_query: %s", select_query.c_str()); + ok(false, "mysql_stmt_prepare at line %d failed: %s\n", __LINE__ , mysql_error(mysql)); + mysql_close(mysql); + mysql_library_end(); + return exit_status(); + } + + stmts.push_back(stmt); + } + + // Alter the tables either dropping or adding columns + for (int i = 0; i < NUM_TEST_TABLES; i++) { + std::string alter_table_query_t {}; + + if (i % 2 == 0) { + alter_table_query_t = "ALTER TABLE test.reg_test_read_execute_response_1574_%d ADD c1 BIGINT AFTER id"; + } else { + alter_table_query_t = "ALTER TABLE test.reg_test_read_execute_response_1574_%d DROP COLUMN c2"; + } + + std::string alter_table_query (static_cast(alter_table_query_t.size() + 10), '\0'); + snprintf(&alter_table_query[0], alter_table_query.size(), alter_table_query_t.c_str(), i); + + MYSQL_QUERY(mysql, alter_table_query.c_str()); + } + + // Execute the prepared statement and check that the field count is correct after doing the execute + for (int i = 0; i < NUM_TEST_TABLES; i++) { + MYSQL_STMT* stmt = stmts[i]; + if (mysql_stmt_execute(stmt)) { + ok(false, "mysql_stmt_execute at line %d failed: %s\n", __LINE__ , mysql_stmt_error(stmt)); + } + + int field_count = mysql_stmt_field_count(stmt); + if (i % 2 == 0) { + ok(field_count == 3, "Field count should be '3' in case of 'i % 2' being '0'"); + } else { + ok(field_count == 1, "Field count should be '1' in case of 'i % 2' being '1'"); + } + + if (mysql_stmt_close(stmt)) + { + ok(false, "mysql_stmt_close at line %d failed: %s\n", __LINE__ , mysql_error(mysql)); + return exit_status(); + } + } + + mysql_close(mysql); + + return exit_status(); +} diff --git a/test/tap/tests/reg_test_1574-stmt_metadata-t.cpp b/test/tap/tests/reg_test_1574-stmt_metadata-t.cpp new file mode 100644 index 000000000..8cfdd8761 --- /dev/null +++ b/test/tap/tests/reg_test_1574-stmt_metadata-t.cpp @@ -0,0 +1,197 @@ +/** + * @file reg_test_1574-stmt_metadata-t.cpp + * @brief This test is a regression test for issue #1574. + * @details The test checks that the metadata for a prepared statement is properly updated by ProxySQL + * after a single 'mysql_stmt_execute' and that the resulset received by the client is correct. + * The test performs the following actions: + * 1. Prepares a prepared statement to 'SELECT *' from a table with only 2 columns. + * 2. Runs an 'ALTER TABLE' on that table, adding a new column and inserts new data into it. + * 3. Check the ProxySQL holds the old METADATA information in 'stats_mysql_prepared_statements_info'. + * 4. Performs an execute of the prepared statement. + * 5. Check the ProxySQL holds the new METADATA information in 'stats_mysql_prepared_statements_info'. + * 6. Checks that the results retrieve from the execute holds the correct information. + * @date 2021-02-15 + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +using std::string; + +const int STRING_SIZE=32; + +int g_seed = 0; + +inline int fastrand() { + g_seed = (214013*g_seed+2531011); + return (g_seed>>16)&0x7FFF; +} + +void gen_random_str(char *s, const int len) { + g_seed = time(NULL) ^ getpid() ^ pthread_self(); + static const char alphanum[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + + for (int i = 0; i < len; ++i) { + s[i] = alphanum[fastrand() % (sizeof(alphanum) - 1)]; + } + + s[len] = 0; +} + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return -1; + } + + plan(3); + + MYSQL* proxysql_mysql = mysql_init(NULL); + if (!proxysql_mysql) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_mysql)); + return exit_status(); + } + + 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_status(); + } + + MYSQL* proxysql_admin = mysql_init(NULL); + if (!proxysql_admin) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxysql_admin)); + return exit_status(); + } + + 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_status(); + } + + MYSQL_QUERY(proxysql_mysql, "CREATE DATABASE IF NOT EXISTS test"); + MYSQL_QUERY(proxysql_mysql, "DROP TABLE IF EXISTS test.reg_test_1574"); + MYSQL_QUERY(proxysql_mysql, "CREATE TABLE IF NOT EXISTS test.reg_test_1574 (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `c2` varchar(32))"); + + MYSQL_STMT *stmt = mysql_stmt_init(proxysql_mysql); + if (!stmt) { + ok(false, " mysql_stmt_init(), out of memory\n"); + return exit_status(); + } + + std::string query_t = "SELECT /* %s */ * FROM test.reg_test_1574"; + std::string query (static_cast(query_t.size() + 20), '\0'); + + std::string rnd_str(static_cast(20), '\0'); + gen_random_str(&rnd_str[0], 20); + + snprintf(&query[0], query.size(), query_t.c_str(), rnd_str.c_str()); + + if (mysql_stmt_prepare(stmt, query.c_str(), strlen(query.c_str()))) { + ok(false, "mysql_stmt_prepare at line %d failed: %s\n", __LINE__ , mysql_error(proxysql_mysql)); + mysql_close(proxysql_mysql); + mysql_library_end(); + return exit_status(); + } + + MYSQL_QUERY(proxysql_mysql, "ALTER TABLE test.reg_test_1574 ADD c1 BIGINT AFTER id"); + MYSQL_QUERY(proxysql_mysql, "INSERT INTO test.reg_test_1574 (c1,c2) VALUES (100, 'abcde')"); + + // Check that ProxySQL cached metadata for the query has the old information + std::string num_columns_query_t = "SELECT num_columns FROM stats.stats_mysql_prepared_statements_info WHERE query='%s'"; + std::string num_columns_query (static_cast(num_columns_query_t.size() + query.size()), '\0'); + snprintf(&num_columns_query[0], num_columns_query.size(), num_columns_query_t.c_str(), query.c_str()); + + // Admin query checking for old metadata number of columns + MYSQL_QUERY(proxysql_admin, num_columns_query.c_str()); + MYSQL_RES* result = mysql_store_result(proxysql_admin); + MYSQL_ROW row = mysql_fetch_row(result); + int num_columns = atoi(row[0]); + + ok(num_columns == 2, "Number of 'num_columns' in prepared statement metadata *before* execute should be: (Exp '2' == Actual: %d)", num_columns); + mysql_free_result(result); + + if (mysql_stmt_execute(stmt)) { + ok(false, "mysql_stmt_execute at line %d failed: %s\n", __LINE__ , mysql_stmt_error(stmt)); + } + + // Admin query checking for new metadata number of columns + MYSQL_QUERY(proxysql_admin, num_columns_query.c_str()); + result = mysql_store_result(proxysql_admin); + row = mysql_fetch_row(result); + num_columns = atoi(row[0]); + + ok(num_columns == 3, "Number of 'num_columns' in prepared statement metadata *after* execute should be: (Exp '3' == Actual: %d)", num_columns); + mysql_free_result(result); + + MYSQL_BIND bind[3]; + int data_id; + int64_t data_c1; + char data_c2[STRING_SIZE]; + char is_null[3]; + long unsigned int length[3]; + char error[3]; + memset(bind, 0, sizeof(bind)); + + bind[0].buffer_type = MYSQL_TYPE_LONG; + bind[0].buffer = (char *)&data_id; + bind[0].buffer_length = sizeof(int); + bind[0].is_null = &is_null[0]; + bind[0].length = &length[0]; + + bind[1].buffer_type = MYSQL_TYPE_LONGLONG; + bind[1].buffer = (char *)&data_c1; + bind[1].buffer_length = sizeof(int64_t); + bind[1].is_null = &is_null[1]; + bind[1].length = &length[1]; + + bind[2].buffer_type = MYSQL_TYPE_STRING; + bind[2].buffer = (char *)&data_c2; + bind[2].buffer_length = STRING_SIZE; + bind[2].is_null = &is_null[2]; + bind[2].length = &length[2]; + bind[2].error = &error[2]; + + if (mysql_stmt_bind_result(stmt, bind)) { + ok(false, "mysql_stmt_bind_result at line %d failed: %s\n", __LINE__ , mysql_stmt_error(stmt)); + return exit_status(); + } + + if (mysql_stmt_fetch(stmt)) { + ok(false, "mysql_stmt_fetch at line %d failed: %s\n", __LINE__ , mysql_stmt_error(stmt)); + return exit_status(); + } + + bool data_match_expected = + (data_id == static_cast(1)) && + (data_c1 == static_cast(100)) && + (strcmp(data_c2, "abcde") == 0); + + ok( + data_match_expected, + "Prepared statement result matches expected - Exp=(id:1, c1:100, c2:'abcde'), Act=(id:%d, c1:%d, c2:'%s')", + data_id, + data_c1, + data_c2 + ); + + mysql_stmt_close(stmt); + mysql_close(proxysql_mysql); + mysql_close(proxysql_admin); + + return exit_status(); +}