Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions include/query_processor.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
int log;
bool apply;
char* attributes;
char *destination_schema; // parsed from attributes JSON key "destination_schema"
char *comment; // #643
void *regex_engine1;
void *regex_engine2;
Expand Down Expand Up @@ -175,6 +176,7 @@
int log;
int firewall_whitelist_mode;
char *attributes;
char *destination_schema; // when set, session schema is switched before routing
char *comment; // #643

bool create_new_conn;
Expand Down Expand Up @@ -213,6 +215,7 @@
error_msg=NULL;
OK_msg=NULL;
attributes=NULL;
destination_schema=NULL;
comment=NULL; // #643
firewall_whitelist_mode = WUS_NOT_FOUND;
create_new_conn=0;
Expand All @@ -228,9 +231,15 @@
}
if (attributes) {
free(attributes);
attributes=NULL;
}
if (destination_schema) {
free(destination_schema);

Check failure on line 237 in include/query_processor.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "free".

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ9HIYZHZ9F0_slUNe_Y&open=AZ9HIYZHZ9F0_slUNe_Y&pullRequest=5925
destination_schema=NULL;
}
if (comment) { // #643
free(comment);
comment=NULL;
}
}
void get_info_json(nlohmann::json& j);
Expand Down
30 changes: 30 additions & 0 deletions lib/MySQL_Query_Processor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,32 @@ MySQL_Query_Processor_Output* MySQL_Query_Processor::process_query(MySQL_Session
return ret;
}

/**
* @brief Parses the optional "destination_schema" key out of a rule's attributes JSON.
* @details Shared by both new_query_rule() overloads. On any problem the rule is left
* with a NULL destination_schema (no schema switch) and the reason is logged: a
* non-string value is a configuration error, an empty string is a no-op worth warning
* about. Kept as a free function so the attributes parsing in the callers stays flat.
* @param newQR Rule being built; its destination_schema is set on success.
* @param j_attributes Already parsed attributes JSON of the rule.
*/
static void parse_rule_destination_schema(MySQL_Query_Processor_Rule_t* newQR, const nlohmann::json& j_attributes) {
const auto it = j_attributes.find("destination_schema");
if (it == j_attributes.end()) {
return;
}
if (it->type() != nlohmann::json::value_t::string) {
proxy_error("Failed to parse destination_schema in JSON on attributes for rule_id %d : %s\n", newQR->rule_id, it->dump().c_str());
return;
}
const std::string s = *it;
if (s.empty()) {
proxy_warning("destination_schema is empty in attributes for rule_id %d , ignoring it\n", newQR->rule_id);
return;
}
newQR->destination_schema = strdup(s.c_str());
}

MySQL_Query_Processor_Rule_t* MySQL_Query_Processor::new_query_rule(int rule_id, bool active, const char* username, const char* schemaname, int flagIN, const char* client_addr,
const char* proxy_addr, int proxy_port, const char* digest, const char* match_digest, const char* match_pattern, bool negate_match_pattern,
const char* re_modifiers, int flagOUT, const char* replace_pattern, int destination_hostgroup, int cache_ttl, int cache_empty_result,
Expand Down Expand Up @@ -752,6 +778,7 @@ MySQL_Query_Processor_Rule_t* MySQL_Query_Processor::new_query_rule(int rule_id,
newQR->gtid_from_hostgroup = gtid_from_hostgroup;
newQR->apply = apply;
newQR->attributes = (attributes ? strdup(attributes) : NULL);
newQR->destination_schema = NULL;
newQR->comment = (comment ? strdup(comment) : NULL); // see issue #643
newQR->regex_engine1 = NULL;
newQR->regex_engine2 = NULL;
Expand Down Expand Up @@ -821,6 +848,7 @@ MySQL_Query_Processor_Rule_t* MySQL_Query_Processor::new_query_rule(int rule_id,
proxy_error("Failed to parse flagOUTs attributes for rule_id %d : %s\n", newQR->rule_id, flagOUTs.dump().c_str());
}
}
parse_rule_destination_schema(newQR, j_attributes);
}
}
proxy_debug(PROXY_DEBUG_MYSQL_QUERY_PROCESSOR, 5, "Creating new rule in %p : rule_id:%d, active:%d, username=%s, schemaname=%s, flagIN:%d, %smatch_digest=\"%s\", %smatch_pattern=\"%s\", flagOUT:%d replace_pattern=\"%s\", destination_hostgroup:%d, apply:%d\n", newQR, newQR->rule_id, newQR->active, newQR->username, newQR->schemaname, newQR->flagIN, (newQR->negate_match_pattern ? "(!)" : ""), newQR->match_digest, (newQR->negate_match_pattern ? "(!)" : ""), newQR->match_pattern, newQR->flagOUT, newQR->replace_pattern, newQR->destination_hostgroup, newQR->apply);
Expand Down Expand Up @@ -888,6 +916,7 @@ MySQL_Query_Processor_Rule_t* MySQL_Query_Processor::new_query_rule(const MySQL_
newQR->gtid_from_hostgroup = mqr->gtid_from_hostgroup;
newQR->apply = mqr->apply;
newQR->attributes = (mqr->attributes ? strdup(mqr->attributes) : NULL);
newQR->destination_schema = NULL;
newQR->comment = (mqr->comment ? strdup(mqr->comment) : NULL); // see issue #643
newQR->regex_engine1 = NULL;
newQR->regex_engine2 = NULL;
Expand Down Expand Up @@ -957,6 +986,7 @@ MySQL_Query_Processor_Rule_t* MySQL_Query_Processor::new_query_rule(const MySQL_
proxy_error("Failed to parse flagOUTs attributes for rule_id %d : %s\n", newQR->rule_id, flagOUTs.dump().c_str());
}
}
parse_rule_destination_schema(newQR, j_attributes);
}
}
proxy_debug(PROXY_DEBUG_MYSQL_QUERY_PROCESSOR, 5, "Creating new rule in %p : rule_id:%d, active:%d, username=%s, schemaname=%s, flagIN:%d, %smatch_digest=\"%s\", %smatch_pattern=\"%s\", flagOUT:%d replace_pattern=\"%s\", destination_hostgroup:%d, apply:%d\n", newQR, newQR->rule_id, newQR->active, newQR->username, newQR->schemaname, newQR->flagIN, (newQR->negate_match_pattern ? "(!)" : ""), newQR->match_digest, (newQR->negate_match_pattern ? "(!)" : ""), newQR->match_pattern, newQR->flagOUT, newQR->replace_pattern, newQR->destination_hostgroup, newQR->apply);
Expand Down
8 changes: 8 additions & 0 deletions lib/MySQL_Session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7114,6 +7114,14 @@
return true;
}

if (qpo->destination_schema) {
// switch the session schema before the query cache lookup and backend
// connection selection: the cache key and the connection pool both use
// (username, schemaname), and the pool issues COM_INIT_DB on schema
// mismatch, so the query (cached or not) lands on this schema
client_myds->myconn->userinfo->set_schemaname(qpo->destination_schema, strlen(qpo->destination_schema));

Check warning on line 7122 in lib/MySQL_Session.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure use of "strlen" is safe here.

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ9HIYfzZ9F0_slUNe_Z&open=AZ9HIYfzZ9F0_slUNe_Z&pullRequest=5925
}

if (prepare_stmt_type & ps_type_execute_stmt) { // for prepared statement execute we exit here
reset_warning_hostgroup_flag_and_release_connection();
goto __exit_set_destination_hostgroup;
Expand Down Expand Up @@ -9224,7 +9232,7 @@
myds->wait_until=0;
myds->DSS=STATE_NOT_INITIALIZED;
if (mysql_thread___autocommit_false_not_reusable && myds->myconn->IsAutoCommit()==false) {
if (mysql_thread___reset_connection_algorithm == 2 && myds->myconn->healthy) {

Check failure on line 9235 in lib/MySQL_Session.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ_PIV_CXE61jbUvGRTl&open=AZ_PIV_CXE61jbUvGRTl&pullRequest=5925
create_new_session_and_reset_connection(myds);
} else {
myds->destroy_MySQL_Connection_From_Pool(true);
Expand Down
2 changes: 2 additions & 0 deletions lib/PgSQL_Query_Processor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ PgSQL_Query_Processor_Rule_t* PgSQL_Query_Processor::new_query_rule(int rule_id,
newQR->multiplex = multiplex;
newQR->apply = apply;
newQR->attributes = (attributes ? strdup(attributes) : NULL);
newQR->destination_schema = NULL; // not supported for PgSQL
newQR->comment = (comment ? strdup(comment) : NULL); // see issue #643
newQR->regex_engine1 = NULL;
newQR->regex_engine2 = NULL;
Expand Down Expand Up @@ -505,6 +506,7 @@ PgSQL_Query_Processor_Rule_t* PgSQL_Query_Processor::new_query_rule(const PgSQL_
newQR->multiplex = pqr->multiplex;
newQR->apply = pqr->apply;
newQR->attributes = (pqr->attributes ? strdup(pqr->attributes) : NULL);
newQR->destination_schema = NULL; // not supported for PgSQL
newQR->comment = (pqr->comment ? strdup(pqr->comment) : NULL); // see issue #643
newQR->regex_engine1 = NULL;
newQR->regex_engine2 = NULL;
Expand Down
12 changes: 11 additions & 1 deletion lib/Query_Processor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,8 @@
free(qr->OK_msg);
if (qr->attributes)
free(qr->attributes);
if (qr->destination_schema)
free(qr->destination_schema);

Check failure on line 394 in lib/Query_Processor.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "free".

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ9HIYhaZ9F0_slUNe_a&open=AZ9HIYhaZ9F0_slUNe_a&pullRequest=5925
if (qr->comment)
free(qr->comment);
if (qr->regex_engine1) {
Expand Down Expand Up @@ -2068,7 +2070,14 @@
// Note: negative hostgroup means this rule doesn't change
proxy_debug(PROXY_DEBUG_MYSQL_QUERY_PROCESSOR, 5, "query rule %d has set destination hostgroup: %d\n", qr->rule_id, qr->destination_hostgroup);
ret->destination_hostgroup=qr->destination_hostgroup;
}
}
if (qr->destination_schema) {
proxy_debug(PROXY_DEBUG_MYSQL_QUERY_PROCESSOR, 5, "query rule %d has set destination schema: %s\n", qr->rule_id, qr->destination_schema);
if (ret->destination_schema) {
free(ret->destination_schema);

Check failure on line 2077 in lib/Query_Processor.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "free".

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ9HIYhaZ9F0_slUNe_b&open=AZ9HIYhaZ9F0_slUNe_b&pullRequest=5925
}
ret->destination_schema=strdup(qr->destination_schema);
}
if constexpr (has_process_query_extended<QP_DERIVED>::value) {
(static_cast<QP_DERIVED*>(this))->process_query_extended(static_cast<TypeQPOutput*>(ret), static_cast<TypeQueryRule*>(qr));
}
Expand Down Expand Up @@ -2940,6 +2949,7 @@
j["cache_ttl"] = cache_ttl;
j["delay"] = delay;
j["destination_hostgroup"] = destination_hostgroup;
j["destination_schema"] = ( destination_schema ? destination_schema : "" );
j["firewall_whitelist_mode"] = firewall_whitelist_mode;
j["multiplex"] = multiplex;
j["timeout"] = timeout;
Expand Down
1 change: 1 addition & 0 deletions test/tap/groups/groups.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"monitor_health_unit-t" : [ "unit-tests-g1" ],
"multiple_prepared_statements-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql90-g1","mysql95-g1" ],
"mysql-fast_forward-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql90-g1","mysql95-g1" ],
"mysql-dest_schema_routing-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql90-g1","mysql95-g1" ],
"mysql-init_connect-1-t" : [ "legacy-g1","mariadb10-galera-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql93-g1","mysql93-gr-g1","mysql95-g1","mysql95-gr-g1" ],
"mysql-init_connect-2-t" : [ "legacy-g1","mariadb10-galera-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql93-g1","mysql93-gr-g1","mysql95-g1","mysql95-gr-g1" ],
"mysql-last_insert_id-t" : [ "legacy-g1","mariadb10-galera-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql93-g1","mysql93-gr-g1","mysql95-g1","mysql95-gr-g1" ],
Expand Down
219 changes: 219 additions & 0 deletions test/tap/tests/mysql-dest_schema_routing-t.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/**
* @file mysql-dest_schema_routing-t.cpp
* @brief E2E test for logical db routing via mysql_query_rules.attributes
* {"destination_schema": "..."}. A matching rule switches the session schema
* before backend connection selection, so queries land on the remapped
* schema regardless of the db requested by the client. Verifies all three
* schema-selection paths (handshake db, COM_INIT_DB, USE statement) and
* that removing the rule restores the original behavior.
*/

#include <unistd.h>
#include <string>

#include "mysql.h"
#include "command_line.h"
#include "tap.h"
#include "utils.h"

CommandLine cl;

const char* SRC_DB = "dsr_src";
const char* DST_DB = "dsr_dst";
const int RULE_ID = 2; // must sort before the infra read/write split rules (3,4)

#define MYSQL_QUERY_ON_ERR_CLEANUP(mysql, query) \
do { \
if (mysql_query(mysql, query)) { \
fprintf(stderr, "File %s, line %d, Error: %s (%s)\n", __FILE__, __LINE__, mysql_error(mysql), query); \
goto cleanup; \
} \
} while(0)

/**
* @brief Run a single-value query and return the value ("" on NULL/error).
*/
std::string fetch_single(MYSQL* mysql, const char* query) {
std::string result {};
if (mysql_query(mysql, query)) {
diag("Query failed: '%s' error: '%s'", query, mysql_error(mysql));
return result;
}
MYSQL_RES* res = mysql_store_result(mysql);
if (res) {
MYSQL_ROW row = mysql_fetch_row(res);
if (row && row[0]) {
result = row[0];
}
mysql_free_result(res);
}
return result;
}

/**
* @brief Open a fresh proxy connection with 'db' as the handshake schema.
*/
MYSQL* connect_proxy(const char* db) {
MYSQL* conn = mysql_init(NULL);
if (!mysql_real_connect(conn, cl.host, cl.username, cl.password, db, cl.port, NULL, 0)) {
diag("Failed to connect to proxy (db=%s): %s", db ? db : "NULL", mysql_error(conn));
mysql_close(conn);
return NULL;
}
return conn;
}

int main() {
plan(12);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make COM_INIT_DB and USE command failures fail the TAP test.

If mysql_select_db() fails, Line 142 still reads the DST_DB schema from the handshake remap. If USE fails, Line 150 also reads the prior remap. diag() does not fail an assertion, so both path checks can pass without executing their schema-reset command. Add one ok() assertion for each command, stop on failure, and increase the plan to 14.

Proposed fix
-	plan(12);
+	plan(14);
@@
-	if (mysql_select_db(conn, SRC_DB)) {
-		diag("mysql_select_db failed: %s", mysql_error(conn));
+	const bool init_db_succeeded = mysql_select_db(conn, SRC_DB) == 0;
+	ok(init_db_succeeded, "COM_INIT_DB should succeed");
+	if (!init_db_succeeded) {
+		goto cleanup;
 	}
@@
-	if (mysql_query(conn, query)) {
-		diag("USE failed: %s", mysql_error(conn));
+	const bool use_succeeded = mysql_query(conn, query) == 0;
+	ok(use_succeeded, "USE %s should succeed", SRC_DB);
+	if (!use_succeeded) {
+		goto cleanup;
 	}

Also applies to: 139-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/tap/tests/mysql-dest_schema_routing-t.cpp` at line 67, Update the TAP
test plan from 12 to 14 and add an ok() assertion immediately after each
mysql_select_db() command covering the COM_INIT_DB and USE paths. Stop or return
from the test when either assertion fails, ensuring the subsequent
schema-routing checks cannot pass without executing the corresponding schema
reset.


if (cl.getEnv())
return exit_status();

MYSQL* admin = mysql_init(NULL);
MYSQL* setup = NULL;
MYSQL* conn = NULL;
std::string val {};
char query[512];

if (!mysql_real_connect(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(admin));
return -1;
}

// setup: create the two schemas with distinct markers, before any rule exists
setup = connect_proxy(NULL);
if (!setup) {
goto cleanup;
}
snprintf(query, sizeof(query), "CREATE DATABASE IF NOT EXISTS %s", SRC_DB);
MYSQL_QUERY_ON_ERR_CLEANUP(setup, query);
snprintf(query, sizeof(query), "CREATE DATABASE IF NOT EXISTS %s", DST_DB);
MYSQL_QUERY_ON_ERR_CLEANUP(setup, query);
snprintf(query, sizeof(query), "CREATE TABLE IF NOT EXISTS %s.marker (v VARCHAR(32))", SRC_DB);
MYSQL_QUERY_ON_ERR_CLEANUP(setup, query);
snprintf(query, sizeof(query), "CREATE TABLE IF NOT EXISTS %s.marker (v VARCHAR(32))", DST_DB);
MYSQL_QUERY_ON_ERR_CLEANUP(setup, query);
snprintf(query, sizeof(query), "DELETE FROM %s.marker", SRC_DB);
MYSQL_QUERY_ON_ERR_CLEANUP(setup, query);
snprintf(query, sizeof(query), "INSERT INTO %s.marker VALUES ('in_src')", SRC_DB);
MYSQL_QUERY_ON_ERR_CLEANUP(setup, query);
snprintf(query, sizeof(query), "DELETE FROM %s.marker", DST_DB);
MYSQL_QUERY_ON_ERR_CLEANUP(setup, query);
snprintf(query, sizeof(query), "INSERT INTO %s.marker VALUES ('in_dst')", DST_DB);
MYSQL_QUERY_ON_ERR_CLEANUP(setup, query);
// let replicas catch up: reads may be routed to a reader hostgroup
sleep(2);
Comment on lines +104 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wait for reader visibility instead of using a fixed delay.

The next reads can route to a reader hostgroup. A two-second delay does not prove that the replica applied the inserts. Poll the marker through a proxy connection with a bounded timeout before the baseline assertions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/tap/tests/mysql-dest_schema_routing-t.cpp` around lines 104 - 105,
Replace the fixed sleep(2) in the schema-routing test with bounded polling
through a proxy connection, repeatedly checking for the inserted marker to
become visible on the reader hostgroup. Continue to the baseline assertions only
after visibility is confirmed, and fail clearly when the timeout expires.


// baseline: no rule, handshake db is honored
conn = connect_proxy(SRC_DB);
if (!conn) {
goto cleanup;
}
val = fetch_single(conn, "SELECT DATABASE()");
ok(val == SRC_DB, "baseline: DATABASE() should be '%s', got '%s'", SRC_DB, val.c_str());
val = fetch_single(conn, "SELECT v FROM marker");
ok(val == "in_src", "baseline: marker should be 'in_src', got '%s'", val.c_str());
mysql_close(conn);
conn = NULL;

// add destination_schema rule for this user (apply=0: compose with later rules)
snprintf(query, sizeof(query), "DELETE FROM mysql_query_rules WHERE rule_id=%d", RULE_ID);
MYSQL_QUERY_ON_ERR_CLEANUP(admin, query);
snprintf(query, sizeof(query),
"INSERT INTO mysql_query_rules (rule_id, active, username, apply, attributes) "
"VALUES (%d, 1, '%s', 0, '{\"destination_schema\": \"%s\"}')", RULE_ID, cl.username, DST_DB);
MYSQL_QUERY_ON_ERR_CLEANUP(admin, query);
MYSQL_QUERY_ON_ERR_CLEANUP(admin, "LOAD MYSQL QUERY RULES TO RUNTIME");

// path 1: handshake db
conn = connect_proxy(SRC_DB);
if (!conn) {
goto cleanup;
}
val = fetch_single(conn, "SELECT DATABASE()");
ok(val == DST_DB, "handshake path: DATABASE() should be remapped to '%s', got '%s'", DST_DB, val.c_str());
val = fetch_single(conn, "SELECT v FROM marker");
ok(val == "in_dst", "handshake path: marker should be 'in_dst', got '%s'", val.c_str());

// path 2: COM_INIT_DB resets the schema, next query remaps again
if (mysql_select_db(conn, SRC_DB)) {
diag("mysql_select_db failed: %s", mysql_error(conn));
}
val = fetch_single(conn, "SELECT DATABASE()");
ok(val == DST_DB, "COM_INIT_DB path: DATABASE() should be remapped to '%s', got '%s'", DST_DB, val.c_str());

// path 3: USE statement resets the schema, next query remaps again
snprintf(query, sizeof(query), "USE %s", SRC_DB);
if (mysql_query(conn, query)) {
diag("USE failed: %s", mysql_error(conn));
}
val = fetch_single(conn, "SELECT DATABASE()");
ok(val == DST_DB, "USE path: DATABASE() should be remapped to '%s', got '%s'", DST_DB, val.c_str());
mysql_close(conn);
conn = NULL;

// query cache interaction: the schema switch must happen before the cache
// lookup, so cache keys use the remapped schema and a cache HIT still
// leaves the session on the remapped schema
snprintf(query, sizeof(query),
"UPDATE mysql_query_rules SET cache_ttl=60000 WHERE rule_id=%d", RULE_ID);
MYSQL_QUERY_ON_ERR_CLEANUP(admin, query);
MYSQL_QUERY_ON_ERR_CLEANUP(admin, "LOAD MYSQL QUERY RULES TO RUNTIME");

conn = connect_proxy(SRC_DB);
if (!conn) {
goto cleanup;
}
val = fetch_single(conn, "SELECT v FROM marker");
ok(val == "in_dst", "cache path: first (cache-miss) marker should be 'in_dst', got '%s'", val.c_str());
{
std::string hits_before = fetch_single(admin,
"SELECT variable_value FROM stats_mysql_global WHERE variable_name='Query_Cache_count_GET_OK'");
val = fetch_single(conn, "SELECT v FROM marker");
ok(val == "in_dst", "cache path: second (cache-hit) marker should be 'in_dst', got '%s'", val.c_str());
std::string hits_after = fetch_single(admin,
"SELECT variable_value FROM stats_mysql_global WHERE variable_name='Query_Cache_count_GET_OK'");
ok(atoll(hits_after.c_str()) > atoll(hits_before.c_str()),
"cache path: Query_Cache_count_GET_OK should increase (before=%s, after=%s)",
hits_before.c_str(), hits_after.c_str());
}
// even after a cache hit the session must stay on the remapped schema
val = fetch_single(conn, "SELECT DATABASE()");
ok(val == DST_DB, "cache path: DATABASE() after cache hit should be '%s', got '%s'", DST_DB, val.c_str());
mysql_close(conn);
conn = NULL;

// remove the rule: behavior must revert
snprintf(query, sizeof(query), "DELETE FROM mysql_query_rules WHERE rule_id=%d", RULE_ID);
MYSQL_QUERY_ON_ERR_CLEANUP(admin, query);
MYSQL_QUERY_ON_ERR_CLEANUP(admin, "LOAD MYSQL QUERY RULES TO RUNTIME");

conn = connect_proxy(SRC_DB);
if (!conn) {
goto cleanup;
}
val = fetch_single(conn, "SELECT DATABASE()");
ok(val == SRC_DB, "after rule removal: DATABASE() should be '%s', got '%s'", SRC_DB, val.c_str());
val = fetch_single(conn, "SELECT v FROM marker");
ok(val == "in_src", "after rule removal: marker should be 'in_src', got '%s'", val.c_str());
mysql_close(conn);
conn = NULL;

cleanup:
if (conn) {
mysql_close(conn);
}
if (setup) {
snprintf(query, sizeof(query), "DROP DATABASE IF EXISTS %s", SRC_DB);
mysql_query(setup, query);
snprintf(query, sizeof(query), "DROP DATABASE IF EXISTS %s", DST_DB);
mysql_query(setup, query);
mysql_close(setup);
}
snprintf(query, sizeof(query), "DELETE FROM mysql_query_rules WHERE rule_id=%d", RULE_ID);
mysql_query(admin, query);
mysql_query(admin, "LOAD MYSQL QUERY RULES TO RUNTIME");
mysql_close(admin);

return exit_status();
}