feat(plugin-abi): Step 2.1 — pre-execution query-hook ABI + dispatch

First half of Step 2 from the GenAI plugin carve-out design (see
docs/superpowers/specs/2026-04-16-genai-plugin-carveout-design.md).
Adds the query-hook ABI surface and the manager-level dispatch, but
NOT yet the hot-path call sites in MySQL_Session / PgSQL_Session --
those are intentionally split into the next commit so the hot-path
diff stays small and reviewable on its own.

ABI extension (include/ProxySQL_Plugin.h):
- ProxySQL_PluginProtocol enum {mysql, pgsql}
- ProxySQL_PluginQueryHookPayload {user, client_ip, schema, query_text, query_len}
- ProxySQL_PluginQueryHookAction enum {allow, deny}
- ProxySQL_PluginQueryHookResult {action, message}
- proxysql_plugin_query_hook_cb typedef
- proxysql_plugin_register_query_hook_cb typedef
- new field on ProxySQL_PluginServices: register_query_hook
  (additive at the end of the struct -- older plugins that were built
   against the previous layout don't read past it; new plugins must
   check non-null before calling, same convention used elsewhere)

Manager (lib/ProxySQL_PluginManager.cpp + .h):
- One hook per protocol per manager (nullptr means "no hook").
- register_query_hook(proto, cb): rejects null cb; rejects duplicate
  registration for a protocol that already has a hook; surfaces
  registration errors through the same note_registration_failure /
  init-aborts path that register_table and register_command use.
- dispatch_query_hook(proto, payload, result): synchronous,
  returns true if a hook fired and stored its result, false otherwise.
- has_query_hook(proto): cheap predicate for the hot-path predicate.

Global helpers (for the hot path, next commit):
- proxysql_dispatch_configured_plugin_query_hook(proto, payload, result):
  takes the active-manager mutex, dispatches if there is one.
- proxysql_has_configured_plugin_query_hook(proto): lock-free atomic
  read of the manager pointer + a pointer-sized field read.  Designed
  to be the gate the hot path checks first; the dispatch call can be
  elided entirely on the no-plugin path.  Documented as
  spurious-true-tolerant -- the dispatch helper re-checks under the
  lock.

Test plugin extension (test/tap/test_helpers/fake_plugin.cpp):
- New env vars PROXYSQL_FAKE_PLUGIN[2]_REGISTER_QUERY_HOOK,
  REGISTER_QUERY_HOOK_PROTO ("mysql"|"pgsql"), HOOK_DENY.
- fake_query_hook echoes the SQL through the result message so tests
  can verify the payload reached the callback intact.

New unit test: plugin_query_hook_unit-t (41 assertions)
- empty manager: neither protocol has a hook, dispatch returns false,
  result struct untouched
- register MySQL allow hook → dispatch returns ALLOW with empty msg
- register MySQL deny hook → dispatch returns DENY with the message
- null callback rejected; duplicate registration rejected;
  original hook still in effect after rejected duplicate
- protocols are independent (mysql allow + pgsql deny coexist)
- payload threaded through (echo hook reads user/ip/schema/sql)
- global dispatcher: false when no active manager, untouched result
- global dispatcher with active manager: routes to the fake plugin's
  hook, propagates ALLOW vs DENY (env-flipped between calls), and
  goes back to false after stop_configured_plugins

All 59 unit-test binaries pass.
ProtocolX
Rene Cannao 1 month ago
parent 3d107c3bed
commit 55556979e0

@ -58,6 +58,56 @@ using proxysql_plugin_db_handle_cb =
using proxysql_plugin_log_message_cb =
void (*)(int, const char *);
// Pre-execution query hook (Step 2 ABI extension).
//
// Wire protocol the hook is being invoked for. A plugin can register
// independently for each protocol; one hook per protocol per plugin.
enum class ProxySQL_PluginProtocol : uint8_t {
mysql = 0,
pgsql = 1
};
// Payload handed to a query-hook callback. All pointers are owned by
// core and remain valid only for the duration of the callback. The
// callback must not retain them or mutate the underlying buffers.
//
// query_text is the SQL the client sent, NOT NUL-terminated; query_len
// is its length in bytes. user / client_ip / schema are NUL-terminated
// C strings and may be empty (never NULL).
struct ProxySQL_PluginQueryHookPayload {
const char *user;
const char *client_ip;
const char *schema;
const char *query_text;
uint32_t query_len;
};
// Outcome of a query hook. ALLOW lets the query proceed to the
// backend. DENY returns an error to the client and the query never
// dispatches; the message is copied by core, the plugin need not keep
// it alive after the callback returns.
//
// NOTE: same std::string ABI coupling caveat as
// ProxySQL_PluginCommandResult applies.
enum class ProxySQL_PluginQueryHookAction : uint8_t {
allow = 0,
deny = 1
};
struct ProxySQL_PluginQueryHookResult {
ProxySQL_PluginQueryHookAction action;
std::string message;
};
using proxysql_plugin_query_hook_cb =
ProxySQL_PluginQueryHookResult (*)(const ProxySQL_PluginQueryHookPayload &);
// register_query_hook(proto, cb). Returns true on success, false if a
// hook for that protocol is already registered. Valid only during the
// init callback (same lifetime rule as register_table / register_command).
using proxysql_plugin_register_query_hook_cb =
bool (*)(ProxySQL_PluginProtocol, proxysql_plugin_query_hook_cb);
// Services provided to plugins during init.
// register_table/register_command: valid only during the init callback.
// get_*db, log_message, snapshots: valid for the plugin's entire lifetime.
@ -71,6 +121,10 @@ struct ProxySQL_PluginServices {
proxysql_plugin_db_handle_cb get_admindb;
proxysql_plugin_db_handle_cb get_configdb;
proxysql_plugin_db_handle_cb get_statsdb;
// Step 2 ABI extension: pre-execution query hook. Older plugins
// that don't know about this field stop reading the struct at the
// previous member; new plugins check for non-null before calling.
proxysql_plugin_register_query_hook_cb register_query_hook;
};
using proxysql_plugin_init_cb =

@ -29,6 +29,11 @@ public:
bool has_command_for_test(const std::string& sql) const;
bool register_table(const ProxySQL_PluginTableDef& def);
bool register_command(const char* sql, proxysql_plugin_admin_command_cb cb);
bool register_query_hook(ProxySQL_PluginProtocol proto, proxysql_plugin_query_hook_cb cb);
bool has_query_hook(ProxySQL_PluginProtocol proto) const;
bool dispatch_query_hook(ProxySQL_PluginProtocol proto,
const ProxySQL_PluginQueryHookPayload& payload,
ProxySQL_PluginQueryHookResult& result) const;
size_t size() const;
@ -59,6 +64,9 @@ private:
std::vector<ProxySQL_PluginTableDef> tables_stats_;
std::deque<registered_table_storage_t> table_storage_;
std::vector<registered_command_t> commands_;
// At most one hook per protocol; nullptr means "no hook".
proxysql_plugin_query_hook_cb mysql_query_hook_ { nullptr };
proxysql_plugin_query_hook_cb pgsql_query_hook_ { nullptr };
};
ProxySQL_PluginManager* proxysql_get_plugin_manager();
@ -67,6 +75,17 @@ bool proxysql_dispatch_configured_plugin_admin_command(
const std::string& sql,
ProxySQL_PluginCommandResult& result
);
bool proxysql_dispatch_configured_plugin_query_hook(
ProxySQL_PluginProtocol proto,
const ProxySQL_PluginQueryHookPayload& payload,
ProxySQL_PluginQueryHookResult& result
);
// Fast path for hot code: returns true when the active manager has a hook
// registered for the given protocol. No locks taken. Callers should still
// invoke proxysql_dispatch_configured_plugin_query_hook to actually run the
// hook (which takes the manager lock). Use this to elide the dispatch call
// entirely on the no-plugin path.
bool proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol proto);
bool proxysql_load_configured_plugins(
std::unique_ptr<ProxySQL_PluginManager>& manager,
const std::vector<std::string>& plugin_modules,

@ -83,6 +83,22 @@ void register_command_service(const char* sql, proxysql_plugin_admin_command_cb
}
}
bool register_query_hook_service(ProxySQL_PluginProtocol proto,
proxysql_plugin_query_hook_cb cb) {
if (g_registry_target == nullptr) {
proxy_warning("Plugin query hook registration attempted outside init phase\n");
return false;
}
if (!g_registry_target->register_query_hook(proto, cb)) {
note_registration_failure("plugin query hook",
proto == ProxySQL_PluginProtocol::mysql ? "mysql" : "pgsql");
proxy_warning("Plugin query hook registration failed for %s\n",
proto == ProxySQL_PluginProtocol::mysql ? "mysql" : "pgsql");
return false;
}
return true;
}
SQLite3DB* get_admindb_service() {
return proxysql_plugin_get_admindb();
}
@ -165,6 +181,7 @@ ProxySQL_PluginManager::ProxySQL_PluginManager() {
services_.get_configdb = &get_configdb_service;
services_.get_statsdb = &get_statsdb_service;
services_.log_message = &log_message_service;
services_.register_query_hook = &register_query_hook_service;
}
ProxySQL_PluginManager::~ProxySQL_PluginManager() {
@ -430,6 +447,47 @@ bool ProxySQL_PluginManager::register_table(const ProxySQL_PluginTableDef& def)
return true;
}
bool ProxySQL_PluginManager::register_query_hook(ProxySQL_PluginProtocol proto,
proxysql_plugin_query_hook_cb cb) {
if (cb == nullptr) {
return false;
}
switch (proto) {
case ProxySQL_PluginProtocol::mysql:
if (mysql_query_hook_ != nullptr) return false;
mysql_query_hook_ = cb;
return true;
case ProxySQL_PluginProtocol::pgsql:
if (pgsql_query_hook_ != nullptr) return false;
pgsql_query_hook_ = cb;
return true;
}
return false;
}
bool ProxySQL_PluginManager::has_query_hook(ProxySQL_PluginProtocol proto) const {
switch (proto) {
case ProxySQL_PluginProtocol::mysql: return mysql_query_hook_ != nullptr;
case ProxySQL_PluginProtocol::pgsql: return pgsql_query_hook_ != nullptr;
}
return false;
}
bool ProxySQL_PluginManager::dispatch_query_hook(ProxySQL_PluginProtocol proto,
const ProxySQL_PluginQueryHookPayload& payload,
ProxySQL_PluginQueryHookResult& result) const {
proxysql_plugin_query_hook_cb cb = nullptr;
switch (proto) {
case ProxySQL_PluginProtocol::mysql: cb = mysql_query_hook_; break;
case ProxySQL_PluginProtocol::pgsql: cb = pgsql_query_hook_; break;
}
if (cb == nullptr) {
return false;
}
result = cb(payload);
return true;
}
bool ProxySQL_PluginManager::register_command(const char* sql, proxysql_plugin_admin_command_cb cb) {
if (sql == nullptr || *sql == '\0' || cb == nullptr) {
return false;
@ -467,6 +525,33 @@ bool proxysql_dispatch_configured_plugin_admin_command(
return g_active_plugin_manager.load()->dispatch_admin_command(ctx, sql, result);
}
bool proxysql_dispatch_configured_plugin_query_hook(
ProxySQL_PluginProtocol proto,
const ProxySQL_PluginQueryHookPayload& payload,
ProxySQL_PluginQueryHookResult& result
) {
std::lock_guard<std::mutex> lock(g_active_plugin_manager_mutex);
ProxySQL_PluginManager* mgr = g_active_plugin_manager.load();
if (mgr == nullptr) {
return false;
}
return mgr->dispatch_query_hook(proto, payload, result);
}
bool proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol proto) {
// Hot path: lock-free. Reads the atomic pointer; if non-null, calls
// has_query_hook() which only reads two pointer-sized fields. A
// concurrent unload can null the pointer between this check and a
// subsequent dispatch call -- the dispatch helper handles that case
// by re-checking under the lock. Callers must tolerate spurious
// "yes" returns.
ProxySQL_PluginManager* mgr = g_active_plugin_manager.load(std::memory_order_acquire);
if (mgr == nullptr) {
return false;
}
return mgr->has_query_hook(proto);
}
bool proxysql_load_configured_plugins(
std::unique_ptr<ProxySQL_PluginManager>& manager,
const std::vector<std::string>& plugin_modules,

@ -2,6 +2,8 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
namespace {
@ -11,6 +13,21 @@ ProxySQL_PluginCommandResult fake_command(const ProxySQL_PluginCommandContext&,
return {0, 1, "fake command executed"};
}
ProxySQL_PluginQueryHookResult fake_query_hook(const ProxySQL_PluginQueryHookPayload& payload) {
// Echo the SQL back through the message field so tests can verify the
// payload was wired through. DENY-vs-ALLOW is selected by env var so
// a test can exercise both paths without rebuilding the plugin.
std::string msg(payload.query_text, payload.query_len);
const char* deny_env = std::getenv("PROXYSQL_FAKE_PLUGIN_HOOK_DENY");
if (deny_env == nullptr) {
deny_env = std::getenv("PROXYSQL_FAKE_PLUGIN2_HOOK_DENY");
}
if (deny_env != nullptr && *deny_env != '\0') {
return {ProxySQL_PluginQueryHookAction::deny, std::string("denied: ") + msg};
}
return {ProxySQL_PluginQueryHookAction::allow, msg};
}
void fake_log_event(const char *event) {
const char *log_path = std::getenv("PROXYSQL_FAKE_PLUGIN_LOG");
if (log_path == nullptr || *log_path == '\0') {
@ -47,6 +64,16 @@ bool fake_init(ProxySQL_PluginServices *services) {
services->register_command != nullptr) {
services->register_command("PLUGIN FAKE NOOP", &fake_command);
}
if (std::getenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK") != nullptr &&
services != nullptr &&
services->register_query_hook != nullptr) {
const char* proto_env = std::getenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK_PROTO");
ProxySQL_PluginProtocol proto = ProxySQL_PluginProtocol::mysql;
if (proto_env != nullptr && std::strcmp(proto_env, "pgsql") == 0) {
proto = ProxySQL_PluginProtocol::pgsql;
}
services->register_query_hook(proto, &fake_query_hook);
}
fake_log_event("init");
return true;
}

@ -323,6 +323,7 @@ UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t \
glovars_unit-t \
plugin_manager_unit-t \
plugin_registry_unit-t \
plugin_query_hook_unit-t \
test_mysqlx_plugin_load-t \
mysqlx_config_store_unit-t \
test_mysqlx_admin_tables-t \
@ -540,6 +541,12 @@ mysql_resolution_unit-t: mysql_resolution_unit-t.cpp $(ODIR)/tap.o $(ODIR)/tap_n
-I$(TAP_IDIR) -I$(PROXYSQL_PATH)/include \
$(STDCPP) -O0 -ggdb $(WGCOV) $(LWGCOV) -lpthread -o $@
plugin_query_hook_unit-t: plugin_query_hook_unit-t.cpp $(FAKE_PLUGIN_SO) $(ODIR)/tap.o $(ODIR)/test_globals.o $(ODIR)/test_init.o $(LIBPROXYSQLAR)
$(CXX) $< $(ODIR)/tap.o $(ODIR)/test_globals.o $(ODIR)/test_init.o \
-DPROXYSQL_FAKE_PLUGIN_PATH=\"$(FAKE_PLUGIN_SO)\" \
$(IDIRS) $(LDIRS) $(OPT) $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) \
$(MYLIBS) -ldl $(ALLOW_MULTI_DEF) -o $@
# Pattern rule: all unit tests use the same compile + link flags.
# Each test binary is built from its .cpp source, linked against
# the test harness objects and libproxysql.a with all dependencies.

@ -0,0 +1,226 @@
// Step 2 ABI extension test: pre-execution query hook.
//
// Exercises the manager-level register/dispatch pair AND the global
// dispatcher (proxysql_dispatch_configured_plugin_query_hook) that the
// MySQL_Session / PgSQL_Session hot path will call once Commit 2 lands.
// Hot-path call sites themselves are tested separately.
#include "ProxySQL_PluginManager.h"
#include "ProxySQL_Plugin.h"
#include "tap.h"
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
#ifndef PROXYSQL_FAKE_PLUGIN_PATH
#error "PROXYSQL_FAKE_PLUGIN_PATH must be defined"
#endif
namespace {
char g_fake_admin_db = '\0';
char g_fake_config_db = '\0';
char g_fake_stats_db = '\0';
ProxySQL_PluginQueryHookResult always_allow_hook(const ProxySQL_PluginQueryHookPayload&) {
return {ProxySQL_PluginQueryHookAction::allow, ""};
}
ProxySQL_PluginQueryHookResult always_deny_hook(const ProxySQL_PluginQueryHookPayload&) {
return {ProxySQL_PluginQueryHookAction::deny, "blocked"};
}
ProxySQL_PluginQueryHookResult echo_hook(const ProxySQL_PluginQueryHookPayload& p) {
std::string body(p.query_text, p.query_len);
return {ProxySQL_PluginQueryHookAction::allow,
std::string(p.user) + "/" + p.client_ip + "/" + p.schema + ":" + body};
}
ProxySQL_PluginQueryHookPayload payload_for(const char* user, const char* ip,
const char* schema, const char* query) {
return ProxySQL_PluginQueryHookPayload {
user, ip, schema, query, static_cast<uint32_t>(std::strlen(query))
};
}
} // namespace
SQLite3DB* proxysql_plugin_get_admindb() { return reinterpret_cast<SQLite3DB*>(&g_fake_admin_db); }
SQLite3DB* proxysql_plugin_get_configdb() { return reinterpret_cast<SQLite3DB*>(&g_fake_config_db); }
SQLite3DB* proxysql_plugin_get_statsdb() { return reinterpret_cast<SQLite3DB*>(&g_fake_stats_db); }
static void test_unregistered_protocols_have_no_hook() {
ProxySQL_PluginManager mgr;
ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::mysql),
"fresh manager has no MySQL hook");
ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::pgsql),
"fresh manager has no PgSQL hook");
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "x"};
auto p = payload_for("u", "ip", "s", "select 1");
ok(!mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
"dispatch returns false on protocol with no hook");
ok(r.action == ProxySQL_PluginQueryHookAction::deny && r.message == "x",
"dispatch leaves caller's result struct untouched on no-hook miss");
}
static void test_register_and_dispatch_allow() {
ProxySQL_PluginManager mgr;
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_allow_hook),
"register MySQL allow hook");
ok(mgr.has_query_hook(ProxySQL_PluginProtocol::mysql),
"MySQL hook now present");
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "init"};
auto p = payload_for("alice", "10.0.0.1", "test", "select 1");
ok(mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
"dispatch returns true (hook fired)");
ok(r.action == ProxySQL_PluginQueryHookAction::allow,
"hook returned ALLOW");
ok(r.message.empty(),
"ALLOW carries empty message");
}
static void test_register_and_dispatch_deny() {
ProxySQL_PluginManager mgr;
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_deny_hook),
"register MySQL deny hook");
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::allow, ""};
auto p = payload_for("eve", "10.0.0.99", "prod", "drop table users");
ok(mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
"dispatch returns true");
ok(r.action == ProxySQL_PluginQueryHookAction::deny,
"hook returned DENY");
ok(r.message == "blocked",
"DENY message propagated to caller");
}
static void test_register_null_callback_rejected() {
ProxySQL_PluginManager mgr;
ok(!mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, nullptr),
"null callback rejected for MySQL");
ok(!mgr.register_query_hook(ProxySQL_PluginProtocol::pgsql, nullptr),
"null callback rejected for PgSQL");
ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::mysql),
"no hook stored after rejection (MySQL)");
ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::pgsql),
"no hook stored after rejection (PgSQL)");
}
static void test_duplicate_hook_rejected() {
ProxySQL_PluginManager mgr;
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_allow_hook),
"first MySQL registration succeeds");
ok(!mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_deny_hook),
"second MySQL registration rejected (one hook per protocol per manager)");
// Verify the first hook is still in effect
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "x"};
auto p = payload_for("u", "ip", "s", "q");
mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r);
ok(r.action == ProxySQL_PluginQueryHookAction::allow,
"original hook still in effect after rejected duplicate");
}
static void test_protocols_independent() {
ProxySQL_PluginManager mgr;
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_allow_hook),
"MySQL hook registers");
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::pgsql, &always_deny_hook),
"PgSQL hook registers independently");
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, ""};
auto p = payload_for("u", "ip", "s", "q");
mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r);
ok(r.action == ProxySQL_PluginQueryHookAction::allow,
"MySQL dispatch routes to MySQL hook (allow)");
mgr.dispatch_query_hook(ProxySQL_PluginProtocol::pgsql, p, r);
ok(r.action == ProxySQL_PluginQueryHookAction::deny,
"PgSQL dispatch routes to PgSQL hook (deny)");
}
static void test_payload_threaded_through() {
ProxySQL_PluginManager mgr;
mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &echo_hook);
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, ""};
auto p = payload_for("alice", "10.1.2.3", "shop", "SELECT id FROM orders");
ok(mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
"dispatch fires echo hook");
ok(r.message == "alice/10.1.2.3/shop:SELECT id FROM orders",
"all payload fields reach the hook intact (got: '%s')", r.message.c_str());
}
static void test_global_dispatcher_no_active_manager() {
// Make sure no manager is active (defensive — earlier tests may have
// left state behind, but we never set the active pointer in this test).
ok(proxysql_get_plugin_manager() == nullptr,
"global manager is null at start");
ok(!proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql),
"has_hook helper returns false with no active manager");
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "untouched"};
auto p = payload_for("u", "ip", "s", "q");
ok(!proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
"dispatcher returns false with no active manager");
ok(r.message == "untouched",
"dispatcher leaves caller's result untouched on miss");
}
static void test_global_dispatcher_with_active_manager() {
setenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK", "1", 1);
std::unique_ptr<ProxySQL_PluginManager> mgr;
std::vector<std::string> paths { PROXYSQL_FAKE_PLUGIN_PATH };
std::string err;
ok(proxysql_load_configured_plugins(mgr, paths, err),
"load fake plugin (which registers a MySQL query hook)");
ok(proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql),
"has_hook helper reports a MySQL hook is now active");
ok(!proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::pgsql),
"has_hook helper reports no PgSQL hook (fake plugin only registered MySQL)");
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, ""};
auto p = payload_for("u", "ip", "s", "select 42");
ok(proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
"global dispatcher routes to fake hook");
ok(r.action == ProxySQL_PluginQueryHookAction::allow,
"hook returned ALLOW (no DENY env set)");
ok(r.message == "select 42",
"fake hook echoed the SQL through the result message (got: '%s')", r.message.c_str());
// Now flip to DENY and re-dispatch
setenv("PROXYSQL_FAKE_PLUGIN_HOOK_DENY", "1", 1);
ProxySQL_PluginQueryHookResult r2 {ProxySQL_PluginQueryHookAction::allow, ""};
ok(proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r2),
"second dispatch with DENY env still fires");
ok(r2.action == ProxySQL_PluginQueryHookAction::deny,
"hook now returns DENY");
ok(r2.message == "denied: select 42",
"DENY message includes the offending SQL (got: '%s')", r2.message.c_str());
(void)proxysql_stop_configured_plugins(mgr, err);
unsetenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK");
unsetenv("PROXYSQL_FAKE_PLUGIN_HOOK_DENY");
// After stop, neither helper should report a hook.
ok(!proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql),
"has_hook helper returns false after stop");
ProxySQL_PluginQueryHookResult r3 {ProxySQL_PluginQueryHookAction::deny, "still-here"};
ok(!proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r3),
"dispatcher returns false after stop");
}
int main() {
plan(41);
test_unregistered_protocols_have_no_hook();
test_register_and_dispatch_allow();
test_register_and_dispatch_deny();
test_register_null_callback_rejected();
test_duplicate_hook_rejected();
test_protocols_independent();
test_payload_threaded_through();
test_global_dispatcher_no_active_manager();
test_global_dispatcher_with_active_manager();
return exit_status();
}
Loading…
Cancel
Save