Repository: sysown/proxysql (language: cpp)

Issue:
Query routing working incorrectly in a transaction context in case of query errors causing a connection leak
Problem: We're noticing that query rules don't seem to act correctly when used in conjunction with query rules, specifically around error cases.

Test case: Config mainly utilizes a query routing rules that looks for a query comment, something like `/* afrm_hint_batch_only */` and then routes those to a separate hostgroup. Note that for this Issue, I'm executing queries for this test case using mysql-client, not through a driver. We see similar problems when executing through a python driver however.

```

--- Control test case

mysql> select * from stats_mysql_connection_pool where hostgroup LIKE '101%';
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| hostgroup | srv_host                                                             | srv_port | status | ConnUsed | ConnFree | ConnOK | ConnERR | MaxConnUsed | Queries | Queries_GTID_sync | Bytes_data_sent | Bytes_data_recv | Latency_us |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| 1010      | affirm1<> | 3306     | ONLINE | 0        | 1        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 594        |
| 1011      | affirm1<> | 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 913        |
| 1012      | affirm1<> | 3306     | ONLINE | 0        | 1        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 594        |
| 1013      | affirm1<> | 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 913        |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
4 rows in set (0.00 sec)

mysql> set autocommit=off;
Query OK, 0 rows affected (0.01 sec)

mysql> /* afrm_hint_batch_only */ insert into sraju_test values(30, 'happy');
Query OK, 1 row affected (0.00 sec)

mysql> select * from stats_mysql_connection_pool where hostgroup LIKE '101%';
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| hostgroup | srv_host                                                             | srv_port | status | ConnUsed | ConnFree | ConnOK | ConnERR | MaxConnUsed | Queries | Queries_GTID_sync | Bytes_data_sent | Bytes_data_recv | Latency_us |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| 1010      | affirm1<>  | 3306     | ONLINE | 0        | 1        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 568        |
| 1011      | affirm1<>| 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 862        |
| 1012      | affirm1<> | 3306     | ONLINE | 1        | 0        | 0      | 0       | 1           | 2       | 0                 | 469             | 0               | 568        |
| 1013      | affirm1<>| 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 862        |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
4 rows in set (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from stats_mysql_connection_pool where hostgroup LIKE '101%';
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| hostgroup | srv_host                                                             | srv_port | status | ConnUsed | ConnFree | ConnOK | ConnERR | MaxConnUsed | Queries | Queries_GTID_sync | Bytes_data_sent | Bytes_data_recv | Latency_us |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| 1010      | affirm1<> | 3306     | ONLINE | 0        | 1        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 538        |
| 1011      | affirm1<> | 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 866        |
| 1012      | affirm1<>| 3306     | ONLINE | 0        | 1        | 0      | 0       | 1           | 3       | 0                 | 485             | 0               | 538        |
| 1013      | affirm1<> | 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 866        |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
4 rows in set (0.00 sec)

--- Problematic case

mysql> /* afrm_hint_batch_only */ insert into sraju_test values(20, 'unhappy');
ERROR 1062 (23000): Duplicate entry '20' for key 'PRIMARY'

mysql> select * from stats_mysql_connection_pool where hostgroup LIKE '101%';
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| hostgroup | srv_host                                                             | srv_port | status | ConnUsed | ConnFree | ConnOK | ConnERR | MaxConnUsed | Queries | Queries_GTID_sync | Bytes_data_sent | Bytes_data_recv | Latency_us |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| 1010      | affirm1<>| 3306     | ONLINE | 0        | 1        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 563        |
| 1011      | affirm1<> | 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 874        |
| 1012      | affirm1<>| 3306     | ONLINE | 1        | 0        | 0      | 0       | 1           | 4       | 0                 | 556             | 0               | 563        |
| 1013      | affirm1> | 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 874        |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
4 rows in set (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from stats_mysql_connection_pool where hostgroup LIKE '101%';
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| hostgroup | srv_host                                                             | srv_port | status | ConnUsed | ConnFree | ConnOK | ConnERR | MaxConnUsed | Queries | Queries_GTID_sync | Bytes_data_sent | Bytes_data_recv | Latency_us |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
| 1010      | affirm1<> | 3306     | ONLINE | 0        | 1        | 0      | 0       | 1           | 1       | 0                 | 8               | 0               | 550        |
| 1011      | affirm1<> | 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 853        |
| 1012      | affirm1<>| 3306     | ONLINE | 1        | 0        | 0      | 0       | 1           | 4       | 0                 | 556             | 0               | 550        |
| 1013      | affirm1<> | 3306     | ONLINE | 0        | 0        | 0      | 0       | 0           | 0       | 0                 | 0               | 0               | 853        |
+-----------+----------------------------------------------------------------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+
4 rows in set (0.00 sec)
```

In the second case, you will note that the `ROLLBACK` query executed against the wrong hostgroup 1010 and kept the connection open and leaking in the 1012 (see ConnUsed stat), note that we have transaction_persistent on for this user. We expect the rollback to occur on the right hostgroup (as it does in the happy path) even though they don't have the query comment on. 

Platform:
```
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.5 LTS

Release:        18.04
Codename:       bionic
```
[proxysql-config.txt](https://github.com/sysown/proxysql/files/11777283/proxysql-config.txt)


@renecannao 


Discussion / hints:

Fix patch:
diff --git a/include/MySQL_Session.h b/include/MySQL_Session.h
index dda1304e36..c5a8023409 100644
--- a/include/MySQL_Session.h
+++ b/include/MySQL_Session.h
@@ -127,6 +127,16 @@ class MySQL_Session
 
 	void return_proxysql_internal(PtrSize_t *);
 	bool handler_special_queries(PtrSize_t *);
+	/**
+	 * @brief Handles 'COMMIT|ROLLBACK' commands.
+	 * @details Forwarding the packet is required when there are active transactions. Since we are limited to
+	 *  forwarding just one 'COMMIT|ROLLBACK', we work under the assumption that we only have one active
+	 *  transaction. If more transactions are simultaneously open for the session, more 'COMMIT|ROLLBACK'.
+	 *  commands are required to be issued by the client, so they could be forwarded to the corresponding
+	 *  backend connections.
+	 * @param The received packet to be handled.
+	 * @return 'true' if the packet is intercepted and never forwarded to the client, 'false' otherwise.
+	 */
 	bool handler_CommitRollback(PtrSize_t *);
 	bool handler_SetAutocommit(PtrSize_t *);
 	/**
@@ -329,6 +339,18 @@ class MySQL_Session
 	unsigned int NumActiveTransactions(bool check_savpoint=false);
 	bool HasOfflineBackends();
 	bool SetEventInOfflineBackends();
+	/**
+	 * @brief Finds one active transaction in the current backend connections.
+	 * @details Since only one connection is returned, if the session holds multiple backend connections with
+	 *  potential transactions, the priority is:
+	 *   1. Connections flagged with 'SERVER_STATUS_IN_TRANS', or 'autocommit=0' in combination with
+	 *      'autocommit_false_is_transaction'.
+	 *   2. Connections with 'autocommit=0' holding a 'SAVEPOINT'.
+	 *   3. Connections with 'unknown transaction status', e.g: connections with errors.
+	 * @param check_savepoint Used to also check for connections holding savepoints. See MySQL bug
+	 *  https://bugs.mysql.com/bug.php?id=107875.
+	 * @returns The hostgroup in which the connection was found, -1 in case no connection is found.
+	 */
 	int FindOneActiveTransaction(bool check_savepoint=false);
 	unsigned long long IdleTime();
 
diff --git a/include/mysql_connection.h b/include/mysql_connection.h
index dda947f2da..9fd9a66eba 100644
--- a/include/mysql_connection.h
+++ b/include/mysql_connection.h
@@ -209,16 +209,21 @@ class MySQL_Connection {
 	void process_rows_in_ASYNC_STMT_EXECUTE_STORE_RESULT_CONT(unsigned long long& processed_bytes);
 
 	void async_free_result();
-	bool IsActiveTransaction(); /* {
-		bool ret=false;
-			if (mysql) {
-				ret = (mysql->server_status & SERVER_STATUS_IN_TRANS);
-				if (ret == false && (mysql)->net.last_errno) {
-					ret = true;
-				}
-			}
-		return ret;
-	} */
+	/**
+	 * @brief Returns if the connection is **for sure**, known to be in an active transaction.
+	 * @details The function considers two things:
+	 *   1. If 'server_status' is flagged with 'SERVER_STATUS_IN_TRANS'.
+	 *   2. If the connection has 'autcommit=0' and 'autocommit_false_is_transaction' is set.
+	 * @return True if the connection is known to be in a transaction, or equivalent state.
+	 */
+	bool IsKnownActiveTransaction();
+	/**
+	 * @brief Returns if the connection is in a **potential transaction**.
+	 * @details This function is a more strict version of 'IsKnownActiveTransaction', which also considers
+	 *  connections which holds 'unknown_transaction_status' as potentially active transactions.
+	 * @return True if the connection is in potentially in an active transaction.
+	 */
+	bool IsActiveTransaction();
 	bool IsServerOffline();
 	bool IsAutoCommit();
 	bool AutocommitFalse_AndSavepoint();
diff --git a/lib/MySQL_Session.cpp b/lib/MySQL_Session.cpp
index 215cff61c1..86d2cba5ce 100644
--- a/lib/MySQL_Session.cpp
+++ b/lib/MySQL_Session.cpp
@@ -917,9 +917,16 @@ bool MySQL_Session::handler_CommitRollback(PtrSize_t *pkt) {
 	// in this part of the code (as at release 2.4.3) where we call
 	// NumActiveTransactions() with the check_savepoint flag .
 	// This to try to handle MySQL bug https://bugs.mysql.com/bug.php?id=107875
-	unsigned int nTrx=NumActiveTransactions(true);
-	if (nTrx) {
+	//
+	// Since we are limited to forwarding just one 'COMMIT|ROLLBACK', we work under the assumption that we
+	// only have one active transaction. Under this premise, we should execute this command under that
+	// specific connection, for that, we update 'current_hostgroup' with the first active transaction we are
+	// able to find. If more transactions are simultaneously open for the session, more 'COMMIT|ROLLBACK'
+	// commands are required to be issued by the client to continue ending transactions.
+	unsigned int hg = FindOneActiveTransaction(true);
+	if (hg != -1) {
 		// there is an active transaction, we must forward the request
+		current_hostgroup = hg;
 		return false;
 	} else {
 		// there is no active transaction, we will just reply OK
@@ -7497,8 +7504,10 @@ int MySQL_Session::FindOneActiveTransaction(bool check_savepoint) {
 		_mybe=(MySQL_Backend *)mybes->index(i);
 		if (_mybe->server_myds) {
 			if (_mybe->server_myds->myconn) {
-				if (_mybe->server_myds->myconn->IsActiveTransaction()) {
+				if (_mybe->server_myds->myconn->IsKnownActiveTransaction()) {
 					return (int)_mybe->server_myds->myconn->parent->myhgc->hid;
+				} else if (_mybe->server_myds->myconn->IsActiveTransaction()) {
+					ret = (int)_mybe->server_myds->myconn->parent->myhgc->hid;
 				} else {
 					// we use check_savepoint to check if we shouldn't ignore COMMIT or ROLLBACK due
 					// to MySQL bug https://bugs.mysql.com/bug.php?id=107875 related to
diff --git a/lib/mysql_connection.cpp b/lib/mysql_connection.cpp
index d3300bb2e9..d1aa96368c 100644
--- a/lib/mysql_connection.cpp
+++ b/lib/mysql_connection.cpp
@@ -2385,6 +2385,16 @@ bool MySQL_Connection::AutocommitFalse_AndSavepoint() {
 	return ret;
 }
 
+bool MySQL_Connection::IsKnownActiveTransaction() {
+	bool in_trx = mysql ? mysql->server_status & SERVER_STATUS_IN_TRANS : false;
+
+	if (in_trx == false) {
+		in_trx = mysql_thread___autocommit_false_is_transaction && (IsAutoCommit() == false);
+	}
+
+	return in_trx;
+}
+
 bool MySQL_Connection::IsActiveTransaction() {
 	bool ret=false;
 	if (mysql) {


Test patch:
diff --git a/test/tap/tap/utils.cpp b/test/tap/tap/utils.cpp
index 9a9afedb6e..2875f9a879 100644
--- a/test/tap/tap/utils.cpp
+++ b/test/tap/tap/utils.cpp
@@ -16,15 +16,21 @@
 #include "utils.h"
 
 #include <unistd.h>
+#include <utility>
 #include <sys/wait.h>
 #include <fcntl.h>
 #include <iostream>
 
 #include "proxysql_utils.h"
 
+using std::pair;
+using std::map;
 using std::string;
 using std::vector;
 
+using std::to_string;
+using nlohmann::json;
+
 std::size_t count_matches(const string& str, const string& substr) {
 	std::size_t result = 0;
 	std::size_t pos = 0;
@@ -1256,3 +1262,314 @@ sq3_res_t sqlite3_execute_stmt(sqlite3* db, const string& query) {
 
 	return res;
 }
+
+json fetch_internal_session(MYSQL* proxy) {
+	int rc = mysql_query_t(proxy, "PROXYSQL INTERNAL SESSION");
+
+	if (rc ) {
+		return json {};
+	} else {
+		MYSQL_RES* myres = mysql_store_result(proxy);
+		MYSQL_ROW row = mysql_fetch_row(myres);
+		json j_session = json::parse(row[0]);
+		mysql_free_result(myres);
+
+		return j_session;
+	}
+}
+
+struct cols_table_info_t {
+	vector<string> names;
+	vector<size_t> widths;
+};
+
+std::string dump_as_table(MYSQL_RES* result, const cols_table_info_t& cols_info) {
+	if (!result) { return {}; }
+
+	const vector<string>& cols_names { cols_info.names };
+	const vector<size_t>& cols_widths { cols_info.widths };
+
+	uint32_t num_fields = mysql_num_fields(result);
+	std::string table_str { "+" };
+
+	for (size_t width : cols_widths) {
+		table_str += std::string(width + 2, '-') + "+";
+	}
+	table_str += "\n";
+
+	table_str += "|";
+	for (size_t col = 0; col < num_fields; col++) {
+		table_str += " " + cols_names[col] + std::string(cols_widths[col] - cols_names[col].size(), ' ') + " |";
+	}
+	table_str += "\n";
+
+	table_str += "+";
+	for (size_t width : cols_widths) {
+		table_str += std::string(width + 2, '-') + "+";
+	}
+	table_str += "\n";
+
+	while (MYSQL_ROW row = mysql_fetch_row(result)) {
+		table_str += "|";
+		for (size_t col = 0; col < num_fields; col++) {
+			std::string value = row[col] ? row[col] : "";
+			table_str += " " + value + std::string(cols_widths[col] - value.size(), ' ') + " |";
+		}
+		table_str += "\n";
+	}
+
+	table_str += "+";
+	for (size_t width : cols_widths) {
+		table_str += std::string(width + 2, '-') + "+";
+	}
+	table_str += "\n";
+
+	mysql_data_seek(result, 0);
+
+	return table_str;
+}
+
+std::string dump_as_table(MYSQL_RES* result) {
+	if (!result) { return {}; }
+
+	uint32_t num_fields = mysql_num_fields(result);
+	MYSQL_FIELD* fields = mysql_fetch_fields(result);
+
+	vector<string> columns {};
+	for (uint32_t i = 0; i < num_fields; ++i) {
+		columns.push_back(fields[i].name);
+	}
+
+	vector<size_t> cols_widths(num_fields, 0);
+
+	for (int col = 0; col < num_fields; ++col) {
+		cols_widths[col] = std::max(cols_widths[col], columns[col].size());
+	}
+
+	while (MYSQL_ROW row = mysql_fetch_row(result)) {
+		for (uint32_t col = 0; col < num_fields; col++) {
+			if (row[col]) {
+				cols_widths[col] = std::max(cols_widths[col], strlen(row[col]));
+			}
+		}
+	}
+
+	mysql_data_seek(result, 0);
+	std::string res { dump_as_table(result, {columns, cols_widths}) };
+
+	return res;
+}
+
+pair<int,vector<mysql_row_t>> exec_dql_query(MYSQL* conn, const string& query, bool dump_res) {
+	if (mysql_query(conn, query.c_str())) {
+		diag("Failed to executed query `%s`", query.c_str());
+		return { EXIT_FAILURE, {} };
+	}
+
+	MYSQL_RES* my_stats_res = mysql_store_result(conn);
+	if (my_stats_res == nullptr) {
+		diag("Failed to retrieve a resultset, expected DQL query");
+
+		return { EXIT_FAILURE, {} };
+	} else {
+		if (dump_res) {
+			fprintf(stderr, "%s", dump_as_table(my_stats_res).c_str());
+		}
+
+		vector<mysql_row_t> my_rows { extract_mysql_rows(my_stats_res) };
+		mysql_free_result(my_stats_res);
+
+		return { EXIT_SUCCESS, my_rows };
+	}
+}
+
+string join(string delim, const vector<string>& words) {
+	return std::accumulate(
+		words.begin(), words.end(), string {},
+		[&delim] (const string& s1, const string& s2) {
+			if (s1.empty()) {
+				return s2;
+			} else {
+				return s1 + delim + s2;
+			}
+		}
+	);
+}
+
+string gen_conn_stats_query(const vector<uint32_t>& hgs) {
+	const auto _to_string = [] (uint32_t n) -> string { return to_string(n); };
+
+	vector<string> hgs_str {};
+	std::transform(hgs.begin(), hgs.end(), std::back_inserter(hgs_str), _to_string);
+
+	const string CONN_STATS_HGS { join(",", hgs_str) };
+	const string CONN_STATS_QUERY_T {
+		"SELECT hostgroup,ConnUsed,ConnFree,ConnOk,ConnERR,MaxConnUsed,Queries"
+			" FROM stats.stats_mysql_connection_pool"
+	};
+
+	if (hgs.empty()) {
+		return CONN_STATS_QUERY_T;
+	} else {
+		return CONN_STATS_QUERY_T + " WHERE hostgroup IN (" + CONN_STATS_HGS +  ")";
+	}
+}
+
+int dump_conn_stats(MYSQL* admin, const vector<uint32_t> hgs) {
+	const string query { gen_conn_stats_query(hgs) };
+	MYSQL_QUERY(admin, query.c_str());
+
+	MYSQL_RES* myres = mysql_store_result(admin);
+	const string table { dump_as_table(myres) };
+	mysql_free_result(myres);
+	fprintf(stderr, "%s", table.c_str());
+
+	return EXIT_SUCCESS;
+}
+
+pair<int,pool_state_t> fetch_conn_stats(MYSQL* admin, const vector<uint32_t> hgs) {
+	const string stats_query { gen_conn_stats_query(hgs) };
+	const pair<int,vector<mysql_row_t>> conn_pool_stats { exec_dql_query(admin, stats_query, true) };
+
+	if (conn_pool_stats.first || conn_pool_stats.second.size() != hgs.size()) {
+		if (conn_pool_stats.first) {
+			diag("Failed to extract stats from 'CONNPOOL'");
+		}
+		if (conn_pool_stats.second.size() != hgs.size()) {
+			diag("Expected '%ld' row in 'CONNPOOL' stats resultset", hgs.size());
+		}
+		return { EXIT_FAILURE, {} };
+	}
+
+	if (conn_pool_stats.first) {
+		return { conn_pool_stats.first, {} };
+	} else {
+		map<uint32_t,mysql_row_t> res_map {};
+
+		for (const mysql_row_t& row : conn_pool_stats.second) {
+			const string& column = row[POOL_STATS_IDX::HOSTGROUP];
+			const uint32_t hg = std::stol(row[POOL_STATS_IDX::HOSTGROUP]);
+
+			res_map.insert({ hg, row });
+		}
+
+		return { EXIT_SUCCESS, res_map };
+	}
+}
+
+int wait_for_cond(MYSQL* mysql, const std::string& query, uint32_t timeout) {
+	int result = EXIT_FAILURE;
+
+	auto start = std::chrono::system_clock::now();
+	std::chrono::duration<double> elapsed {};
+
+	while (elapsed.count() < timeout && result == EXIT_FAILURE) {
+		int rc = mysql_query(mysql, query.c_str());
+		fprintf(
+			stderr, "# %s: Waiting for condition '%s' in ('%s':%d)\n",
+			get_formatted_time().c_str(), query.c_str(), mysql->host, mysql->port
+		);
+
+		if (rc == EXIT_SUCCESS) {
+			MYSQL_RES* myres = mysql_store_result(mysql);
+			if (myres) {
+				uint32_t field_num = mysql_num_fields(myres);
+				uint32_t row_num = mysql_num_rows(myres);
+
+				if (field_num == 1 && row_num == 1) {
+					MYSQL_ROW row = mysql_fetch_row(myres);
+
+					if (row && strcasecmp("TRUE", row[0]) == 0) {
+						result = EXIT_SUCCESS;
+					}
+				}
+
+				mysql_free_result(myres);
+
+				if (result == EXIT_SUCCESS) {
+					break;
+				}
+			}
+		} else {
+			diag("Condition query failed with error: ('%d','%s')", mysql_errno(mysql), mysql_error(mysql));
+			result = EXIT_FAILURE;
+			break;
+		}
+
+		usleep(500 * 1000);
+
+		auto it_end = std::chrono::system_clock::now();
+		elapsed = it_end - start;
+	}
+
+	return result;
+}
+
+void check_conn_count(MYSQL* admin, const string& conn_type, uint32_t conn_num, int32_t hg) {
+	const string hg_s { to_string(hg) };
+	const string conn_num_s { to_string(conn_num) };
+	string select_conns_in_hg {};
+
+	if (hg == -1) {
+		select_conns_in_hg = "SELECT SUM(" + conn_type + ") FROM stats_mysql_connection_pool";
+	} else {
+		select_conns_in_hg = "SELECT " + conn_type + " FROM stats_mysql_connection_pool WHERE hostgroup=" + hg_s;
+	}
+
+	const string check_used_conns {
+		"SELECT IIF((" + select_conns_in_hg + ")=" + conn_num_s + ",'TRUE','FALSE')"
+	};
+
+	int to = wait_for_cond(admin, check_used_conns, 3);
+	ok(to == EXIT_SUCCESS, "Conns should met the required condition");
+
+	if (to != EXIT_SUCCESS) {
+		dump_conn_stats(admin, {});
+	}
+};
+
+void check_query_count(MYSQL* admin, uint32_t queries, uint32_t hg) {
+	const string queries_s { to_string(queries) };
+	const string hg_s { to_string(hg) };
+
+	const string select_hg_queries {
+		"SELECT Queries FROM stats_mysql_connection_pool WHERE hostgroup=" + to_string(hg)
+	};
+	const string check_queries {
+		"SELECT IIF((" + select_hg_queries + ")=" + queries_s + ",'TRUE','FALSE')"
+	};
+
+	int to = wait_for_cond(admin, check_queries, 3);
+	ok(to == EXIT_SUCCESS, "Queries counted on hg '%d' should be '%d'", hg, queries);
+
+	if (to != EXIT_SUCCESS) {
+		dump_conn_stats(admin, {});
+	}
+};
+
+void check_query_count(MYSQL* admin, vector<uint32_t> queries, uint32_t hg) {
+	const string queries_s {
+		std::accumulate(queries.begin(), queries.end(), std::string(),
+			[](const std::string& str, const uint32_t& n) -> std::string {
+				return str + (str.length() > 0 ? "," : "") + std::to_string(n);
+			}
+		)
+	};
+	const string hg_s { to_string(hg) };
+
+	const string select_hg_queries {
+		"SELECT Queries FROM stats_mysql_connection_pool WHERE hostgroup=" + to_string(hg)
+	};
+	const string check_queries {
+		"SELECT IIF((" + select_hg_queries + ") IN (" + queries_s + "),'TRUE','FALSE')"
+	};
+
+	int to = wait_for_cond(admin, check_queries, 3);
+	ok(to == EXIT_SUCCESS, "Queries counted on hg '%d' should be in '%s'", hg, queries_s.c_str());
+
+	if (to != EXIT_SUCCESS) {
+		dump_conn_stats(admin, {});
+	} else {
+		dump_conn_stats(admin, { hg });
+	}
+};
diff --git a/test/tap/tap/utils.h b/test/tap/tap/utils.h
index 0863dd33d4..c490b9dfe4 100644
--- a/test/tap/tap/utils.h
+++ b/test/tap/tap/utils.h
@@ -14,6 +14,7 @@
 #include "sqlite3db.h"
 
 #include "command_line.h"
+#include "json.hpp"
 
 inline std::string get_formatted_time() {
 	time_t __timer;
@@ -459,6 +460,11 @@ int extract_sqlite3_host_port(MYSQL* admin, std::pair<std::string, int>& host_po
  */
 std::vector<std::string> split(const std::string& s, char delim);
 
+/**
+ * @brief Joins the supplied list of words using the supplied delim.
+ */
+std::string join(std::string delim, const std::vector<std::string>& words);
+
 /**
  * @brief Gets the supplied environmental variable as a std::string.
  * @param var The variable to value to extract.
@@ -518,4 +524,72 @@ enum SQ3_RES_T {
  */
 sq3_res_t sqlite3_execute_stmt(sqlite3* db, const std::string& query);
 
+/**
+ * @brief Returns a 'JSON' object holding 'PROXYSQL INTERNAL SESSION' contents.
+ * @param proxy And already openned connection to ProxySQL.
+ */
+nlohmann::json fetch_internal_session(MYSQL* proxy);
+
+/**
+ * @brief Returns a string table representation of the supplied resultset.
+ */
+std::string dump_as_table(MYSQL_RES* result);
+
+using mysql_row_t = std::vector<std::string>;
+
+/**
+ * @brief Executes a DQL query and returns the contents of its resultset.
+ * @param conn An already opened MYSQL connection.
+ * @param query The DQL query to be executed.
+ * @param dump_res Wether or not to dump the resultset contents as a table to 'stderr'.
+ * @return A pair with the shape {err_code, contents}.
+ */
+std::pair<int,std::vector<mysql_row_t>> exec_dql_query(MYSQL* conn, const std::string& query, bool dump_res=false);
+
+struct POOL_STATS_IDX {
+	enum {
+		HOSTGROUP,
+		CONN_USED,
+		CONN_FREE,
+		CONN_OK,
+		CONN_ERR,
+		MAX_CONN_USED,
+		QUERIES,
+	};
+};
+
+/**
+ * @brief Dumps a resultset with fields from the supplied hgs from 'stats_mysql_connection_pool'.
+ * @details The fetched fields are 'hostgroup,ConnUsed,ConnFree,ConnOk,ConnERR,MaxConnUsed,Queries'.
+ */
+int dump_conn_stats(MYSQL* admin, const std::vector<uint32_t> hgs);
+
+using pool_state_t = std::map<uint32_t,mysql_row_t>;
+
+/**
+ * @brief Fetches several fields from table 'stats_mysql_connection_pool' for supplied hostgroups.
+ * @details The fetched fields are 'hostgroup,ConnUsed,ConnFree,ConnOk,ConnERR,MaxConnUsed,Queries'.
+ * @param admin An already opened connection to Admin.
+ * @param hgs The hostgroups from which to fetch several fields.
+ * @return A pair of the shape {err_code, pool_state_t}.
+ */
+std::pair<int,pool_state_t> fetch_conn_stats(MYSQL* admin, const std::vector<uint32_t> hgs);
+/**
+ * @brief Waits until the condition specified by the 'query' holds, or 'timeout' is reached.
+ * @details Several details about the function impl:
+ *   - Sleeps of 500ms are performed between each check.
+ *   - The time and check being performed is always logged ahead.
+ *   - If query execution fails, reason is logged, wait aborted and EXIT_FAILURE returned.
+ * @param mysql And already opened conn to ProxySQL in which the query is to be executed.
+ * @param query Query with the condition check, it's expected to return 'TRUE' when the check succeeds.
+ * @param timeout A timeout specified in seconds.
+ * @return EXIT_SUCCESS if the checks holds before the timeout, EXIT_FAILURE otherwise.
+ */
+int wait_for_cond(MYSQL* mysql, const std::string& query, uint32_t timeout);
+
+// Helpers using 'wait_for_cond' on 'stats_mysql_connection'
+void check_conn_count(MYSQL* admin, const std::string& conn_type, uint32_t conn_num, int32_t hg=-1);
+void check_query_count(MYSQL* admin, uint32_t queries, uint32_t hg);
+void check_query_count(MYSQL* admin, std::vector<uint32_t> queries, uint32_t hg);
+
 #endif // #define UTILS_H
diff --git a/test/tap/tests/reg_test_3184-set_wait_timeout-t.cpp b/test/tap/tests/reg_test_3184-set_wait_timeout-t.cpp
index cb3f967d0b..ff5b5cdcc5 100644
--- a/test/tap/tests/reg_test_3184-set_wait_timeout-t.cpp
+++ b/test/tap/tests/reg_test_3184-set_wait_timeout-t.cpp
@@ -23,24 +23,6 @@
 using std::string;
 using namespace nlohmann;
 
-
-/**
- * @brief Helper function to convert a 'MYSQL_RES' into a
- *   nlohmann::json.
- *
- * @param result The 'MYSQL_RES*' to be converted into JSON.
- * @param j 'nlohmann::json' output parameter holding the
- *   converted 'MYSQL_RES' supplied.
- */
-void parse_result_json_column(MYSQL_RES *result, json& j) {
-	if(!result) return;
-	MYSQL_ROW row;
-
-	while ((row = mysql_fetch_row(result))) {
-		j = json::parse(row[0]);
-	}
-}
-
 /**
  * @brief Valid variations of 'SET wait_timeout' supported
  *  by ProxySQL to be ignored.
@@ -96,12 +78,7 @@ int main(int argc, char** argv) {
 		int query_err = mysql_query(proxysql_mysql, set_wait_timeout.c_str());
 		ok (query_err == 0, "Query '%s' should be properly executed.", set_wait_timeout.c_str());
 
-		MYSQL_QUERY(proxysql_mysql, "PROXYSQL INTERNAL SESSION");
-		json j_status {};
-		MYSQL_RES* int_session_res = mysql_store_result(proxysql_mysql);
-		parse_result_json_column(int_session_res, j_status);
-		mysql_free_result(int_session_res);
-
+		json j_status = fetch_internal_session(proxysql_mysql);
 		bool found_backends = j_status.contains("backends");
 		ok(found_backends == false, "No backends should be found for the current connection.");
 	}
diff --git a/test/tap/tests/reg_test_3493-USE_with_comment-t.cpp b/test/tap/tests/reg_test_3493-USE_with_comment-t.cpp
index 9ae2251088..ee83a67f71 100644
--- a/test/tap/tests/reg_test_3493-USE_with_comment-t.cpp
+++ b/test/tap/tests/reg_test_3493-USE_with_comment-t.cpp
@@ -45,15 +45,7 @@ void parse_result_json_column(MYSQL_RES *result, json& j) {
 int get_session_schemaname(MYSQL* proxysql, std::string& schemaname) {
 	int res = EXIT_FAILURE;
 
-	json j_status;
-	int query_res = mysql_query(proxysql, "PROXYSQL INTERNAL SESSION");
-	if (query_res) {
-		return query_res;
-	}
-
-	MYSQL_RES* tr_res = mysql_store_result(proxysql);
-	parse_result_json_column(tr_res, j_status);
-	mysql_free_result(tr_res);
+	json j_status = fetch_internal_session(proxysql);
 
 	try {
 		schemaname = j_status["client"]["userinfo"]["schemaname"];
diff --git a/test/tap/tests/reg_test_4264-commit_rollback-t.cpp b/test/tap/tests/reg_test_4264-commit_rollback-t.cpp
new file mode 100644
index 0000000000..15369d8895
--- /dev/null
+++ b/test/tap/tests/reg_test_4264-commit_rollback-t.cpp
@@ -0,0 +1,1197 @@
+/**
+ * @file reg_test_4264-commit_rollback-t.cpp
+ * @brief Verifies that 'COMMIT' and 'ROLLBACK' are executed in the correct backend connections when several
+ *  connections are hold by a session.
+ * @details General test methodology:
+ *  0. Create database and tables for performing the tests
+ *  1. Configure the required 'mysql_servers' and 'mysql_query_rules'.
+ *  2. Extract data 'stats_mysql_connection_pool' (and or 'PROXYSQL INTERNAL SESSION').
+ *  3. Perform operations (INSERT,COMMIT,ROLLBACK) in the target servers.
+ *  4. Extract data again from 'stats_mysql_connection_pool' (and or 'PROXYSQL INTERNAL SESSION').
+ *
+ *  Repeat the last three elements for every query cycle to test:
+ *   - Simple BEGIN, 'COMMIT|ROLLBACK' - Explicit trxs.
+ *   - Autocommit=0, Query, 'COMMIT|ROLLBACK' - Implicit trxs.
+ *   - Failing queries - Unknown Transaction Status:
+ *
+ *  Using the previous query cycles, check increasingly complex scenarios:
+ *  - With persistent connections:
+ *    + Check with explicit transaction in def/non-def hgs.
+ *    + Check with explicit transaction in def/non-def hgs + error.
+ *    + Check with implicit transaction in def/non-def hgs.
+ *    + Check with implicit transaction in def/non-def hgs + error.
+ *    + Check no transaction + error.
+ *   - Without persistent connections:
+ *    + Previous scenarios but on different backend connections.
+ *    + Include a 'SAVEPOINT' in a third connection.
+ */
+
+#include <chrono>
+#include <iomanip>
+#include <iostream>
+
+#include <algorithm>
+#include <cstring>
+#include <functional>
+#include <map>
+#include <string>
+#include <vector>
+#include <utility>
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include <mysql.h>
+#include <mysql/mysqld_error.h>
+
+#include "tap.h"
+#include "utils.h"
+#include "command_line.h"
+#include "proxysql_utils.h"
+#include "json.hpp"
+
+using std::function;
+using std::pair;
+using std::vector;
+using std::string;
+
+using std::to_string;
+using nlohmann::json;
+
+const uint32_t DF_HG = 0;
+const uint32_t TG_HG_1 = 1047;
+const uint32_t TG_HG_2 = 1048;
+const string TG_HG_STR { to_string(TG_HG_1) };
+
+/**
+ * @details Flow for explicit and persistent trxs:
+ *  - BEGIN -> Starts a trx, in default hostgroup.
+ *    + Check that ConnUsed incremented in that hostgroup.
+ *    + Check that query should have been issued in that hostgroup.
+ *  - TG_HG_1 - INSERT INTO -> Should try to reach another hostgroup. Failing to do so due to persist.
+ *    + Check that query have been executed in the 'BEGIN' hostgroup.
+ *  - COMMIT|ROLLBACK -> Should be executed in original hostgroup.
+ *    + Check that query have been executed in the 'BEGIN' hostgroup.
+ *    + Check that ConnUsed have decreased after query.
+ */
+int explicit_trx_persist(CommandLine& cl, MYSQL* admin, MYSQL* proxy, const string& trx_cmd) {
+	const vector<uint32_t> tg_hgs { DF_HG };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t df_hg_qs = std::stol(pre_pool_state.at(DF_HG)[POOL_STATS_IDX::QUERIES]);
+
+	MYSQL_QUERY_T(proxy, "BEGIN");
+	diag("Only connection should be in use for any hg");
+	check_conn_count(admin, "ConnUsed", 1);
+	diag("Only connection should be in use for hg '%d'", DF_HG);
+	check_conn_count(admin, "ConnUsed", 1, DF_HG);
+	diag("Query should have been issued to hg '%d'", TG_HG_1);
+	check_query_count(admin, df_hg_qs + 1, DF_HG);
+
+	diag("Query intentionally targeting unreachable hostgroup due to 'persist'");
+	MYSQL_QUERY_T(proxy, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+	check_query_count(admin, df_hg_qs + 2, DF_HG);
+
+	MYSQL_QUERY_T(proxy, trx_cmd.c_str());
+	check_query_count(admin, df_hg_qs + 3, DF_HG);
+	check_conn_count(admin, "ConnUsed", 0, DF_HG);
+
+	return EXIT_SUCCESS;
+}
+
+/**
+ * @details Same check as 'explicit_trx_persist' but trx is created in random hostgroup.
+ *  Ensures that default hostgroup routing works as non-default routing.
+ */
+int explicit_trx_persist_2(CommandLine& cl, MYSQL* admin, MYSQL* proxy, const string& trx_cmd) {
+	const vector<uint32_t> tg_hgs { DF_HG, TG_HG_1 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t df_hg_qs = std::stol(pre_pool_state.at(DF_HG)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+
+	MYSQL_QUERY_T(proxy, ("/* hostgroup=" + to_string(TG_HG_1) + "*/ BEGIN").c_str());
+	diag("Only connection should be in use for hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+	diag("Query should have been issued to hg '%d'", TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	diag("Query intentionally targeting unreachable hostgroup due to 'persist'");
+	MYSQL_QUERY_T(proxy, "DO 1");
+	check_query_count(admin, tg_hg_1_qs + 2, TG_HG_1);
+
+	MYSQL_QUERY_T(proxy, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_1_qs + 3, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	return EXIT_SUCCESS;
+}
+
+/**
+ * @details Tests that explicit transactions via 'BEGIN' and 'COMMIT' with
+ *  'transaction_persistent=1' should disable routing, and all operations
+ *  should be done in the same backend connection.
+ */
+int explicit_trx_persist_c(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_trx_persist(cl, admin, proxy, "COMMIT");
+}
+
+int explicit_trx_persist_r(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_trx_persist(cl, admin, proxy, "ROLLBACK");
+}
+
+int explicit_trx_persist_2_c(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_trx_persist_2(cl, admin, proxy, "COMMIT");
+}
+
+int explicit_trx_persist_2_r(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_trx_persist_2(cl, admin, proxy, "ROLLBACK");
+}
+
+int implicit_trx_persist(CommandLine& cl, MYSQL* admin, MYSQL* proxy, const string& trx_cmd) {
+	const vector<uint32_t> tg_hgs { DF_HG, TG_HG_1 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t df_hg_qs = std::stol(pre_pool_state.at(DF_HG)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+
+	MYSQL_QUERY_T(proxy, "SET autocommit=0");
+
+	diag("No conns should be in use for any hostgroup");
+	check_conn_count(admin, "ConnUsed", 0);
+	diag("No conns should be in use for hg '%d'", DF_HG);
+	check_conn_count(admin, "ConnUsed", 0, DF_HG);
+
+	diag("No queries should have been issued to hg '%d'", DF_HG);
+	check_query_count(admin, df_hg_qs, DF_HG);
+
+	MYSQL_QUERY_T(proxy, "INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+	check_query_count(admin, df_hg_qs + 1, DF_HG);
+	check_conn_count(admin, "ConnUsed", 1, DF_HG);
+
+	MYSQL_QUERY_T(proxy, trx_cmd.c_str());
+	check_query_count(admin, df_hg_qs + 2, DF_HG);
+	check_conn_count(admin, "ConnUsed", 0, DF_HG);
+
+	return EXIT_SUCCESS;
+}
+
+int implicit_trx_persist_c(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return implicit_trx_persist(cl, admin, proxy, "COMMIT");
+}
+
+int implicit_trx_persist_r(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return implicit_trx_persist(cl, admin, proxy, "ROLLBACK");
+}
+
+int explicit_trx_persist_no_def_hg(CommandLine& cl, MYSQL* admin, MYSQL* proxy, const string& trx_cmd) {
+	const vector<uint32_t> tg_hgs { TG_HG_1 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+
+	MYSQL_QUERY_T(proxy, ("/* hostgroup=" + to_string(TG_HG_1) + "*/ BEGIN").c_str());
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	diag("Query intentionally targeting unreachable hostgroup due to 'persist'");
+	MYSQL_QUERY_T(proxy, "/* TG_HG_2 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+	check_query_count(admin, tg_hg_1_qs + 2, TG_HG_1);
+
+	MYSQL_QUERY_T(proxy, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_1_qs + 3, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	return EXIT_SUCCESS;
+}
+
+int explicit_trx_persist_no_def_hg_c(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_trx_persist_no_def_hg(cl, admin, proxy, "COMMIT");
+}
+
+int explicit_trx_persist_no_def_hg_r(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_trx_persist_no_def_hg(cl, admin, proxy, "ROLLBACK");
+}
+
+int implicit_trx_persist_no_def_hg(CommandLine& cl, MYSQL* admin, MYSQL* proxy, const string& trx_cmd) {
+	const vector<uint32_t> tg_hgs { DF_HG, TG_HG_1 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t df_hg_qs = std::stol(pre_pool_state.at(DF_HG)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+
+	MYSQL_QUERY_T(proxy, "SET autocommit=0");
+
+	diag("No conns should be in use for hg '%d'", DF_HG);
+	check_conn_count(admin, "ConnUsed", 0, DF_HG);
+	diag("No queries should have been issued to hg '%d'", DF_HG);
+	check_query_count(admin, df_hg_qs, DF_HG);
+
+	MYSQL_QUERY_T(proxy, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+	MYSQL_QUERY_T(proxy, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_1_qs + 2, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	return EXIT_SUCCESS;
+}
+
+int implicit_trx_persist_no_def_hg_c(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return implicit_trx_persist_no_def_hg(cl, admin, proxy, "COMMIT");
+}
+
+int implicit_trx_persist_no_def_hg_r(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return implicit_trx_persist_no_def_hg(cl, admin, proxy, "ROLLBACK");
+}
+
+/**
+ * @details Flow for explicit and persistent trxs:
+ *  - BEGIN -> Starts a trx, in default hostgroup.
+ *    + Check that ConnUsed incremented in that hostgroup.
+ *    + Check that query should have been issued in that hostgroup.
+ *  - TG_HG_1 - INSERT INTO -> Should succeed to execute in hostgroup 'N' (no-persist).
+ *    + Check that query have been executed in the 'N' hostgroup.
+ *  - COMMIT|ROLLBACK -> Should be executed in original hostgroup.
+ *    + Check that query have been executed in the 'BEGIN' hostgroup.
+ *    + Check that ConnUsed have decreased after query.
+ */
+int explicit_trx_no_persist(CommandLine& cl, MYSQL* admin, MYSQL*, const string& trx_cmd) {
+	MYSQL* proxy_sbtest = mysql_init(NULL);
+
+	if (!mysql_real_connect(proxy_sbtest, cl.host, "sbtest1", "sbtest1", NULL, cl.port, NULL, 0)) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy_sbtest));
+		return EXIT_FAILURE;
+	}
+
+	const vector<uint32_t> tg_hgs { DF_HG, TG_HG_1 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t df_hg_qs = std::stol(pre_pool_state.at(DF_HG)[POOL_STATS_IDX::QUERIES]);
+
+	// Started transaction in 'DF_HG'
+	MYSQL_QUERY_T(proxy_sbtest, "BEGIN");
+	check_conn_count(admin, "ConnUsed", 1, DF_HG);
+	check_query_count(admin, df_hg_qs + 1, DF_HG);
+
+	// Query redirected to 'TG_HG' imposed by query rule
+	MYSQL_QUERY_T(proxy_sbtest, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	// Query redirected to 'DF_HG' where trx was started
+	MYSQL_QUERY_T(proxy_sbtest, trx_cmd.c_str());
+	check_query_count(admin, df_hg_qs + 2, DF_HG);
+	check_conn_count(admin, "ConnUsed", 0, DF_HG);
+
+	mysql_close(proxy_sbtest);
+
+	return EXIT_SUCCESS;
+};
+
+/**
+ * @details Same check as 'explicit_trx_no_persist' but trx is created in random hostgroup.
+ *  Ensures that default hostgroup routing works as non-default routing.
+ */
+int explicit_trx_no_persist_2(CommandLine& cl, MYSQL* admin, MYSQL*, const string& trx_cmd) {
+	MYSQL* sbtest = mysql_init(NULL);
+
+	if (!mysql_real_connect(sbtest, cl.host, "sbtest1", "sbtest1", NULL, cl.port, NULL, 0)) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(sbtest));
+		return EXIT_FAILURE;
+	}
+
+	const vector<uint32_t> tg_hgs { DF_HG, TG_HG_1 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t df_hg_qs = std::stol(pre_pool_state.at(DF_HG)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+
+	MYSQL_QUERY_T(sbtest, ("/* hostgroup=" + to_string(TG_HG_1) + "*/ BEGIN").c_str());
+	diag("Only connection should be in use for hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+	diag("Query should have been issued to hg '%d'", TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	diag("Query intentionally targeting unreachable hostgroup due to 'persist'");
+	MYSQL_QUERY_T(sbtest, "DO 1");
+	check_query_count(admin, df_hg_qs + 1, DF_HG);
+
+	MYSQL_QUERY_T(sbtest, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_1_qs + 2, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	mysql_close(sbtest);
+
+	return EXIT_SUCCESS;
+}
+
+int explicit_trx_no_persist_c(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return explicit_trx_no_persist(cl, admin, nullptr, "COMMIT");
+}
+
+int explicit_trx_no_persist_r(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return explicit_trx_no_persist(cl, admin, nullptr, "ROLLBACK");
+}
+
+int explicit_trx_no_persist_2_c(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_trx_no_persist_2(cl, admin, proxy, "COMMIT");
+}
+
+int explicit_trx_no_persist_2_r(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_trx_no_persist_2(cl, admin, proxy, "ROLLBACK");
+}
+
+int explicit_trx_no_persist_no_def_hg(CommandLine& cl, MYSQL* admin, MYSQL*, const string& trx_cmd) {
+	MYSQL* proxy_sbtest = mysql_init(NULL);
+
+	if (!mysql_real_connect(proxy_sbtest, cl.host, "sbtest1", "sbtest1", NULL, cl.port, NULL, 0)) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy_sbtest));
+		return EXIT_FAILURE;
+	}
+
+	const vector<uint32_t> tg_hgs { TG_HG_1, TG_HG_2 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_2_qs = std::stol(pre_pool_state.at(TG_HG_2)[POOL_STATS_IDX::QUERIES]);
+
+	// Started transaction in 'TG_HG_2'
+	MYSQL_QUERY_T(proxy_sbtest, ("/* hostgroup=" + to_string(TG_HG_2) + " */ BEGIN").c_str());
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_2);
+	check_query_count(admin, tg_hg_2_qs + 1, TG_HG_2);
+
+	// Query redirected to 'TG_HG' imposed by query rule
+	MYSQL_QUERY_T(proxy_sbtest, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	// Query redirected to 'TG_HG_2' where trx was started
+	MYSQL_QUERY_T(proxy_sbtest, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_2_qs + 2, TG_HG_2);
+	check_conn_count(admin, "ConnUsed", 0, DF_HG);
+
+	mysql_close(proxy_sbtest);
+
+	return EXIT_SUCCESS;
+};
+
+int explicit_trx_no_persist_no_def_hg_c(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return explicit_trx_no_persist_no_def_hg(cl, admin, nullptr, "COMMIT");
+}
+
+int explicit_trx_no_persist_no_def_hg_r(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return explicit_trx_no_persist_no_def_hg(cl, admin, nullptr, "ROLLBACK");
+}
+
+/**
+ * @details Checks that implicit transactions with no persistence execute the rollback in the correct
+ *  hostgroup.
+ */
+int implicit_trx_no_persist_no_def_hg(CommandLine& cl, MYSQL* admin, MYSQL*, const string& trx_cmd) {
+	MYSQL* proxy_sbtest = mysql_init(NULL);
+
+	if (!mysql_real_connect(proxy_sbtest, cl.host, "sbtest1", "sbtest1", NULL, cl.port, NULL, 0)) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy_sbtest));
+		return EXIT_FAILURE;
+	}
+
+	const vector<uint32_t> tg_hgs { TG_HG_1, TG_HG_2 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_2_qs = std::stol(pre_pool_state.at(TG_HG_2)[POOL_STATS_IDX::QUERIES]);
+
+	// Started transaction in 'DF_HG'
+	MYSQL_QUERY_T(proxy_sbtest, "SET autocommit=0");
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs, TG_HG_1);
+
+	// Query redirected to 'TG_HG_1' imposed by query rule, trx started
+	MYSQL_QUERY_T(proxy_sbtest, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+	// Query redirected to 'TG_HG_2' imposed by query rule, non-persistent conn, another trx started
+	MYSQL_QUERY_T(proxy_sbtest, "/* TG_HG_2 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+
+	diag("Dump 'conn_pool' stats after previous queries");
+	dump_conn_stats(admin, { TG_HG_1, TG_HG_2 });
+
+	diag("Checking that trx was started for previous query on hg '%d'", TG_HG_2);
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+	diag("Checking that trx was started for previous query on hg '%d'", TG_HG_2);
+	check_query_count(admin, tg_hg_2_qs + 1, TG_HG_2);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_2);
+
+	diag("Checking that we currently have two globally started trx");
+	check_conn_count(admin, "ConnUsed", 2);
+
+	// Since ProxySQL cannot issue multiple 'COMMIT|ROLLBACK' as a response to a client one, only one of the
+	// oppened trxs will received the closing statement.
+	MYSQL_QUERY_T(proxy_sbtest, trx_cmd.c_str());
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_2);
+
+	MYSQL_QUERY_T(proxy_sbtest, trx_cmd.c_str());
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_2);
+
+	mysql_close(proxy_sbtest);
+
+	return EXIT_SUCCESS;
+};
+
+int implicit_trx_no_persist_no_def_hg_c(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return implicit_trx_no_persist_no_def_hg(cl, admin, nullptr, "COMMIT");
+}
+
+int implicit_trx_no_persist_no_def_hg_r(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return implicit_trx_no_persist_no_def_hg(cl, admin, nullptr, "ROLLBACK");
+}
+
+/**
+ * @details Flow for persistent-implicit transaction:
+ *  - BEGIN executed in hg 'N':
+ *    + Check that ConnUsed incremented in that hostgroup.
+ *    + Check that query should have been issued in that hostgroup.
+ *  - Query failing to be executed in different hg from BEGIN:
+ *    + Should target other hg than 'N' but due to persistent be executed in 'N'.
+ *    + Check that conns in use have increased in tg hg.
+ *    + Check that queries have increased in tg hg.
+ *  - COMMIT|ROLLBACK -> Should be executed in hg from prev query.
+ *    + Check that command is executed in hg from previous query.
+ *    + Check that conns used have decreased in hg.
+ */
+int explicit_unknown_trx_persist_no_def_hg(
+	CommandLine& cl, MYSQL* admin, MYSQL* proxy, const string& trx_cmd
+) {
+	diag("Ensure 'autocommit=1' for reused connection");
+	MYSQL_QUERY_T(proxy, "SET autocommit=1");
+
+	diag("Initial insert to ensure that 'id=1' is taken in the table");
+	MYSQL_QUERY_T(proxy, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+
+	diag("Checking that a trx wasn't started for previous query on hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	const vector<uint32_t> tg_hgs { TG_HG_1 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+
+	MYSQL_QUERY_T(proxy, ("/* hostgroup=" + to_string(TG_HG_1) + "*/ BEGIN").c_str());
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	diag("Query intentionally targeting unreachable hostgroup due to 'persist'");
+	int rc = mysql_query_t(
+		proxy, "/* TG_HG_2 */ INSERT INTO test.commit_rollback (id,k,c,p) VALUES (1,1,'foo','bar')"
+	);
+	int err_code = mysql_errno(proxy);
+	ok(rc != 0 && err_code == 1062, "Insert should failed - exp_err: 1062, act_err: %d", err_code);
+
+	diag("Queries should have been issued to hg '%d'", TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs + 2, TG_HG_1);
+
+	diag("Checking that trx was started for previous query on hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+	diag("Issuing '%s' should end the 'unknown-status' trx due to error", trx_cmd.c_str());
+	MYSQL_QUERY_T(proxy, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_1_qs + 3, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	return EXIT_SUCCESS;
+}
+
+int explicit_unknown_trx_persist_no_def_hg_c(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_unknown_trx_persist_no_def_hg(cl, admin, proxy, "COMMIT");
+}
+
+int explicit_unknown_trx_persist_no_def_hg_r(CommandLine& cl, MYSQL* admin, MYSQL* proxy) {
+	return explicit_unknown_trx_persist_no_def_hg(cl, admin, proxy, "ROLLBACK");
+}
+
+/**
+ * @details Flow for persistent-implicit transaction with unknown state:
+ *  - SET autocommit=0
+ *    + Check that conns didn't increase.
+ *    + Check that query haven't been issued, intercepted by ProxySQL.
+ *  - Query failing to be executed in non-def hg -> Unknown trx state
+ *    + Check that conns in use have increased in tg hg.
+ *    + Check that queries have increased in tg hg.
+ *  - COMMIT|ROLLBACK -> Should be executed in hg from prev query.
+ *    + Check that command is executed in hg from previous query.
+ *    + Check that conns used have decreased in hg.
+ */
+int implicit_unknown_trx_persist_no_def_hg(
+	CommandLine& cl, MYSQL* admin, MYSQL* proxy, const string& trx_cmd
+) {
+	diag("Ensure 'autocommit=1' for reused connection");
+	MYSQL_QUERY_T(proxy, "SET autocommit=1");
+
+	diag("Initial insert to ensure that 'id=1' is taken in the table");
+	MYSQL_QUERY_T(proxy, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+
+	diag("Checking that a trx wasn't started for previous query on hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	const vector<uint32_t> tg_hgs { TG_HG_1 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+
+	MYSQL_QUERY_T(proxy, "SET autocommit=0");
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs, TG_HG_1);
+
+	int rc = mysql_query_t(
+		proxy, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (id,k,c,p) VALUES (1,1,'foo','bar')"
+	);
+	int err_code = mysql_errno(proxy);
+	ok(rc != 0 && err_code == 1062, "Insert should failed - exp_err: 1062, act_err: %d", err_code);
+
+	diag("Query should have been issued to hg '%d'", TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	diag("Checking that trx was started for previous query on hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+	diag("Issuing '%s' should end the 'unknown-status' trx due to error", trx_cmd.c_str());
+	MYSQL_QUERY_T(proxy, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_1_qs + 2, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	return EXIT_SUCCESS;
+}
+
+int implicit_unknown_trx_persist_no_def_hg_c(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return implicit_unknown_trx_persist_no_def_hg(cl, admin, nullptr, "COMMIT");
+}
+
+int implicit_unknown_trx_persist_no_def_hg_r(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return implicit_unknown_trx_persist_no_def_hg(cl, admin, nullptr, "ROLLBACK");
+}
+
+/**
+ * @details Flow for explicit, and unknown non-persistent trxs:
+ *  - BEGIN
+ *  - Execute failing query in another hg ('N') due to non-persist ('unknown trx status').
+ *  - COMMIT|ROLLBACK
+ *    + Should be executed in BEGIN hg.
+ *  - COMMIT|ROLLBACK
+ *    + Should be executed in trx with 'unknown_transaction_status'.
+ */
+int explicit_and_unknown_trx_no_persist_no_def_hg(
+	CommandLine& cl, MYSQL* admin, MYSQL*, const string& trx_cmd
+) {
+	MYSQL* proxy_sbtest = mysql_init(NULL);
+
+	if (!mysql_real_connect(proxy_sbtest, cl.host, "sbtest1", "sbtest1", NULL, cl.port, NULL, 0)) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy_sbtest));
+		return EXIT_FAILURE;
+	}
+
+	diag("Initial insert to ensure that 'id=1' is taken in the table");
+	MYSQL_QUERY_T(
+		proxy_sbtest, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')"
+	);
+
+	const vector<uint32_t> tg_hgs { DF_HG, TG_HG_1, TG_HG_2 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_2_qs = std::stol(pre_pool_state.at(TG_HG_2)[POOL_STATS_IDX::QUERIES]);
+
+	MYSQL_QUERY_T(proxy_sbtest, ("/* hostgroup=" + to_string(TG_HG_1) + "*/ BEGIN").c_str());
+	diag("Only connection should be in use for hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+	diag("Query should have been issued to hg '%d'", TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	int rc = mysql_query_t(
+		proxy_sbtest, "/* TG_HG_2 */ INSERT INTO test.commit_rollback (id,k,c,p) VALUES (1,1,'foo','bar')"
+	);
+	int err_code = mysql_errno(proxy_sbtest);
+	ok(rc != 0 && err_code == 1062, "Insert should failed - exp_err: 1062, act_err: %d", err_code);
+
+	diag("Queries should have been issued to hg '%d'", TG_HG_2);
+	check_query_count(admin, tg_hg_2_qs + 1, TG_HG_2);
+
+	diag("Checking that conn was flagged as 'unknown_transaction_status' on hg '%d'", TG_HG_2);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_2);
+
+	diag("Issuing '%s' should end the initial explicit transaction first", trx_cmd.c_str());
+	MYSQL_QUERY_T(proxy_sbtest, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_1_qs + 2, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	diag("Issuing '%s' should end the 'unknown-status' trx due to error", trx_cmd.c_str());
+	MYSQL_QUERY_T(proxy_sbtest, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_2_qs + 2, TG_HG_2);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_2);
+
+	mysql_close(proxy_sbtest);
+
+	return EXIT_SUCCESS;
+}
+
+int explicit_and_unknown_trx_no_persist_no_def_hg_c(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return explicit_and_unknown_trx_no_persist_no_def_hg(cl, admin, nullptr, "COMMIT");
+}
+
+int explicit_and_unknown_trx_no_persist_no_def_hg_r(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return explicit_and_unknown_trx_no_persist_no_def_hg(cl, admin, nullptr, "ROLLBACK");
+}
+
+/**
+ * @details Flow for implicit, and unknown non-persistent trxs:
+ *  - SET autcommit=0
+ *  - Execute query in hg ('N'), this creates a trx.
+ *  - Execute failing query in another hg ('M') due to non-persist ('unknown trx status').
+ *  - COMMIT|ROLLBACK
+ *    + Should be executed in hg 'N'.
+ *  - COMMIT|ROLLBACK
+ *    + Should be executed in trx with 'unknown_transaction_status', hg 'M'.
+ */
+int implicit_and_unknown_trx_no_persist_no_def_hg(
+	CommandLine& cl, MYSQL* admin, MYSQL*, const string& trx_cmd
+) {
+	MYSQL* sbtest = mysql_init(NULL);
+
+	if (!mysql_real_connect(sbtest, cl.host, "sbtest1", "sbtest1", NULL, cl.port, NULL, 0)) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(sbtest));
+		return EXIT_FAILURE;
+	}
+
+	diag("Initial insert to ensure that 'id=1' is taken in the table");
+	MYSQL_QUERY_T(
+		sbtest, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')"
+	);
+
+	const vector<uint32_t> tg_hgs { TG_HG_1, TG_HG_2 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_2_qs = std::stol(pre_pool_state.at(TG_HG_2)[POOL_STATS_IDX::QUERIES]);
+
+	diag("Checking that a trx wasn't started for previous query on hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	MYSQL_QUERY_T(sbtest, "SET autocommit=0");
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs, TG_HG_1);
+
+	MYSQL_QUERY_T(sbtest, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+
+	diag("Queries should have been issued to hg '%d'", TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	diag("Checking that trx was started for previous query on hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+	int rc = mysql_query_t(
+		sbtest, "/* TG_HG_2 */ INSERT INTO test.commit_rollback (id,k,c,p) VALUES (1,1,'foo','bar')"
+	);
+	int err_code = mysql_errno(sbtest);
+	ok(rc != 0 && err_code == 1062, "Insert should failed - exp_err: 1062, act_err: %d", err_code);
+
+	diag("Queries should have been issued to hg '%d'", TG_HG_2);
+	check_query_count(admin, tg_hg_2_qs + 1, TG_HG_2);
+
+	diag("Checking that conn was flagged as 'unknown_transaction_status' on hg '%d'", TG_HG_2);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_2);
+
+	diag("Issuing '%s' should end the initial explicit transaction first", trx_cmd.c_str());
+	MYSQL_QUERY_T(sbtest, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_1_qs + 2, TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	diag("Issuing '%s' should end the 'unknown-status' trx due to error", trx_cmd.c_str());
+	MYSQL_QUERY_T(sbtest, trx_cmd.c_str());
+	check_query_count(admin, tg_hg_2_qs + 2, TG_HG_2);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_2);
+
+	mysql_close(sbtest);
+
+	return EXIT_SUCCESS;
+}
+
+int implicit_and_unknown_trx_no_persist_no_def_hg_c(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return implicit_and_unknown_trx_no_persist_no_def_hg(cl, admin, nullptr, "COMMIT");
+}
+
+int implicit_and_unknown_trx_no_persist_no_def_hg_r(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return implicit_and_unknown_trx_no_persist_no_def_hg(cl, admin, nullptr, "ROLLBACK");
+}
+
+/**
+ * @details This test involves the three different logics for trx detection. In a non-persistent session:
+ *  - Savepoint creation in TG_HG_1
+ *    + Check that 'SAVEPOINT' is detected in the conn
+ *  - Transaction started for TG_HG_2
+ *    + Check that transaction is detected query properly routed
+ *  - Error on DF_HG
+ *    + Check that error provoques retaining of the conn due to unknown status.
+ *  - Three 'COMMIT|ROLLBACK' are issued
+ *    + Check that each command is issued in the correct conn
+ *
+ *  For the final 'COMMIT|ROLLBACK' commands it's expected that:
+ *   * First two commands hit either 'SAVEPOINT' or known trx conn.
+ *   * Third command hits the conn with 'unknown trx' status.
+ */
+int implicit_trx_and_savepoints_no_persist_no_def_hg_(
+	CommandLine& cl, MYSQL* admin, MYSQL*, const string& trx_cmd
+) {
+	MYSQL* sbtest = mysql_init(NULL);
+
+	if (!mysql_real_connect(sbtest, cl.host, "sbtest1", "sbtest1", NULL, cl.port, NULL, 0)) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(sbtest));
+		return EXIT_FAILURE;
+	}
+
+	diag("Initial insert to ensure that 'id=1' is taken in the table");
+	MYSQL_QUERY_T(
+		sbtest, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')"
+	);
+
+	pool_state_t pre_pool_state {};
+	uint32_t df_hg_qs = 0;
+	uint32_t tg_hg_1_qs = 0;
+	uint32_t tg_hg_2_qs = 0;
+
+	auto op_0 = [&] () -> int {
+		const vector<uint32_t> tg_hgs { DF_HG, TG_HG_1, TG_HG_2 };
+		const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+
+		pre_pool_state = pre_pool_state_res.second;
+		df_hg_qs = std::stol(pre_pool_state.at(DF_HG)[POOL_STATS_IDX::QUERIES]);
+		tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+		tg_hg_2_qs = std::stol(pre_pool_state.at(TG_HG_2)[POOL_STATS_IDX::QUERIES]);
+
+		diag("Checking that a trx wasn't started for previous query on hg '%d'", TG_HG_1);
+		check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+		return EXIT_SUCCESS;
+	};
+
+	auto op_1 = [&] () -> int {
+		// DOC-NOTE: Force refresh 'active_transactions' field in 'INTERNAL SESSION'. Active transactions
+		// field is only refreshed if a query is executed in a backend connection which doesn't have the
+		// 'SERVER_STATUS_IN_TRANS' flag active. Thus, ProxySQL is forced to look for other "potential active
+		// transactions" for the session.
+		{
+			MYSQL_QUERY_T(sbtest, ("/* hostgroup=" + to_string(TG_HG_1) + " */ SET autocommit=1").c_str());
+			MYSQL_QUERY_T(sbtest, ("/* hostgroup=" + to_string(TG_HG_1) + " */ DO 1").c_str());
+		}
+
+		json j_session = fetch_internal_session(sbtest);
+		int prev_trxs = -1;
+
+		try {
+			prev_trxs = j_session["active_transactions"];
+		} catch (std::exception& e) {
+			diag("ERROR: Accessing 'INTERNAL SESSION' fields failed with error - %s", e.what());
+			return EXIT_FAILURE;
+		}
+
+		// DOC-NOTE: This autocommit is not forwarded, because in the flow of `op_1, op_2, op_3` no
+		// transaction is started, thus, ProxySQL wont forward it, instead, will send it with the next query,
+		// which won't count as a `query_sent`. That's the reason for the interval for the the `SAVEPOINT`
+		// query to start at `tg_hg_1_qs + 2`. The only autocommit values that are directly forwared, are the
+		// ones that can potentially end an ongoing transaction.
+		MYSQL_QUERY_T(sbtest, "SET autocommit=0");
+		check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+		check_query_count(admin, {tg_hg_1_qs + 1, tg_hg_1_qs + 2}, TG_HG_1);
+
+		MYSQL_QUERY_T(sbtest, ("/* hostgroup=" + to_string(TG_HG_1) + " */ SAVEPOINT s1").c_str());
+
+		diag("Queries should have been issued to hg '%d'", TG_HG_1);
+		check_query_count(admin, {tg_hg_1_qs + 2, tg_hg_1_qs + 3}, TG_HG_1);
+
+		diag("Checking that conn is kept ('has_savepoint') due to previous query on hg '%d'", TG_HG_1);
+		check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+		j_session = fetch_internal_session(sbtest);
+		bool has_savepoint = false;
+		int after_trxs = -1;
+
+		try {
+			has_savepoint = j_session["backends"][0]["conn"]["status"]["has_savepoint"];
+			after_trxs = j_session["active_transactions"];
+		} catch (std::exception& e) {
+			diag("ERROR: Accessing 'INTERNAL SESSION' fields failed with error - %s", e.what());
+			return EXIT_FAILURE;
+		}
+
+		bool ok_res = has_savepoint == true && prev_trxs == after_trxs;
+
+		ok(
+			ok_res,
+			"Savepoint should be present, but no trxs due to MySQL bug #107875 -"
+				" savepoint: %d, pre_trxs: %d, after_trxs: %d",
+			has_savepoint, prev_trxs, after_trxs
+		);
+		if (!ok_res) {
+			dump_conn_stats(admin, {});
+		}
+
+		return EXIT_SUCCESS;
+	};
+
+	auto op_2 = [&] () -> int {
+		MYSQL_QUERY_T(sbtest, "SET autocommit=0");
+		MYSQL_QUERY_T(sbtest, "/* TG_HG_2 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+
+		check_query_count(admin, {tg_hg_2_qs + 1, tg_hg_2_qs + 2}, TG_HG_2);
+		check_conn_count(admin, "ConnUsed", 1, TG_HG_2);
+
+		return EXIT_SUCCESS;
+	};
+
+	auto op_3 = [&] () -> int {
+		MYSQL_QUERY_T(sbtest, "SET autocommit=1");
+		int rc = mysql_query_t(sbtest, "INSERT INTO test.commit_rollback (id,k,c,p) VALUES (1,1,'foo','bar')");
+
+		int err_code = mysql_errno(sbtest);
+		ok(rc != 0 && err_code == 1062, "Insert should failed - exp_err: 1062, act_err: %d", err_code);
+
+		// DOC-NOTE: This autocommit is potentially forwared to the backend and counted as 'query_sent', this
+		// is because, in the cases this is not the first operation, and 'autocommit=0' have been previously
+		// executed in the conn, there would be an implicit ongoing transaction when this 'autocommit=1' is
+		// received, since this change in autocommit will end the current transaction, ProxySQL is forced to
+		// send it to the backend (in contrast to store it for later applying). This is the reason for the
+		// query interval of `{df_hg_qs + 1, df_hg_qs + 2}`.
+		diag("Checking the two previous queries were issued - AUTOCOMMIT + INSERT");
+		check_query_count(admin, {df_hg_qs + 1, df_hg_qs + 2}, DF_HG);
+		check_conn_count(admin, "ConnUsed", 1, DF_HG);
+
+		return EXIT_SUCCESS;
+	};
+
+	auto op_4 = [&] () -> int {
+		MYSQL_QUERY_T(sbtest, trx_cmd.c_str());
+		check_conn_count(admin, "ConnUsed", 2);
+		check_conn_count(admin, "ConnUsed", 1, DF_HG);
+
+		MYSQL_QUERY_T(sbtest, trx_cmd.c_str());
+		check_conn_count(admin, "ConnUsed", 1);
+		check_conn_count(admin, "ConnUsed", 1, DF_HG);
+
+		MYSQL_QUERY_T(sbtest, trx_cmd.c_str());
+		check_conn_count(admin, "ConnUsed", 0);
+		check_conn_count(admin, "ConnUsed", 0, DF_HG);
+
+		return EXIT_SUCCESS;
+	};
+
+	vector<pair<function<int()>, string>> ops {
+		{ op_1, "OP1 - SavePoint creation in HG '" + to_string(TG_HG_1) + "'" },
+		{ op_2, "OP2 - Trx creation with 'autocommit=0' + INSERT in HG '" + to_string(TG_HG_2) + "'" },
+		{ op_3, "OP3 - Unknown transaction status with failing 'INSERT' in HG '" + to_string(DF_HG) + "'" },
+	};
+
+	vector<vector<uint32_t>> permutations { get_permutations(vector<uint32_t> {1,2,3}) };
+
+	for (const auto& p : permutations) {
+		fprintf(stderr, "\n");
+		const string p_str {
+			std::accumulate(p.begin(), p.end(), std::string(),
+				[](const std::string& str, const uint32_t& n) -> std::string {
+					return str + (str.length() > 0 ? "," : "") + std::to_string(n);
+				}
+			)
+		};
+
+		diag("Executing test permutation '%s'", p_str.c_str());
+		ok(op_0() == EXIT_SUCCESS, "Fetching stats and setup operation succeeded");
+
+		const auto& p_op_1 { ops[p[0] - 1] };
+		const auto& p_op_2 { ops[p[1] - 1] };
+		const auto& p_op_3 { ops[p[2] - 1] };
+
+		diag("Executing operation - %s", p_op_1.second.c_str());
+		ok(p_op_1.first() == EXIT_SUCCESS, "Operation should exit successfully") ;
+
+		diag("Executing operation - %s", p_op_2.second.c_str());
+		ok(p_op_2.first() == EXIT_SUCCESS, "Operation should exit successfully") ;
+
+		diag("Executing operation - %s", p_op_3.second.c_str());
+		ok(p_op_3.first() == EXIT_SUCCESS, "Operation should exit successfully") ;
+
+		ok(op_4() == EXIT_SUCCESS, "Final '%s' commands executed in correct hgs", trx_cmd.c_str());
+	}
+
+	mysql_close(sbtest);
+
+	return EXIT_SUCCESS;
+}
+
+/**
+ * @details This test involves the three different logics for trx detection. In a non-persistent session:
+ *  - Savepoint creation in TG_HG_1
+ *    + Check that 'SAVEPOINT' is detected in the conn
+ *  - Transaction started for TG_HG_2
+ *    + Check that transaction is detected query properly routed
+ *  - Error on DF_HG
+ *    + Check that error provoques retaining of the conn due to unknown status.
+ *  - Three 'COMMIT|ROLLBACK' are issued
+ *    + Check that each command is issued in the correct conn
+ *
+ *  For the final 'COMMIT|ROLLBACK' commands it's expected that:
+ *   * First two commands hit either 'SAVEPOINT' or known trx conn.
+ *   * Third command hits the conn with 'unknown trx' status.
+ */
+int implicit_trx_and_savepoints_no_persist_no_def_hg(
+	CommandLine& cl, MYSQL* admin, MYSQL*, const string& trx_cmd
+) {
+	MYSQL* sbtest = mysql_init(NULL);
+
+	if (!mysql_real_connect(sbtest, cl.host, "sbtest1", "sbtest1", NULL, cl.port, NULL, 0)) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(sbtest));
+		return EXIT_FAILURE;
+	}
+
+	diag("Initial insert to ensure that 'id=1' is taken in the table");
+	MYSQL_QUERY_T(
+		sbtest, "/* TG_HG_1 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')"
+	);
+
+	const vector<uint32_t> tg_hgs { DF_HG, TG_HG_1, TG_HG_2 };
+	const pair<int,pool_state_t> pre_pool_state_res { fetch_conn_stats(admin, tg_hgs) };
+	if (pre_pool_state_res.first) { return EXIT_FAILURE; }
+
+	const pool_state_t& pre_pool_state { pre_pool_state_res.second };
+	const uint32_t df_hg_qs = std::stol(pre_pool_state.at(DF_HG)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_1_qs = std::stol(pre_pool_state.at(TG_HG_1)[POOL_STATS_IDX::QUERIES]);
+	const uint32_t tg_hg_2_qs = std::stol(pre_pool_state.at(TG_HG_2)[POOL_STATS_IDX::QUERIES]);
+
+	diag("Checking that a trx wasn't started for previous query on hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+
+	MYSQL_QUERY_T(sbtest, "SET autocommit=0");
+	check_conn_count(admin, "ConnUsed", 0, TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs, TG_HG_1);
+
+	MYSQL_QUERY_T(sbtest, ("/* hostgroup=" + to_string(TG_HG_1) + " */ SAVEPOINT s1").c_str());
+
+	diag("Queries should have been issued to hg '%d'", TG_HG_1);
+	check_query_count(admin, tg_hg_1_qs + 1, TG_HG_1);
+
+	diag("Checking that conn is kept ('has_savepoint') due to previous query on hg '%d'", TG_HG_1);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_1);
+
+	json j_session = fetch_internal_session(sbtest);
+	bool has_savepoint = false;
+	int trxs = -1;
+
+	try {
+		has_savepoint = j_session["backends"][0]["conn"]["status"]["has_savepoint"];
+		trxs = j_session["active_transactions"];
+	} catch (std::exception& e) {
+		diag("Accessing 'INTERNAL SESSION' fields failed with error - %s", e.what());
+	}
+
+	ok(
+		has_savepoint == true && trxs == 0,
+		"Savepoint should be present, but no trxs due to MySQL bug #107875 - savepoint: %d, trxs: %d",
+		has_savepoint, trxs
+	);
+
+	MYSQL_QUERY_T(sbtest, "/* TG_HG_2 */ INSERT INTO test.commit_rollback (k,c,p) VALUES (1,'foo','bar')");
+	check_query_count(admin, tg_hg_2_qs + 1, TG_HG_2);
+	check_conn_count(admin, "ConnUsed", 1, TG_HG_2);
+
+	MYSQL_QUERY_T(sbtest, "SET autocommit=1");
+	int rc = mysql_query_t(sbtest, "INSERT INTO test.commit_rollback (id,k,c,p) VALUES (1,1,'foo','bar')");
+	int err_code = mysql_errno(sbtest);
+	ok(rc != 0 && err_code == 1062, "Insert should failed - exp_err: 1062, act_err: %d", err_code);
+
+	diag("Checking the two previous queries were issued - AUTOCOMMIT + INSERT");
+	check_query_count(admin, df_hg_qs + 2, DF_HG);
+	check_conn_count(admin, "ConnUsed", 1, DF_HG);
+	MYSQL_QUERY_T(sbtest, "SET autocommit=0");
+
+	MYSQL_QUERY_T(sbtest, trx_cmd.c_str());
+	check_conn_count(admin, "ConnUsed", 2);
+	check_conn_count(admin, "ConnUsed", 1, DF_HG);
+
+	MYSQL_QUERY_T(sbtest, trx_cmd.c_str());
+	check_conn_count(admin, "ConnUsed", 1);
+	check_conn_count(admin, "ConnUsed", 1, DF_HG);
+
+	MYSQL_QUERY_T(sbtest, trx_cmd.c_str());
+	check_conn_count(admin, "ConnUsed", 0);
+	check_conn_count(admin, "ConnUsed", 0, DF_HG);
+
+	mysql_close(sbtest);
+
+	return EXIT_SUCCESS;
+}
+
+int implicit_trx_and_savepoints_no_persist_no_def_hg_c(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return implicit_trx_and_savepoints_no_persist_no_def_hg_(cl, admin, nullptr, "COMMIT");
+}
+
+int implicit_trx_and_savepoints_no_persist_no_def_hg_r(CommandLine& cl, MYSQL* admin, MYSQL*) {
+	return implicit_trx_and_savepoints_no_persist_no_def_hg(cl, admin, nullptr, "ROLLBACK");
+}
+
+struct test_case_t {
+	string name;
+	function<int(CommandLine&,MYSQL*,MYSQL*)> fn;
+};
+
+#define create_test_case(name) { #name, name }
+
+const vector<test_case_t> test_cases {
+	create_test_case(explicit_trx_persist_c),
+	create_test_case(explicit_trx_persist_r),
+	create_test_case(explicit_trx_persist_2_c),
+	create_test_case(explicit_trx_persist_2_r),
+	create_test_case(implicit_trx_persist_c),
+	create_test_case(implicit_trx_persist_r),
+	create_test_case(explicit_trx_persist_no_def_hg_c),
+	create_test_case(explicit_trx_persist_no_def_hg_r),
+	create_test_case(implicit_trx_persist_no_def_hg_c),
+	create_test_case(implicit_trx_persist_no_def_hg_r),
+	create_test_case(explicit_trx_no_persist_c),
+	create_test_case(explicit_trx_no_persist_r),
+	create_test_case(explicit_trx_no_persist_2_c),
+	create_test_case(explicit_trx_no_persist_2_r),
+	create_test_case(explicit_trx_no_persist_no_def_hg_c),
+	create_test_case(explicit_trx_no_persist_no_def_hg_r),
+	create_test_case(implicit_trx_no_persist_no_def_hg_c),
+	create_test_case(implicit_trx_no_persist_no_def_hg_r),
+	create_test_case(explicit_unknown_trx_persist_no_def_hg_c),
+	create_test_case(explicit_unknown_trx_persist_no_def_hg_r),
+	create_test_case(explicit_and_unknown_trx_no_persist_no_def_hg_c),
+	create_test_case(explicit_and_unknown_trx_no_persist_no_def_hg_r),
+	create_test_case(implicit_and_unknown_trx_no_persist_no_def_hg_c),
+	create_test_case(implicit_and_unknown_trx_no_persist_no_def_hg_r),
+	create_test_case(implicit_trx_and_savepoints_no_persist_no_def_hg_c),
+	create_test_case(implicit_trx_and_savepoints_no_persist_no_def_hg_r),
+};
+
+int prepare_tables_and_config(MYSQL* admin, MYSQL* proxy) {
+	MYSQL_QUERY_T(proxy, "CREATE DATABASE IF NOT EXISTS test");
+	MYSQL_QUERY_T(proxy, "DROP TABLE IF EXISTS test.commit_rollback");
+	MYSQL_QUERY_T(proxy,
+		"CREATE TABLE test.commit_rollback ("
+			" id INTEGER NOT NULL AUTO_INCREMENT, "
+			" k INTEGER DEFAULT 0 NOT NULL,"
+			" c CHAR(120) DEFAULT '' NOT NULL,"
+			" p CHAR(60) DEFAULT '' NOT NULL,"
+			" PRIMARY KEY (id)"
+		")"
+	);
+
+	const auto build_server_copy_query = [] (uint32_t tg_hg, uint32_t og_hg) {
+		return cstr_format(
+			"INSERT INTO mysql_servers (hostgroup_id,hostname,port)"
+				" SELECT %d,hostname,port FROM mysql_servers WHERE hostgroup_id=%d",
+			tg_hg, og_hg
+		).str;
+	};
+
+	MYSQL_QUERY_T(admin, ("DELETE FROM mysql_servers WHERE hostgroup_id=" + to_string(TG_HG_1)).c_str());
+	MYSQL_QUERY_T(admin, ("DELETE FROM mysql_servers WHERE hostgroup_id=" + to_string(TG_HG_2)).c_str());
+
+	MYSQL_QUERY_T(admin, build_server_copy_query(TG_HG_1, DF_HG).c_str());
+	MYSQL_QUERY_T(admin, build_server_copy_query(TG_HG_2, DF_HG).c_str());
+	MYSQL_QUERY_T(admin, "LOAD MYSQL SERVERS TO RUNTIME");
+
+	MYSQL_QUERY_T(admin, "SET mysql-auto_increment_delay_multiplex=0");
+	MYSQL_QUERY_T(admin, "LOAD MYSQL VARIABLES TO RUNTIME");
+
+	MYSQL_QUERY_T(admin, "LOAD MYSQL QUERY RULES FROM DISK");
+	MYSQL_QUERY_T(admin, "LOAD MYSQL QUERY RULES TO RUNTIME");
+	MYSQL_QUERY_T(
+		admin,
+		string {
+			"INSERT INTO mysql_query_rules (active,match_pattern,destination_hostgroup,apply) VALUES"
+				" (1,'/\\* TG_HG_1 \\*/ INSERT INTO .*'," + to_string(TG_HG_1) + ",1)"
+		}.c_str()
+	);
+	MYSQL_QUERY_T(
+		admin,
+		string {
+			"INSERT INTO mysql_query_rules (active,match_pattern,destination_hostgroup,apply) VALUES"
+				" (1,'/\\* TG_HG_2 \\*/ INSERT INTO .*'," + to_string(TG_HG_2) + ",1)"
+		}.c_str()
+	);
+	MYSQL_QUERY_T(admin, "LOAD MYSQL QUERY RULES TO RUNTIME");
+
+	MYSQL_QUERY_T(admin, "UPDATE mysql_users SET transaction_persistent=0 WHERE username='sbtest1'");
+	MYSQL_QUERY_T(admin, "LOAD MYSQL USERS TO RUNTIME");
+
+	return EXIT_SUCCESS;
+}
+
+int main(int argc, char** argv) {
+	CommandLine cl;
+
+	plan(313);
+
+	if (cl.getEnv()) {
+		diag("Failed to get the required environmental variables.");
+		return EXIT_FAILURE;
+	}
+
+	MYSQL* proxy = mysql_init(NULL);
+	if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) {
+		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy));
+		return EXIT_FAILURE;
+	}
+
+	MYSQL* admin = mysql_init(NULL);
+	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 EXIT_FAILURE;
+	}
+
+	int prep_res = prepare_tables_and_config(admin, proxy);
+	if (prep_res) {
+		goto cleanup;
+	}
+
+	for (const auto test : test_cases) {
+		fprintf(stderr, "\n");
+		diag("Starting test '%s'", test.name.c_str());
+		test.fn(cl, admin, proxy);
+	}
+
+cleanup:
+
+	mysql_close(proxy);
+	mysql_close(admin);
+
+	return exit_status();
+}
diff --git a/test/tap/tests/test_auto_increment_delay_multiplex-t.cpp b/test/tap/tests/test_auto_increment_delay_multiplex-t.cpp
index a6b321bec5..1ffd35e0c8 100644
--- a/test/tap/tests/test_auto_increment_delay_multiplex-t.cpp
+++ b/test/tap/tests/test_auto_increment_delay_multiplex-t.cpp
@@ -82,20 +82,10 @@ int get_query_result(MYSQL* mysql, const string& query, uint64_t& out_val) {
 #define log_err(err_msg) fprintf(stderr, "File %s, line %d, Error: \"%s\"\n", __FILE__, __LINE__, err_msg);
 
 int get_conn_auto_inc_delay_token(MYSQL* proxy_mysql, int& out_auto_inc_delay) {
-	MYSQL_QUERY(proxy_mysql, "PROXYSQL INTERNAL SESSION");
-	MYSQL_RES* my_res = mysql_store_result(proxy_mysql);
-	vector<mysql_res_row> int_sess_res = extract_mysql_rows(my_res);
-	mysql_free_result(my_res);
-
 	int cur_auto_inc_delay_mult = 0;
 
-	if (int_sess_res.empty()) {
-		log_err("Empty result received from 'PROXYSQL INTERNAL SESSION'");
-		return EXIT_FAILURE;
-	}
-
 	try {
-		nlohmann::json j_int_sess = nlohmann::json::parse(int_sess_res[0][0]);
+		nlohmann::json j_int_sess = fetch_internal_session(proxy_mysql);
 		nlohmann::json backend_conns = j_int_sess.at("backends");
 		nlohmann::json m_off_conn {};
 
@@ -124,20 +114,8 @@ int get_conn_auto_inc_delay_token(MYSQL* proxy_mysql, int& out_auto_inc_delay) {
 }
 
 int get_session_backends(MYSQL* proxy_mysql,vector<json>& out_backend_conns) {
-	MYSQL_QUERY(proxy_mysql, "PROXYSQL INTERNAL SESSION");
-	MYSQL_RES* my_res = mysql_store_result(proxy_mysql);
-	vector<mysql_res_row> int_sess_res = extract_mysql_rows(my_res);
-	mysql_free_result(my_res);
-
-	int cur_auto_inc_delay_mult = 0;
-
-	if (int_sess_res.empty()) {
-		log_err("Empty result received from 'PROXYSQL INTERNAL SESSION'");
-		return EXIT_FAILURE;
-	}
-
 	try {
-		nlohmann::json j_int_sess = nlohmann::json::parse(int_sess_res[0][0]);
+		nlohmann::json j_int_sess = fetch_internal_session(proxy_mysql);
 		nlohmann::json backend_conns = j_int_sess.at("backends");
 		vector<json> _out_conns {};
 
diff --git a/test/tap/tests/test_default_value_transaction_isolation_attr-t.cpp b/test/tap/tests/test_default_value_transaction_isolation_attr-t.cpp
index 342205bcbe..a84fe833f6 100644
--- a/test/tap/tests/test_default_value_transaction_isolation_attr-t.cpp
+++ b/test/tap/tests/test_default_value_transaction_isolation_attr-t.cpp
@@ -33,23 +33,6 @@
 using std::string;
 using namespace nlohmann;
 
-/**
- * @brief Helper function to convert a 'MYSQL_RES' into a
- *   nlohmann::json.
- *
- * @param result The 'MYSQL_RES*' to be converted into JSON.
- * @param j 'nlohmann::json' output parameter holding the
- *   converted 'MYSQL_RES' supplied.
- */
-void parse_result_json_column(MYSQL_RES *result, json& j) {
-	if(!result) return;
-	MYSQL_ROW row;
-
-	while ((row = mysql_fetch_row(result))) {
-		j = json::parse(row[0]);
-	}
-}
-
 using user_attributes = std::tuple<std::string, std::string, std::string, std::string>;
 
 /**
@@ -87,11 +70,7 @@ int check_front_conn_isolation_level(
 	const std::string& exp_iso_level,
 	const bool set_via_attr
 ) {
-	MYSQL_QUERY(proxysql_mysql, "PROXYSQL INTERNAL SESSION");
-	json j_status {};
-	MYSQL_RES* int_session_res = mysql_store_result(proxysql_mysql);
-	parse_result_json_column(int_session_res, j_status);
-	mysql_free_result(int_session_res);
+	json j_status = fetch_internal_session(proxysql_mysql);
 
 	try {
 		std::string front_conn_isolation_level =
@@ -145,6 +124,7 @@ int check_backend_conn_isolation_level(
 	// Verify that the query produced a correct result
 	if (trx_iso_row && trx_iso_row[0]) {
 		trx_iso_val = std::string { trx_iso_row[0] };
+		mysql_free_result(trx_iso_res);
 	} else {
 		const std::string err_msg {
 			"Empty result received from query '" + select_trx_iso_query + "'"
@@ -335,5 +315,8 @@ int main(int argc, char** argv) {
 		mysql_close(proxysql_mysql);
 	}
 
+	mysql_close(proxysql_admin);
+	mysql_close(mysql_server);
+
 	return exit_status();
 }
diff --git a/test/tap/tests/test_keep_multiplexing_variables-t.cpp b/test/tap/tests/test_keep_multiplexing_variables-t.cpp
index 2ed9285493..ef4995bc91 100644
--- a/test/tap/tests/test_keep_multiplexing_variables-t.cpp
+++ b/test/tap/tests/test_keep_multiplexing_variables-t.cpp
@@ -18,15 +18,6 @@
 using std::string;
 using namespace nlohmann;
 
-void parse_result_json_column(MYSQL_RES *result, json& j) {
-	if(!result) return;
-	MYSQL_ROW row;
-
-	while ((row = mysql_fetch_row(result))) {
-		j = json::parse(row[0]);
-	}
-}
-
 std::vector<std::string> select_queries {
 	"select @@session.autocommit,         @@session.big_tables, @@autocommit,@@bulk_insert_buffer_size,     @@character_set_database,@@transaction_isolation,    @@version,@@session.transaction_isolation",
 	"select  @@autocommit, @@sql_mode,        @@big_tables,    @@autocommit,@@bulk_insert_buffer_size,     @@character_set_database,@@session.transaction_isolation,    @@version,@@transaction_isolation",
@@ -53,11 +44,7 @@ int check_multiplexing_disabled(const CommandLine& cl, const std::string query,
 	MYSQL_RES* dummy_res = mysql_store_result(proxysql_mysql);
 	mysql_free_result(dummy_res);
 
-	MYSQL_QUERY(proxysql_mysql, "PROXYSQL INTERNAL SESSION");
-	json j_status {};
-	MYSQL_RES* int_session_res = mysql_store_result(proxysql_mysql);
-	parse_result_json_column(int_session_res, j_status);
-	mysql_free_result(int_session_res);
+	json j_status = fetch_internal_session(proxysql_mysql);
 
 	if (j_status.contains("backends")) {
 		for (auto& backend : j_status["backends"]) {