diff --git a/test/tap/tests/mysql-select_version_without_backend-t.cpp b/test/tap/tests/mysql-select_version_without_backend-t.cpp index a7b1994a2..5decf433d 100644 --- a/test/tap/tests/mysql-select_version_without_backend-t.cpp +++ b/test/tap/tests/mysql-select_version_without_backend-t.cpp @@ -1,6 +1,76 @@ /** * @file mysql-select_version_without_backend-t.cpp - * @brief This TAP test validates if SELECT VERSION() works without making calls to the backend. + * @brief TAP test for validating SELECT VERSION() behavior with mysql-select_version_forwarding + * + * ## Overview + * + * This test validates the behavior of the `mysql-select_version_forwarding` variable + * when ProxySQL has NO backend servers configured. This scenario tests how ProxySQL + * handles SELECT VERSION() and SELECT @@VERSION queries when there are no available + * backend connections to peek at. + * + * ## Background + * + * Since ProxySQL 3.0.4, SELECT VERSION() queries are intercepted by ProxySQL. + * The `mysql-select_version_forwarding` variable controls this behavior with 4 modes: + * + * - Mode 0 (NEVER): Always return ProxySQL's own mysql-server_version + * - Mode 1 (ALWAYS): Always proxy the query to a backend server + * - Mode 2 (SMART_FALLBACK_INTERNAL): Try to get version from backend connection, + * fallback to ProxySQL's mysql-server_version if no connection available + * - Mode 3 (SMART_FALLBACK_PROXY, default): Try to get version from backend connection, + * fallback to proxying the query if no connection available + * + * ## Test Scenarios + * + * This test runs TWO scenarios with NO backend servers configured: + * + * ### Scenario 1: Mode 3 (smart with fallback to proxying) - EXPECTED TO FAIL + * + * When `mysql-select_version_forwarding=3` and there are NO backend servers: + * - ProxySQL tries to peek at backend connections to get the version + * - No connections exist (no backends configured) + * - Fallback behavior: ProxySQL attempts to proxy the query to a backend + * - Result: Query FAILS because there is no backend to proxy to + * + * This test explicitly verifies that mode 3 behaves correctly when there are no + * backends - it should attempt to proxy and fail, rather than returning an internal + * version incorrectly. + * + * ### Scenario 2: Mode 2 (smart with fallback to internal) - EXPECTED TO SUCCEED + * + * When `mysql-select_version_forwarding=2` and there are NO backend servers: + * - ProxySQL tries to peek at backend connections to get the version + * - No connections exist (no backends configured) + * - Fallback behavior: Return ProxySQL's mysql-server_version (internal version) + * - Result: Query SUCCEEDS and returns the configured mysql-server_version + * + * This test verifies that mode 2 provides a safe fallback that allows queries + * to succeed even when no backends are available. + * + * ## Test Execution Flow + * + * For each scenario: + * 1. Set `mysql-select_version_forwarding` to the desired mode (3 or 2) + * 2. Set `mysql-server_version` to a known test value (e.g., "8.4.6") + * 3. Delete all backend servers (ensure no backends exist) + * 4. Execute two queries: + * - "SELECT @@VERSION" - Returns ProxySQL's mysql-server_version + * - "SELECT VERSION()" - Behavior depends on mode + * 5. Verify the results match the expected behavior for the mode + * + * ## Why Both Scenarios Matter + * + * - Mode 3 is the DEFAULT and provides the most accurate version information + * by ensuring clients get the real backend version or nothing. This is + * important for clients like SQLAlchemy that need to detect MariaDB vs MySQL. + * + * - Mode 2 provides a safer fallback that maintains availability at the cost + * of potentially returning less accurate version information when no backends + * are available. + * + * Testing both ensures the feature works correctly in all configurations and + * prevents regressions in future versions. */ #include @@ -37,32 +107,30 @@ int run_q(MYSQL *mysql, const char *q) { return 0; } -int main(int argc, char** argv) { - plan(2); - - CommandLine cl; - if (cl.getEnv()) { - diag("Failed to get the required environmental variables."); - return exit_status(); - } - - MYSQL* admin = init_mysql_conn(cl.host, cl.admin_username, cl.admin_password, cl.admin_port); - if (!admin) { - fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin)); - return exit_status(); - } - - MYSQL_QUERY_T(admin, MYSQL_SET_SERVER_VERSION_QUERY); +/** + * @brief Test SELECT VERSION() behavior with a specific mode + * + * @param admin Admin connection for configuration + * @param proxy Proxy connection for queries + * @param mode The mysql-select_version_forwarding mode to test + * @param expect_success Whether queries are expected to succeed + * @return int 0 on success, EXIT_FAILURE on error + */ +int test_mode(MYSQL* admin, MYSQL* proxy, int mode, bool expect_success) { + // Set the mode explicitly + char set_mode_query[128]; + snprintf(set_mode_query, sizeof(set_mode_query), + "SET mysql-select_version_forwarding=%d", mode); + MYSQL_QUERY_T(admin, set_mode_query); MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + // Ensure no backends exist MYSQL_QUERY_T(admin, "DELETE FROM mysql_servers"); MYSQL_QUERY_T(admin, "LOAD MYSQL SERVERS TO RUNTIME"); - MYSQL* proxy = init_mysql_conn(cl.host, cl.username, cl.password, cl.port); - if (!proxy) { - fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); - return exit_status(); - } + diag("=== Testing mode %d (expect %s) ===", + mode, + expect_success ? "SUCCESS" : "FAILURE"); const char *version_get_queries[2] = { "SELECT @@VERSION", @@ -73,8 +141,11 @@ int main(int argc, char** argv) { MYSQL_ROW row = nullptr; string res_server_version; + // Attempt to run the query int rc = run_q(proxy, version_get_queries[i]); + if (rc == 0) { + // Query succeeded - fetch result MYSQL_RES* proxy_res = mysql_store_result(proxy); row = mysql_fetch_row(proxy_res); @@ -85,9 +156,65 @@ int main(int argc, char** argv) { mysql_free_result(proxy_res); } - ok(row && (res_server_version == MYSQL_TEST_SERVER_VERSION), "Server version: %s", res_server_version.c_str()); + // Verify result matches expected behavior + if (expect_success) { + // Mode 2: Query should succeed and return ProxySQL's version + bool test_passed = row && (res_server_version == MYSQL_TEST_SERVER_VERSION); + ok(test_passed, + "Mode %d: %s should return '%s' - got '%s'", + mode, + version_get_queries[i], + MYSQL_TEST_SERVER_VERSION, + res_server_version.c_str()); + } else { + // Mode 3: Query should FAIL (no backend to proxy to) + bool test_passed = (row == nullptr); + ok(test_passed, + "Mode %d: %s should FAIL (no backend) - %s", + mode, + version_get_queries[i], + row ? "UNEXPECTED SUCCESS" : "correctly failed"); + } + } + + return 0; +} + +int main(int argc, char** argv) { + // We have 2 queries × 2 modes = 4 tests total + plan(4); + + CommandLine cl; + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return exit_status(); + } + + MYSQL* admin = init_mysql_conn(cl.host, cl.admin_username, cl.admin_password, cl.admin_port); + if (!admin) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin)); + return exit_status(); } + // Set the ProxySQL version that will be returned for internal fallback + MYSQL_QUERY_T(admin, MYSQL_SET_SERVER_VERSION_QUERY); + MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + + MYSQL* proxy = init_mysql_conn(cl.host, cl.username, cl.password, cl.port); + if (!proxy) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); + mysql_close(admin); + return exit_status(); + } + + // Scenario 1: Mode 3 (smart with fallback to proxying) - EXPECTED TO FAIL + // With no backends, mode 3 will try to proxy the query and fail + test_mode(admin, proxy, 3, false); + + // Scenario 2: Mode 2 (smart with fallback to internal) - EXPECTED TO SUCCEED + // With no backends, mode 2 will fall back to returning mysql-server_version + test_mode(admin, proxy, 2, true); + mysql_close(admin); mysql_close(proxy);