diff --git a/docs/superpowers/specs/2026-05-03-stats-projection-abi-design.md b/docs/superpowers/specs/2026-05-03-stats-projection-abi-design.md new file mode 100644 index 000000000..9bc9e06b9 --- /dev/null +++ b/docs/superpowers/specs/2026-05-03-stats-projection-abi-design.md @@ -0,0 +1,144 @@ +# Stats Projection ABI for GenAI Plugin + +**Date:** 2026-05-03 +**Issue:** #5729 +**Branch:** issue-5729-stats-projection-abi + +## Problem + +The genai plugin carve-out (Step 4) left `stats_mcp_*` tables as a known gap. The DDL macros exist in `ProxySQL_Admin_Tables_Definitions.h`, the in-memory data structures and snapshot methods exist in the plugin, but there is no bridge to populate the tables from admin SELECT queries. Additionally, `ProxySQL_PluginRuntimeView` only carries an `admindb` handle — plugins that need to write to `statsdb` (like mysqlx's stats views) must reach through a back-channel, violating the separation-of-duties principle. + +## Design Decision: Option 2 — Extend `ProxySQL_PluginRuntimeView` + +Add a `db_kind` field to the existing `ProxySQL_PluginRuntimeView` struct. The chassis dispatches the correct DB handle based on `db_kind`. Single hook, no duplication, fixes the architectural leak. + +Alternatives considered and rejected: +- **Option 1** (parallel `register_stats_view`): Works but cements two near-identical hooks. +- **Option 3** (context struct): More flexible but bigger ABI surface for no current need. + +## ABI Change (v3 → v4) + +### `ProxySQL_PluginRuntimeView` struct (`include/ProxySQL_Plugin.h`) + +```cpp +struct ProxySQL_PluginRuntimeView { + ProxySQL_PluginDBKind db_kind; // NEW: chassis passes matching DB handle + const char *table_name; + void (*refresh)(SQLite3DB *db, void *opaque); + void *opaque; +}; +``` + +`PROXYSQL_PLUGIN_ABI_VERSION` bumps from 3 to 4. + +The `db_kind` field uses the existing `ProxySQL_PluginDBKind` enum (`admin_db`, `config_db`, `stats_db`). The chassis passes the matching handle to `refresh()`. + +### `registered_runtime_view_t` (`include/ProxySQL_PluginManager.h`) + +```cpp +struct registered_runtime_view_t { + ProxySQL_PluginDBKind db_kind { ProxySQL_PluginDBKind::admin_db }; + std::string table_name {}; + void (*refresh)(SQLite3DB*, void*) { nullptr }; + void* opaque { nullptr }; +}; +``` + +### Dispatch change (`lib/ProxySQL_PluginManager.cpp`) + +`refresh_runtime_views_for_query()` gains `configdb` and `statsdb` parameters: + +```cpp +void refresh_runtime_views_for_query(const std::string& sql, + SQLite3DB* admindb, SQLite3DB* configdb, SQLite3DB* statsdb) const; +``` + +For each registered view, selects the DB handle matching `view.db_kind`: +- `admin_db` → `admindb` +- `config_db` → `configdb` +- `stats_db` → `statsdb` + +The free function `proxysql_refresh_configured_plugin_runtime_views()` mirrors the new signature. The call site in `ProxySQL_Admin::GenericRefreshStatistics()` (line 1623 of `ProxySQL_Admin.cpp`) passes `this->admindb`, `this->configdb`, `this->statsdb`. + +## Genai Stats Tables + +### Tables (5 total) + +Move DDL definitions from `ProxySQL_Admin_Tables_Definitions.h` to `plugins/genai/src/plugin_tables.cpp` as local constants. + +| Table | `db_kind` | Data Source | Reset | +|-------|-----------|-------------|-------| +| `stats_mcp_query_digest` | `stats_db` | `Discovery_Schema::get_mcp_query_digest(false)` | No | +| `stats_mcp_query_digest_reset` | `stats_db` | `Discovery_Schema::get_mcp_query_digest(true)` | Yes | +| `stats_mcp_query_rules` | `stats_db` | `Discovery_Schema::get_stats_mcp_query_rules()` | No | +| `stats_mcp_query_tools_counters` | `stats_db` | `Query_Tool_Handler::get_tool_usage_stats_resultset(false)` | No | +| `stats_mcp_query_tools_counters_reset` | `stats_db` | `Query_Tool_Handler::get_tool_usage_stats_resultset(true)` | Yes | + +`stats_mcp_query_rules` has no `_reset` variant because `get_stats_mcp_query_rules()` does not support reset semantics. + +### Refresh callback pattern + +Each callback: +1. Receives `SQLite3DB* db` (will be `statsdb` because `db_kind = stats_db`) +2. Executes `DELETE FROM