From 366164ab26a53270162e9b088eb1a859ce06500b Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 6 Jan 2026 11:37:20 +0000 Subject: [PATCH 1/3] Fix #5272: Add mysql-select_version_forwarding variable for SELECT VERSION() Since ProxySQL 3.0.4, SELECT VERSION() queries were intercepted and returned ProxySQL's mysql-server_version variable instead of proxying to backends. This broke SQLAlchemy for MariaDB which expects "MariaDB" in the version string. This commit adds a new variable `mysql-select_version_forwarding` with 4 modes: - 0 = never: Always return ProxySQL's version (3.0.4+ behavior) - 1 = always: Always proxy to backend (3.0.3 behavior) - 2 = smart (fallback to 0): Try backend connection, else ProxySQL version - 3 = smart (fallback to 1): Try backend connection, else proxy (default) The implementation includes: - New global variable mysql_thread___select_version_forwarding - New function get_backend_version_for_hostgroup() to peek at backend connection versions without removing them from the pool - Modified SELECT VERSION() handler to support all 4 modes - ProxySQL backend detection to avoid recursion Mode 3 (default) ensures SQLAlchemy always gets the real MariaDB version string while maintaining fast response when connections are available. --- include/MySQL_Session.h | 12 ++++ include/MySQL_Thread.h | 1 + include/proxysql_structs.h | 2 + lib/MySQL_Session.cpp | 109 ++++++++++++++++++++++++++++++++++++- lib/MySQL_Thread.cpp | 5 ++ 5 files changed, 127 insertions(+), 2 deletions(-) diff --git a/include/MySQL_Session.h b/include/MySQL_Session.h index 9d4d6fe39..dae82c6b0 100644 --- a/include/MySQL_Session.h +++ b/include/MySQL_Session.h @@ -473,6 +473,18 @@ class MySQL_Session: public Base_Sessionsize==SELECT_MYSQL_VERSION_LEN+5 && *((char *)(pkt->ptr)+4)==(char)0x03 && strncasecmp((char *)SELECT_MYSQL_VERSION,(char *)pkt->ptr+5,pkt->size-5)==0) || (pkt->size==SELECT_MYSQL_VERSION_FUNC_LEN+5 && *((char *)(pkt->ptr)+4)==(char)0x03 && strncasecmp((char *)SELECT_MYSQL_VERSION_FUNC,(char *)pkt->ptr+5,pkt->size-5)==0)) { + char *version_to_return = NULL; + int mode = mysql_thread___select_version_forwarding; // 0=never, 1=always, 2=smart(fallback to 0), 3=smart(fallback to 1) + + if (mode == 1) { + // always: proxy to backend, don't handle here + return false; + } + else if (mode == 2) { + // smart (fallback to 0): try to get version from backend connection, else use ProxySQL version + int target_hg = (current_hostgroup >= 0) ? current_hostgroup : default_hostgroup; + + if (target_hg >= 0) { + version_to_return = get_backend_version_for_hostgroup(target_hg); + + // Check if backend is ProxySQL (to avoid recursion) + if (version_to_return && strstr(version_to_return, "ProxySQL")) { + version_to_return = NULL; + } + } + + // Fallback to ProxySQL version if no backend version found + if (!version_to_return) { + version_to_return = mysql_thread___server_version; + } + } + else if (mode == 3) { + // smart (fallback to 1): try to get version from backend connection, else proxy to backend + int target_hg = (current_hostgroup >= 0) ? current_hostgroup : default_hostgroup; + + if (target_hg >= 0) { + version_to_return = get_backend_version_for_hostgroup(target_hg); + + // Check if backend is ProxySQL (to avoid recursion) + if (version_to_return && strstr(version_to_return, "ProxySQL")) { + version_to_return = NULL; + } + } + + // Fallback: if no backend version found, proxy to backend (mode 1 behavior) + if (!version_to_return) { + return false; + } + } + else { + // mode 0 (never): use ProxySQL's version + version_to_return = mysql_thread___server_version; + } + + // Generate response packet with version_to_return char buf2[32]; int l0=0; if (pkt->size == SELECT_MYSQL_VERSION_LEN+5) @@ -7085,8 +7134,8 @@ bool MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C } char **p=(char **)malloc(sizeof(char*)*1); unsigned long *l=(unsigned long *)malloc(sizeof(unsigned long *)*1); - l[0]= strlen(mysql_thread___server_version); - p[0]=mysql_thread___server_version; + l[0]= strlen(version_to_return); + p[0]=version_to_return; myprot->generate_pkt_row(true,NULL,NULL,sid,1,l,p); sid++; myds->DSS=STATE_ROW; if (deprecate_eof_active) { @@ -7163,6 +7212,62 @@ __exit_set_destination_hostgroup: return false; } +char * MySQL_Session::get_backend_version_for_hostgroup(int hostgroup_id) { + // Step 1: Validate input + if (hostgroup_id < 0) { + return NULL; + } + + // Step 2: Access the global MySQL_HostGroups_Manager + // MyHGM is the global instance + MySQL_HostGroups_Manager *myhgm = MyHGM; + if (!myhgm) { + return NULL; + } + + // Step 3: Lock for reading + // We only have wrlock(), no rdlock() - use wrlock() for safety + myhgm->wrlock(); + + // Step 4: Find the hostgroup + MyHGC *myhgc = myhgm->MyHGC_lookup(hostgroup_id); + if (!myhgc) { + // Hostgroup doesn't exist + myhgm->wrunlock(); + return NULL; + } + + // Step 5: Check if the hostgroup has any servers + if (!myhgc->mysrvs || !myhgc->mysrvs->servers || myhgc->mysrvs->servers->len == 0) { + myhgm->wrunlock(); + return NULL; + } + + // Step 6: Iterate through servers in the hostgroup + for (unsigned int i = 0; i < myhgc->mysrvs->servers->len; i++) { + MySrvC *mysrvc = myhgc->mysrvs->idx(i); + if (!mysrvc) { + continue; + } + + // Step 7: Check if this server has any free connections + if (mysrvc->ConnectionsFree && mysrvc->ConnectionsFree->conns_length() > 0) { + // Step 8: Peek at the first free connection WITHOUT removing it + // ConnectionsFree::index() only returns a pointer, doesn't remove from array + MySQL_Connection *myconn = mysrvc->ConnectionsFree->index(0); + if (myconn && myconn->options.server_version) { + // Found a connection with a version string + myhgm->wrunlock(); + return myconn->options.server_version; + } + } + } + + // Step 9: No free connections found in any server of this hostgroup + myhgm->wrunlock(); + return NULL; +} + void MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_COM_STATISTICS(PtrSize_t *pkt) { proxy_debug(PROXY_DEBUG_MYSQL_COM, 5, "Got COM_STATISTICS packet\n"); l_free(pkt->size,pkt->ptr); diff --git a/lib/MySQL_Thread.cpp b/lib/MySQL_Thread.cpp index f8191ba9e..20b157144 100644 --- a/lib/MySQL_Thread.cpp +++ b/lib/MySQL_Thread.cpp @@ -466,6 +466,7 @@ static char * mysql_thread_variables_names[]= { (char *)"poll_timeout_on_failure", (char *)"server_capabilities", (char *)"server_version", + (char *)"select_version_forwarding", (char *)"keep_multiplexing_variables", (char *)"default_authentication_plugin", (char *)"kill_backend_connection_when_disconnect", @@ -1106,6 +1107,7 @@ MySQL_Threads_Handler::MySQL_Threads_Handler() { variables.handle_unknown_charset=1; variables.interfaces=strdup((char *)""); variables.server_version=strdup((char *)"8.0.11"); // changed in 2.6.0 , was 5.5.30 + variables.select_version_forwarding=3; // 0=never, 1=always, 2=smart(fallback to 0), 3=smart(fallback to 1, default) variables.eventslog_filename=strdup((char *)""); // proxysql-mysql-eventslog is recommended variables.eventslog_filesize=100*1024*1024; variables.eventslog_buffer_history_size=0; @@ -2357,6 +2359,7 @@ char ** MySQL_Threads_Handler::get_variables_list() { VariablesPointers_int["binlog_reader_connect_retry_msec"] = make_tuple(&variables.binlog_reader_connect_retry_msec, 0, 0, true); VariablesPointers_int["eventslog_format"] = make_tuple(&variables.eventslog_format, 0, 0, true); VariablesPointers_int["wait_timeout"] = make_tuple(&variables.wait_timeout, 0, 0, true); + VariablesPointers_int["select_version_forwarding"] = make_tuple(&variables.select_version_forwarding, 0, 3, false); VariablesPointers_int["data_packets_history_size"] = make_tuple(&variables.data_packets_history_size, 0, 0, true); } @@ -4182,6 +4185,7 @@ void MySQL_Thread::refresh_variables() { REFRESH_VARIABLE_INT(connect_timeout_server_max); REFRESH_VARIABLE_INT(free_connections_pct); REFRESH_VARIABLE_INT(fast_forward_grace_close_ms); + REFRESH_VARIABLE_INT(select_version_forwarding); #ifdef IDLE_THREADS REFRESH_VARIABLE_INT(session_idle_ms); #endif // IDLE_THREADS @@ -4349,6 +4353,7 @@ MySQL_Thread::MySQL_Thread() { last_processing_idles=0; __thread_MySQL_Thread_Variables_version=0; mysql_thread___server_version=NULL; + mysql_thread___select_version_forwarding=3; // default: smart (fallback to 1) mysql_thread___init_connect=NULL; mysql_thread___ldap_user_variable=NULL; mysql_thread___add_ldap_user_comment=NULL; From fc73ec1c50e14c020e5a51f7b69ffe2a3f39fa3b Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Wed, 7 Jan 2026 04:52:57 +0000 Subject: [PATCH 2/3] Code review improvements: Add enum and refactor SELECT VERSION() handling - Add SelectVersionForwardingMode enum to replace magic numbers (0,1,2,3) - Refactor modes 2 and 3 to eliminate code duplication - Improve code readability and maintainability Addresses feedback from gemini-code-assist on PR #5277 --- include/MySQL_Session.h | 16 ++++++++++++++++ lib/MySQL_Session.cpp | 36 ++++++++++++------------------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/include/MySQL_Session.h b/include/MySQL_Session.h index dae82c6b0..45c6231f4 100644 --- a/include/MySQL_Session.h +++ b/include/MySQL_Session.h @@ -49,6 +49,22 @@ enum ps_type : uint8_t { ps_type_execute_stmt = 0x2 }; +/** + * @enum SelectVersionForwardingMode + * @brief Defines modes for handling SELECT VERSION() queries in ProxySQL. + * + * These modes control how ProxySQL responds to SELECT VERSION() queries: + * - NEVER: Always return ProxySQL's own version + * - ALWAYS: Always proxy the query to a backend server + * - SMART_FALLBACK_INTERNAL: Try to get version from backend connection, fallback to ProxySQL version + * - SMART_FALLBACK_PROXY: Try to get version from backend connection, fallback to proxying the query + */ +enum SelectVersionForwardingMode : uint8_t { + SELECT_VERSION_NEVER = 0, + SELECT_VERSION_ALWAYS = 1, + SELECT_VERSION_SMART_FALLBACK_INTERNAL = 2, + SELECT_VERSION_SMART_FALLBACK_PROXY = 3 +}; //std::string proxysql_session_type_str(enum proxysql_session_type session_type); diff --git a/lib/MySQL_Session.cpp b/lib/MySQL_Session.cpp index ff1cd865e..6e4bc4cd5 100644 --- a/lib/MySQL_Session.cpp +++ b/lib/MySQL_Session.cpp @@ -7061,14 +7061,14 @@ bool MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C if ((pkt->size==SELECT_MYSQL_VERSION_LEN+5 && *((char *)(pkt->ptr)+4)==(char)0x03 && strncasecmp((char *)SELECT_MYSQL_VERSION,(char *)pkt->ptr+5,pkt->size-5)==0) || (pkt->size==SELECT_MYSQL_VERSION_FUNC_LEN+5 && *((char *)(pkt->ptr)+4)==(char)0x03 && strncasecmp((char *)SELECT_MYSQL_VERSION_FUNC,(char *)pkt->ptr+5,pkt->size-5)==0)) { char *version_to_return = NULL; - int mode = mysql_thread___select_version_forwarding; // 0=never, 1=always, 2=smart(fallback to 0), 3=smart(fallback to 1) + int mode = mysql_thread___select_version_forwarding; // SelectVersionForwardingMode enum values - if (mode == 1) { + if (mode == SELECT_VERSION_ALWAYS) { // always: proxy to backend, don't handle here return false; } - else if (mode == 2) { - // smart (fallback to 0): try to get version from backend connection, else use ProxySQL version + else if (mode == SELECT_VERSION_SMART_FALLBACK_INTERNAL || mode == SELECT_VERSION_SMART_FALLBACK_PROXY) { + // smart modes: try to get version from backend connection, then fallback based on mode int target_hg = (current_hostgroup >= 0) ? current_hostgroup : default_hostgroup; if (target_hg >= 0) { @@ -7080,31 +7080,19 @@ bool MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C } } - // Fallback to ProxySQL version if no backend version found + // Fallback behavior depends on mode if (!version_to_return) { - version_to_return = mysql_thread___server_version; - } - } - else if (mode == 3) { - // smart (fallback to 1): try to get version from backend connection, else proxy to backend - int target_hg = (current_hostgroup >= 0) ? current_hostgroup : default_hostgroup; - - if (target_hg >= 0) { - version_to_return = get_backend_version_for_hostgroup(target_hg); - - // Check if backend is ProxySQL (to avoid recursion) - if (version_to_return && strstr(version_to_return, "ProxySQL")) { - version_to_return = NULL; + if (mode == SELECT_VERSION_SMART_FALLBACK_INTERNAL) { + // fallback to internal (ProxySQL) version + version_to_return = mysql_thread___server_version; + } else { + // SELECT_VERSION_SMART_FALLBACK_PROXY: fallback to proxying the query + return false; } } - - // Fallback: if no backend version found, proxy to backend (mode 1 behavior) - if (!version_to_return) { - return false; - } } else { - // mode 0 (never): use ProxySQL's version + // SELECT_VERSION_NEVER (mode 0): use ProxySQL's version version_to_return = mysql_thread___server_version; } From acf2e28905d9f54d54fb65ff2398365b2d80f26d Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Wed, 7 Jan 2026 05:14:48 +0000 Subject: [PATCH 3/3] Fix TAP test for mysql-select_version_forwarding with default mode 3 The test was failing because the default mode 3 (smart with fallback to proxying) tries to proxy the query when no backends exist, causing the query to fail. Changes: - Split test into two scenarios: 1. Mode 3 (explicitly set): Expected to FAIL with no backends 2. Mode 2: Expected to SUCCEED with no backends (fallback to internal) - Added extensive documentation explaining: * All 4 modes of mysql-select_version_forwarding * Why mode 3 fails with no backends (correct behavior) * Why mode 2 succeeds with no backends (fallback to internal version) * Test execution flow and why both scenarios matter - Refactored test into test_mode() helper function for clarity --- ...mysql-select_version_without_backend-t.cpp | 173 +++++++++++++++--- 1 file changed, 150 insertions(+), 23 deletions(-) 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);