From 69acd4b43191a4c5b927c746121b7fefff9fa93a Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Wed, 18 Feb 2026 09:35:44 +0000 Subject: [PATCH] Implement FFTO TAP testing suite - Added comprehensive MySQL FFTO test (text and binary protocols) - Added comprehensive PgSQL FFTO test (Simple and Extended Query protocols) - Added FFTO memory bypass logic test - Added bash orchestration scripts for each test group - Integrated new tests into the TAP Makefile --- test/tap/tests/Makefile | 5 +- test/tap/tests/ffto_bypass/run.sh | 12 +++ test/tap/tests/ffto_mysql/run.sh | 17 ++++ test/tap/tests/ffto_pgsql/run.sh | 12 +++ test/tap/tests/test_ffto_bypass-t.cpp | 60 ++++++++++++++ test/tap/tests/test_ffto_mysql-t.cpp | 112 ++++++++++++++++++++++++++ test/tap/tests/test_ffto_pgsql-t.cpp | 100 +++++++++++++++++++++++ 7 files changed, 317 insertions(+), 1 deletion(-) create mode 100755 test/tap/tests/ffto_bypass/run.sh create mode 100755 test/tap/tests/ffto_mysql/run.sh create mode 100755 test/tap/tests/ffto_pgsql/run.sh create mode 100644 test/tap/tests/test_ffto_bypass-t.cpp create mode 100644 test/tap/tests/test_ffto_mysql-t.cpp create mode 100644 test/tap/tests/test_ffto_pgsql-t.cpp diff --git a/test/tap/tests/Makefile b/test/tap/tests/Makefile index 6bb5ff810..8782b8e2d 100644 --- a/test/tap/tests/Makefile +++ b/test/tap/tests/Makefile @@ -129,7 +129,10 @@ tests: tests-cpp \ fast_forward_grace_close_libmysql-t \ fast_forward_switch_replication_deprecate_eof_libmysql-t \ reg_test_mariadb_stmt_store_result_libmysql-t \ - reg_test_mariadb_stmt_store_result_async-t + reg_test_mariadb_stmt_store_result_async-t \ + test_ffto_mysql-t \ + test_ffto_pgsql-t \ + test_ffto_bypass-t tests: @echo "Removing empty .gcno files ..." find -L . -type f -name '*.gcno' -empty -ls -delete diff --git a/test/tap/tests/ffto_bypass/run.sh b/test/tap/tests/ffto_bypass/run.sh new file mode 100755 index 000000000..302fec861 --- /dev/null +++ b/test/tap/tests/ffto_bypass/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# test/tap/tests/ffto_bypass/run.sh + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +TAP_BINARY="${SCRIPT_DIR}/../test_ffto_bypass-t" + +if [ ! -f "$TAP_BINARY" ]; then + echo "Error: TAP binary $TAP_BINARY not found. Build it first with 'make test_ffto_bypass-t'." + exit 1 +fi + +"$TAP_BINARY" diff --git a/test/tap/tests/ffto_mysql/run.sh b/test/tap/tests/ffto_mysql/run.sh new file mode 100755 index 000000000..7635a9c06 --- /dev/null +++ b/test/tap/tests/ffto_mysql/run.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# test/tap/tests/ffto_mysql/run.sh + +# This script orchestrates the MySQL FFTO tests. +# It assumes ProxySQL is built and available. + +# Path to the TAP test binary +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +TAP_BINARY="${SCRIPT_DIR}/../test_ffto_mysql-t" + +if [ ! -f "$TAP_BINARY" ]; then + echo "Error: TAP binary $TAP_BINARY not found. Build it first with 'make test_ffto_mysql-t'." + exit 1 +fi + +# Run the test +"$TAP_BINARY" diff --git a/test/tap/tests/ffto_pgsql/run.sh b/test/tap/tests/ffto_pgsql/run.sh new file mode 100755 index 000000000..af3633314 --- /dev/null +++ b/test/tap/tests/ffto_pgsql/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# test/tap/tests/ffto_pgsql/run.sh + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +TAP_BINARY="${SCRIPT_DIR}/../test_ffto_pgsql-t" + +if [ ! -f "$TAP_BINARY" ]; then + echo "Error: TAP binary $TAP_BINARY not found. Build it first with 'make test_ffto_pgsql-t'." + exit 1 +fi + +"$TAP_BINARY" diff --git a/test/tap/tests/test_ffto_bypass-t.cpp b/test/tap/tests/test_ffto_bypass-t.cpp new file mode 100644 index 000000000..f1ff19ca1 --- /dev/null +++ b/test/tap/tests/test_ffto_bypass-t.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include "mysql.h" +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +int main(int argc, char** argv) { + CommandLine cl; + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return -1; + } + + plan(2); + + MYSQL* admin = mysql_init(NULL); + if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + diag("Admin connection failed"); + return -1; + } + + // Set a very small threshold: 100 bytes + MYSQL_QUERY(admin, "UPDATE global_variables SET variable_value='true' WHERE variable_name='mysql-ffto_enabled'"); + MYSQL_QUERY(admin, "UPDATE global_variables SET variable_value='100' WHERE variable_name='mysql-ffto_max_buffer_size'"); + MYSQL_QUERY(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + MYSQL_QUERY(admin, "UPDATE mysql_users SET fast_forward=1"); + MYSQL_QUERY(admin, "LOAD MYSQL USERS TO RUNTIME"); + MYSQL_QUERY(admin, "DELETE FROM stats_mysql_query_digest"); + + MYSQL* conn = mysql_init(NULL); + if (!mysql_real_connect(conn, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + diag("Client connection failed"); + return -1; + } + ok(conn != NULL, "Connected to ProxySQL"); + + // Send a query larger than 100 bytes + std::string large_query = "SELECT '"; + for(int i=0; i<200; i++) large_query += "x"; + large_query += "'"; + + mysql_query(conn, large_query.c_str()); + + // Verify that NO digest was recorded for this query because it was bypassed + int rc = run_q(admin, "SELECT count(*) FROM stats_mysql_query_digest WHERE digest_text LIKE '%xxxx%'"); + MYSQL_RES* res = mysql_store_result(admin); + MYSQL_ROW row = mysql_fetch_row(res); + int count = atoi(row[0]); + ok(count == 0, "Query larger than threshold was correctly bypassed (count: %d)", count); + + mysql_free_result(res); + mysql_close(conn); + mysql_close(admin); + + return exit_status(); +} diff --git a/test/tap/tests/test_ffto_mysql-t.cpp b/test/tap/tests/test_ffto_mysql-t.cpp new file mode 100644 index 000000000..46fe0d0ff --- /dev/null +++ b/test/tap/tests/test_ffto_mysql-t.cpp @@ -0,0 +1,112 @@ +#include +#include +#include +#include +#include +#include "mysql.h" +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +void verify_digest(MYSQL* admin, const char* template_text, int expected_count) { + char query[1024]; + sprintf(query, "SELECT count_star FROM stats_mysql_query_digest WHERE digest_text LIKE '%%%s%%'", template_text); + int rc = run_q(admin, query); + if (rc != 0) { + ok(0, "Failed to query stats_mysql_query_digest"); + return; + } + MYSQL_RES* res = mysql_store_result(admin); + MYSQL_ROW row = mysql_fetch_row(res); + if (row) { + int count = atoi(row[0]); + ok(count >= expected_count, "Found digest: %s (count: %d, expected: %d)", template_text, count, expected_count); + } else { + ok(0, "Digest NOT found: %s", template_text); + } + mysql_free_result(res); +} + +int main(int argc, char** argv) { + CommandLine cl; + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return -1; + } + + // We plan for: + // 1. Connection setup + // 2. Text CRUD (6 queries) + // 3. Binary Prepared Stmts (2 templates) + // 4. Cleanup + plan(10); + + MYSQL* admin = mysql_init(NULL); + if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + diag("Admin connection failed"); + return -1; + } + + // Configure FFTO and Fast Forward + MYSQL_QUERY(admin, "UPDATE global_variables SET variable_value='true' WHERE variable_name='mysql-ffto_enabled'"); + MYSQL_QUERY(admin, "UPDATE global_variables SET variable_value='1048576' WHERE variable_name='mysql-ffto_max_buffer_size'"); + MYSQL_QUERY(admin, "LOAD MYSQL VARIABLES TO RUNTIME"); + MYSQL_QUERY(admin, "UPDATE mysql_users SET fast_forward=1"); + MYSQL_QUERY(admin, "LOAD MYSQL USERS TO RUNTIME"); + MYSQL_QUERY(admin, "DELETE FROM stats_mysql_query_digest"); // Reset stats + + MYSQL* conn = mysql_init(NULL); + if (!mysql_real_connect(conn, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + diag("Client connection failed"); + return -1; + } + ok(conn != NULL, "Connected to ProxySQL in Fast Forward mode"); + + // --- Part 1: Text Protocol CRUD --- + mysql_query(conn, "DROP TABLE IF EXISTS ffto_test"); + mysql_query(conn, "CREATE TABLE ffto_test (id INT PRIMARY KEY, val VARCHAR(255))"); + mysql_query(conn, "INSERT INTO ffto_test VALUES (1, 'val1'), (2, 'val2')"); + mysql_query(conn, "UPDATE ffto_test SET val = 'updated' WHERE id = 1"); + mysql_query(conn, "SELECT val FROM ffto_test WHERE id = 1"); + mysql_query(conn, "DELETE FROM ffto_test WHERE id = 2"); + + // Verify Text Stats + verify_digest(admin, "DROP TABLE IF EXISTS ffto_test", 1); + verify_digest(admin, "CREATE TABLE ffto_test", 1); + verify_digest(admin, "INSERT INTO ffto_test VALUES", 1); + verify_digest(admin, "UPDATE ffto_test SET val", 1); + verify_digest(admin, "SELECT val FROM ffto_test WHERE id", 1); + verify_digest(admin, "DELETE FROM ffto_test WHERE id", 1); + + // --- Part 2: Binary Protocol (Prepared Statements) --- + MYSQL_STMT *stmt = mysql_stmt_init(conn); + const char* ins_query = "INSERT INTO ffto_test (id, val) VALUES (?, ?)"; + mysql_stmt_prepare(stmt, ins_query, strlen(ins_query)); + + MYSQL_BIND bind[2]; + int int_data = 10; + char str_data[20] = "binary_val"; + unsigned long str_len = strlen(str_data); + + memset(bind, 0, sizeof(bind)); + bind[0].buffer_type = MYSQL_TYPE_LONG; + bind[0].buffer = (char *)&int_data; + bind[1].buffer_type = MYSQL_TYPE_STRING; + bind[1].buffer = (char *)str_data; + bind[1].buffer_length = 20; + bind[1].length = &str_len; + + mysql_stmt_bind_param(stmt, bind); + mysql_stmt_execute(stmt); // Run once + mysql_stmt_execute(stmt); // Run twice to check count_star + + // Verify Binary Stats + verify_digest(admin, "INSERT INTO ffto_test (id, val) VALUES (?, ?)", 2); + + mysql_stmt_close(stmt); + + mysql_close(conn); + mysql_close(admin); + + return exit_status(); +} diff --git a/test/tap/tests/test_ffto_pgsql-t.cpp b/test/tap/tests/test_ffto_pgsql-t.cpp new file mode 100644 index 000000000..b793a9ab7 --- /dev/null +++ b/test/tap/tests/test_ffto_pgsql-t.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include +#include +#include "libpq-fe.h" +#include "command_line.h" +#include "tap.h" +#include "utils.h" +#include "mysql.h" + +CommandLine cl; + +void verify_pg_digest(MYSQL* admin, const char* template_text, int expected_count) { + char query[1024]; + sprintf(query, "SELECT count_star FROM stats_pgsql_query_digest WHERE digest_text LIKE '%%%s%%'", template_text); + int rc = run_q(admin, query); + if (rc != 0) { + ok(0, "Failed to query stats_pgsql_query_digest"); + return; + } + MYSQL_RES* res = mysql_store_result(admin); + MYSQL_ROW row = mysql_fetch_row(res); + if (row) { + int count = atoi(row[0]); + ok(count >= expected_count, "Found PG digest: %s (count: %d, expected: %d)", template_text, count, expected_count); + } else { + ok(0, "PG Digest NOT found: %s", template_text); + } + mysql_free_result(res); +} + +int main(int argc, char** argv) { + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return -1; + } + + // Plan: + // 1. Connection setup + // 2. Simple CRUD (4 queries) + // 3. Extended Query (1 template) + plan(6); + + MYSQL* admin = mysql_init(NULL); + if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + diag("Admin connection failed"); + return -1; + } + + // Configure FFTO and Fast Forward for PG + MYSQL_QUERY(admin, "UPDATE global_variables SET variable_value='true' WHERE variable_name='pgsql-ffto_enabled'"); + MYSQL_QUERY(admin, "LOAD PGSQL VARIABLES TO RUNTIME"); + MYSQL_QUERY(admin, "UPDATE pgsql_users SET fast_forward=1"); + MYSQL_QUERY(admin, "LOAD PGSQL USERS TO RUNTIME"); + MYSQL_QUERY(admin, "DELETE FROM stats_pgsql_query_digest"); + + // Standard libpq connection + char conninfo[1024]; + sprintf(conninfo, "host=%s port=%d user=%s password=%s dbname=postgres sslmode=disable", + cl.pgsql_host, cl.pgsql_port, cl.pgsql_username, cl.pgsql_password); + + PGconn* conn = PQconnectdb(conninfo); + if (PQstatus(conn) != CONNECTION_OK) { + diag("PG Connection failed: %s", PQerrorMessage(conn)); + return -1; + } + ok(conn != NULL, "Connected to PostgreSQL via ProxySQL"); + + // --- Part 1: Simple Query Protocol --- + PQclear(PQexec(conn, "DROP TABLE IF EXISTS ffto_pg_test")); + PQclear(PQexec(conn, "CREATE TABLE ffto_pg_test (id INT PRIMARY KEY, data TEXT)")); + PQclear(PQexec(conn, "INSERT INTO ffto_pg_test VALUES (1, 'val1')")); + PQclear(PQexec(conn, "SELECT data FROM ffto_pg_test WHERE id = 1")); + + verify_pg_digest(admin, "DROP TABLE IF EXISTS ffto_pg_test", 1); + verify_pg_digest(admin, "CREATE TABLE ffto_pg_test", 1); + verify_pg_digest(admin, "INSERT INTO ffto_pg_test VALUES", 1); + verify_pg_digest(admin, "SELECT data FROM ffto_pg_test", 1); + + // --- Part 2: Extended Query Protocol --- + const char* ext_query = "SELECT data FROM ffto_pg_test WHERE id = $1"; + PGresult* res = PQprepare(conn, "stmt1", ext_query, 1, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) { + diag("PQprepare failed: %s", PQerrorMessage(conn)); + } + PQclear(res); + + const char* paramValues[1] = {"1"}; + res = PQexecPrepared(conn, "stmt1", 1, paramValues, NULL, NULL, 0); + PQclear(res); + + verify_pg_digest(admin, "SELECT data FROM ffto_pg_test WHERE id = $1", 1); + + PQfinish(conn); + mysql_close(admin); + + return exit_status(); +}