Address PR #5410 review findings across FFTO, sessions, TAP, and docs

Apply actionable bot review feedback from PR #5410 without behavior drift:\n- fix MySQL/PgSQL FFTO digest normalization and max-length hashing\n- add early FFTO packet/message size guards for bypass safety\n- restore MySQL session GTID reset and harden GenAI async error ownership\n- add null-check for stripped KILL query normalization path\n- call FFTO on_close() before observer reset/bypass in session flows\n- fix processlist query-options default pagination limit semantics\n- fix TAP noise_failures locking race and noise-aware plan count\n- extend wait_get_enpoint_ready with optional basic-auth passthrough\n- update MCP architecture docs from observe->stats naming\n- clean corrupted character in pgsql query-cache test comment\n\nValidation run:\n- git diff --check\n- make -C test/tap/tap tap.o utils_mariadb.o utils_mysql57.o utils_mysql8.o -j4\n- make -C lib obj/MySQLFFTO.oo obj/PgSQLFFTO.oo obj/MySQL_Session.oo obj/PgSQL_Session.oo obj/Query_Processor.oo -j4
release-notes-3.0.6-4.0.6-draft
Rene Cannao 1 month ago
parent 79c0b383e8
commit e53d2c76f9

@ -31,7 +31,7 @@ The MCP module implements JSON-RPC 2.0 over HTTPS for LLM (Large Language Model)
│ │ - query_tool_handler (NEW) │ │
│ │ - admin_tool_handler (NEW) │ │
│ │ - cache_tool_handler (NEW) │ │
│ │ - observe_tool_handler (NEW) │ │
│ │ - stats_tool_handler (NEW) │ │
│ │ - ai_tool_handler (NEW) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
@ -47,7 +47,7 @@ The MCP module implements JSON-RPC 2.0 over HTTPS for LLM (Large Language Model)
│ ┌──────────────┬──────────────┼──────────────┬──────────────┬─────────┐ │
│ ▼ ▼ ▼ ▼ ▼ ▼ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌───┐│
│ │conf│ │obs │ │qry │ │adm │ │cach│ │ai ││
│ │conf│ │sts │ │qry │ │adm │ │cach│ │ai ││
│ │TH │ │TH │ │TH │ │TH │ │TH │ │TH ││
│ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ └─┬─┘│
│ │ │ │ │ │ │ │
@ -80,7 +80,7 @@ include/
├── Query_Tool_Handler.h # Query endpoint tool handler (includes discovery tools)
├── Admin_Tool_Handler.h # Administration endpoint tool handler
├── Cache_Tool_Handler.h # Cache endpoint tool handler
├── Observe_Tool_Handler.h # Observability endpoint tool handler
├── Stats_Tool_Handler.h # Stats endpoint tool handler
├── AI_Tool_Handler.h # AI endpoint tool handler
├── Discovery_Schema.h # Discovery catalog implementation
├── Static_Harvester.h # Static database harvester for discovery
@ -94,7 +94,7 @@ lib/
├── Query_Tool_Handler.cpp # Query endpoint implementation
├── Admin_Tool_Handler.cpp # Administration endpoint implementation
├── Cache_Tool_Handler.cpp # Cache endpoint implementation
├── Observe_Tool_Handler.cpp # Observability endpoint implementation
├── Stats_Tool_Handler.cpp # Stats endpoint implementation
├── AI_Tool_Handler.cpp # AI endpoint implementation
├── Discovery_Schema.cpp # Discovery catalog implementation
├── Static_Harvester.cpp # Static database harvester implementation
@ -449,7 +449,7 @@ private:
### Phase 1: Base Infrastructure ✅ COMPLETED
1. ✅ Create `MCP_Tool_Handler` base class
2. ✅ Create implementations for all 6 tool handlers (config, query, admin, cache, observe, ai)
2. ✅ Create implementations for all 6 tool handlers (config, query, admin, cache, stats, ai)
3. ✅ Update `MCP_Threads_Handler` to manage all handlers
4. ✅ Update `ProxySQL_MCP_Server` to pass handlers to endpoints
@ -459,7 +459,7 @@ private:
2. ✅ Implement Query_Tool_Handler tools (includes MySQL tools and discovery tools)
3. ✅ Implement Admin_Tool_Handler tools
4. ✅ Implement Cache_Tool_Handler tools
5. ✅ Implement Observe_Tool_Handler tools
5. ✅ Implement Stats_Tool_Handler tools
6. ✅ Implement AI_Tool_Handler tools
### Phase 3: Authentication & Testing ✅ MOSTLY COMPLETED

@ -12,6 +12,7 @@
#include <tuple>
#include <vector>
#include <array>
#include <limits>
#include "ProxySQL_RESTAPI_Server.hpp"
@ -280,7 +281,7 @@ struct processlist_query_options_t {
processlist_sort_by_t sort_by {processlist_sort_by_t::none}; ///< Optional primary sort key.
bool sort_desc {true}; ///< Sort direction for @ref sort_by.
bool disable_pagination {false}; ///< If true, ignore @ref limit and @ref offset.
uint32_t limit {0}; ///< Page size (`0` means return zero rows).
uint32_t limit {std::numeric_limits<uint32_t>::max()}; ///< Page size (defaults to no limit).
uint32_t offset {0}; ///< Number of rows to skip before page.
};

@ -67,13 +67,18 @@ MySQLFFTO::~MySQLFFTO() {
void MySQLFFTO::on_client_data(const char* buf, std::size_t len) {
if (!buf || len == 0) return;
m_client_buffer.insert(m_client_buffer.end(), buf, buf + len);
while (m_client_buffer.size() - m_client_offset >= sizeof(mysql_hdr)) {
const mysql_hdr* hdr = reinterpret_cast<const mysql_hdr*>(m_client_buffer.data() + m_client_offset);
uint32_t pkt_len = hdr->pkt_length;
if (m_client_buffer.size() - m_client_offset < sizeof(mysql_hdr) + pkt_len) break;
const unsigned char* payload = reinterpret_cast<const unsigned char*>(m_client_buffer.data()) + m_client_offset + sizeof(mysql_hdr);
process_client_packet(payload, pkt_len);
m_client_offset += sizeof(mysql_hdr) + pkt_len;
while (m_client_buffer.size() - m_client_offset >= sizeof(mysql_hdr)) {
const mysql_hdr* hdr = reinterpret_cast<const mysql_hdr*>(m_client_buffer.data() + m_client_offset);
uint32_t pkt_len = hdr->pkt_length;
if (pkt_len > (uint32_t)mysql_thread___ffto_max_buffer_size) {
m_session->ffto_bypassed = true;
on_close();
return;
}
if (m_client_buffer.size() - m_client_offset < sizeof(mysql_hdr) + pkt_len) break;
const unsigned char* payload = reinterpret_cast<const unsigned char*>(m_client_buffer.data()) + m_client_offset + sizeof(mysql_hdr);
process_client_packet(payload, pkt_len);
m_client_offset += sizeof(mysql_hdr) + pkt_len;
}
if (m_client_offset > 0) {
if (m_client_offset >= m_client_buffer.size()) {
@ -238,24 +243,25 @@ void MySQLFFTO::report_query_stats(const std::string& query, unsigned long long
auto* ui = m_session->client_myds->myconn->userinfo;
if (!ui->username || !ui->schemaname) return;
options opts;
opts.lowercase = mysql_thread___query_digests_lowercase;
opts.replace_null = mysql_thread___query_digests_replace_null;
opts.replace_number = !mysql_thread___query_digests_no_digits;
opts.keep_comment = mysql_thread___query_digests_keep_comment;
opts.grouping_limit = mysql_thread___query_digests_grouping_limit;
opts.groups_grouping_limit = mysql_thread___query_digests_groups_grouping_limit;
opts.max_query_length = mysql_thread___query_digests_max_query_length;
options opts;
opts.lowercase = mysql_thread___query_digests_lowercase;
opts.replace_null = mysql_thread___query_digests_replace_null;
opts.replace_number = mysql_thread___query_digests_no_digits;
opts.keep_comment = mysql_thread___query_digests_keep_comment;
opts.grouping_limit = mysql_thread___query_digests_grouping_limit;
opts.groups_grouping_limit = mysql_thread___query_digests_groups_grouping_limit;
opts.max_query_length = mysql_thread___query_digests_max_query_length;
SQP_par_t qp; memset(&qp, 0, sizeof(qp));
char* fst_cmnt = NULL;
char* digest_text = mysql_query_digest_and_first_comment(query.c_str(), query.length(), &fst_cmnt,
((query.length() < QUERY_DIGEST_BUF) ? qp.buf : NULL), &opts);
if (digest_text) {
qp.digest_text = digest_text;
qp.digest = SpookyHash::Hash64(digest_text, strlen(digest_text), 0);
char* ca = (char*)"";
if (mysql_thread___query_digests_track_hostname && m_session->client_myds->addr.addr) ca = m_session->client_myds->addr.addr;
char* digest_text = mysql_query_digest_and_first_comment(query.c_str(), query.length(), &fst_cmnt,
((query.length() < QUERY_DIGEST_BUF) ? qp.buf : NULL), &opts);
if (digest_text) {
qp.digest_text = digest_text;
const int digest_len = strnlen(digest_text, mysql_thread___query_digests_max_digest_length);
qp.digest = SpookyHash::Hash64(digest_text, digest_len, 0);
char* ca = (char*)"";
if (mysql_thread___query_digests_track_hostname && m_session->client_myds->addr.addr) ca = m_session->client_myds->addr.addr;
uint64_t hash2; SpookyHash myhash; myhash.Init(19, 3);
myhash.Update(ui->username, strlen(ui->username));
myhash.Update(&qp.digest, sizeof(qp.digest));

@ -729,15 +729,19 @@ void MySQL_Session::reset() {
delete mybes;
mybes=NULL;
}
mybe=NULL;
mybe=NULL;
with_gtid = false;
backend_closed_in_fast_forward = false;
fast_forward_grace_start_time = 0;
ffto_bypassed = false;
m_ffto.reset();
with_gtid = false;
backend_closed_in_fast_forward = false;
fast_forward_grace_start_time = 0;
ffto_bypassed = false;
if (m_ffto) {
m_ffto->on_close();
}
m_ffto.reset();
//gtid_trxid = 0; gtid_hid = -1;
//gtid_trxid = 0;
gtid_hid = -1;
memset(gtid_buf,0,sizeof(gtid_buf));
if (session_type == PROXYSQL_SESSION_SQLITE) {
SQLite3_Session *sqlite_sess = (SQLite3_Session *)thread->gen_args;
@ -759,9 +763,6 @@ void MySQL_Session::reset() {
* @brief Destructor for the MySQL session.
*/
MySQL_Session::~MySQL_Session() {
if (m_ffto) {
m_ffto->on_close();
}
reset();
// we moved this out to allow CHANGE_USER
@ -4190,6 +4191,11 @@ bool MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___genai_s
if (written != sizeof(hdr)) {
proxy_error("GenAI: Failed to write request header to fd %d: %s\n",
fds[0], strerror(errno));
auto it = pending_genai_requests_.find(hdr.request_id);
if (it != pending_genai_requests_.end() && it->second.original_pkt) {
it->second.original_pkt->ptr = nullptr;
it->second.original_pkt->size = 0;
}
genai_cleanup_request(hdr.request_id);
client_myds->myprot.generate_pkt_ERR(true, NULL, NULL, 1, 1263, (char*)"HY000",
"Failed to send request to GenAI module", true);
@ -4207,6 +4213,11 @@ bool MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___genai_s
}
proxy_error("GenAI: Failed to write JSON query to fd %d: %s\n",
fds[0], strerror(errno));
auto it = pending_genai_requests_.find(hdr.request_id);
if (it != pending_genai_requests_.end() && it->second.original_pkt) {
it->second.original_pkt->ptr = nullptr;
it->second.original_pkt->size = 0;
}
genai_cleanup_request(hdr.request_id);
client_myds->myprot.generate_pkt_ERR(true, NULL, NULL, 1, 1264, (char*)"HY000",
"Failed to send query to GenAI module", true);
@ -4832,6 +4843,9 @@ void MySQL_Session::observe_ffto_client_packet(const PtrSize_t& pkt) {
if (pkt.size > (size_t)mysql_thread___ffto_max_buffer_size) {
ffto_bypassed = true;
if (m_ffto) {
m_ffto->on_close();
}
m_ffto.reset();
return;
}
@ -6024,16 +6038,23 @@ handler_again:
// register the mysql_data_stream
thread->mypolls.add(POLLIN|POLLOUT, mybe->server_myds->fd, mybe->server_myds, thread->curtime);
}
if (mysql_thread___ffto_enabled && !ffto_bypassed && m_ffto) {
for (unsigned int i = 0; i < mybe->server_myds->PSarrayIN->len; i++) {
if (mybe->server_myds->PSarrayIN->pdata[i].size > (size_t)mysql_thread___ffto_max_buffer_size) {
ffto_bypassed = true;
m_ffto.reset();
break;
}
m_ffto->on_server_data((const char*)mybe->server_myds->PSarrayIN->pdata[i].ptr, mybe->server_myds->PSarrayIN->pdata[i].size);
}
} client_myds->PSarrayOUT->copy_add(mybe->server_myds->PSarrayIN, 0, mybe->server_myds->PSarrayIN->len);
if (mysql_thread___ffto_enabled && !ffto_bypassed && m_ffto) {
for (unsigned int i = 0; i < mybe->server_myds->PSarrayIN->len; i++) {
if (mybe->server_myds->PSarrayIN->pdata[i].size > (size_t)mysql_thread___ffto_max_buffer_size) {
ffto_bypassed = true;
if (m_ffto) {
m_ffto->on_close();
}
m_ffto.reset();
break;
}
m_ffto->on_server_data(
(const char*)mybe->server_myds->PSarrayIN->pdata[i].ptr,
mybe->server_myds->PSarrayIN->pdata[i].size
);
}
}
client_myds->PSarrayOUT->copy_add(mybe->server_myds->PSarrayIN, 0, mybe->server_myds->PSarrayIN->len);
while (mybe->server_myds->PSarrayIN->len) mybe->server_myds->PSarrayIN->remove_index(mybe->server_myds->PSarrayIN->len-1,NULL);
break;
case CONNECTING_CLIENT:
@ -9085,6 +9106,10 @@ bool MySQL_Session::handle_command_query_kill(PtrSize_t *pkt) {
if (CurrentQuery.MyComQueryCmd == MYSQL_COM_QUERY_KILL) {
char* qu = mysql_query_strip_comments((char *)pkt->ptr+1+sizeof(mysql_hdr), pkt->size-1-sizeof(mysql_hdr),
mysql_thread___query_digests_lowercase);
if (!qu) {
proxy_error("Failed to normalize KILL query for digest matching (out of memory)\n");
return false;
}
string nq=string(qu,strlen(qu));
re2::RE2::Options *opt2=new re2::RE2::Options(RE2::Quiet);
opt2->set_case_sensitive(false);

@ -80,6 +80,11 @@ void PgSQLFFTO::on_client_data(const char* buf, std::size_t len) {
while (m_client_buffer.size() - m_client_offset >= 5) {
char type = m_client_buffer[m_client_offset];
uint32_t msg_len; memcpy(&msg_len, &m_client_buffer[m_client_offset + 1], 4); msg_len = ntohl(msg_len);
if (msg_len > (uint32_t)pgsql_thread___ffto_max_buffer_size) {
m_session->ffto_bypassed = true;
on_close();
return;
}
if (msg_len < 4 || msg_len > 1024 * 1024 * 1024) { // Sanity check
on_close();
m_client_buffer.clear(); m_client_offset = 0;
@ -271,7 +276,7 @@ void PgSQLFFTO::report_query_stats(const std::string& query, unsigned long long
options opts;
opts.lowercase = pgsql_thread___query_digests_lowercase;
opts.replace_null = pgsql_thread___query_digests_replace_null;
opts.replace_number = !pgsql_thread___query_digests_no_digits;
opts.replace_number = pgsql_thread___query_digests_no_digits;
opts.keep_comment = pgsql_thread___query_digests_keep_comment;
opts.grouping_limit = pgsql_thread___query_digests_grouping_limit;
opts.groups_grouping_limit = pgsql_thread___query_digests_groups_grouping_limit;
@ -283,7 +288,8 @@ void PgSQLFFTO::report_query_stats(const std::string& query, unsigned long long
((query.length() < QUERY_DIGEST_BUF) ? qp.buf : NULL), &opts);
if (digest_text) {
qp.digest_text = digest_text;
qp.digest = SpookyHash::Hash64(digest_text, strlen(digest_text), 0);
const int digest_len = strnlen(digest_text, pgsql_thread___query_digests_max_digest_length);
qp.digest = SpookyHash::Hash64(digest_text, digest_len, 0);
char* ca = (char*)"";
if (pgsql_thread___query_digests_track_hostname && m_session->client_myds->addr.addr) ca = m_session->client_myds->addr.addr;
uint64_t hash2; SpookyHash myhash; myhash.Init(19, 3);

@ -306,17 +306,18 @@ void PgSQL_Session::reset() {
}
}
}
if (client_myds && client_myds->myconn) {
client_myds->myconn->reset();
}
extended_query_phase = EXTQ_PHASE_IDLE;
ffto_bypassed = false;
m_ffto.reset();
}PgSQL_Session::~PgSQL_Session() {
if (client_myds && client_myds->myconn) {
client_myds->myconn->reset();
}
extended_query_phase = EXTQ_PHASE_IDLE;
ffto_bypassed = false;
if (m_ffto) {
m_ffto->on_close();
}
m_ffto.reset();
}
PgSQL_Session::~PgSQL_Session() {
if (locked_on_hostgroup >= 0) {
thread->status_variables.stvar[st_var_hostgroup_locked]--;
}
@ -2284,22 +2285,25 @@ __implicit_sync:
break;
}
break;
case FAST_FORWARD:
if (pgsql_thread___ffto_enabled && !ffto_bypassed) {
if (pkt.size > (size_t)pgsql_thread___ffto_max_buffer_size) {
ffto_bypassed = true;
m_ffto.reset();
} else {
if (!m_ffto) {
m_ffto = std::make_unique<PgSQLFFTO>(this);
}
if (m_ffto) {
m_ffto->on_client_data((const char*)pkt.ptr, pkt.size);
case FAST_FORWARD:
if (pgsql_thread___ffto_enabled && !ffto_bypassed) {
if (pkt.size > (size_t)pgsql_thread___ffto_max_buffer_size) {
ffto_bypassed = true;
if (m_ffto) {
m_ffto->on_close();
}
m_ffto.reset();
} else {
if (!m_ffto) {
m_ffto = std::make_unique<PgSQLFFTO>(this);
}
if (m_ffto) {
m_ffto->on_client_data((const char*)pkt.ptr, pkt.size);
}
}
}
}
mybe->server_myds->PSarrayOUT->add(pkt.ptr, pkt.size);
break;
mybe->server_myds->PSarrayOUT->add(pkt.ptr, pkt.size);
break;
// This state is required because it covers the following situation:
// 1. A new connection is created by a client and the 'FAST_FORWARD' mode is enabled.
// 2. The first packet received for this connection isn't a whole packet, i.e, it's either
@ -2725,20 +2729,27 @@ handler_again:
break;
case FAST_FORWARD:
{
if (mybe->server_myds->mypolls == NULL) {
// register the PgSQL_Data_Stream
thread->mypolls.add(POLLIN | POLLOUT, mybe->server_myds->fd, mybe->server_myds, thread->curtime);
}
if (pgsql_thread___ffto_enabled && !ffto_bypassed && m_ffto) {
for (unsigned int i = 0; i < mybe->server_myds->PSarrayIN->len; i++) {
if (mybe->server_myds->PSarrayIN->pdata[i].size > (size_t)pgsql_thread___ffto_max_buffer_size) {
ffto_bypassed = true;
m_ffto.reset();
break;
}
m_ffto->on_server_data((const char*)mybe->server_myds->PSarrayIN->pdata[i].ptr, mybe->server_myds->PSarrayIN->pdata[i].size);
}
} client_myds->PSarrayOUT->copy_add(mybe->server_myds->PSarrayIN, 0, mybe->server_myds->PSarrayIN->len);
if (mybe->server_myds->mypolls == NULL) {
// register the PgSQL_Data_Stream
thread->mypolls.add(POLLIN | POLLOUT, mybe->server_myds->fd, mybe->server_myds, thread->curtime);
}
if (pgsql_thread___ffto_enabled && !ffto_bypassed && m_ffto) {
for (unsigned int i = 0; i < mybe->server_myds->PSarrayIN->len; i++) {
if (mybe->server_myds->PSarrayIN->pdata[i].size > (size_t)pgsql_thread___ffto_max_buffer_size) {
ffto_bypassed = true;
if (m_ffto) {
m_ffto->on_close();
}
m_ffto.reset();
break;
}
m_ffto->on_server_data(
(const char*)mybe->server_myds->PSarrayIN->pdata[i].ptr,
mybe->server_myds->PSarrayIN->pdata[i].size
);
}
}
client_myds->PSarrayOUT->copy_add(mybe->server_myds->PSarrayIN, 0, mybe->server_myds->PSarrayIN->len);
constexpr unsigned char ready_packet[] = { 0x5A, 0x00, 0x00, 0x00, 0x05 };
bool is_copy_ready_packet = false;
@ -6908,4 +6919,3 @@ std::string PgSQL_DateStyle_Util::datestyle_to_string(PgSQL_DateStyle_t datestyl
std::string PgSQL_DateStyle_Util::datestyle_to_string(std::string_view input, const PgSQL_DateStyle_t& default_datestyle) {
return datestyle_to_string(parse_datestyle(input), default_datestyle);
}

@ -367,12 +367,14 @@ int exit_status()
int noise_count = get_noise_tools_count();
stop_noise_tools();
if (!noise_failures.empty()) {
{
std::lock_guard<std::mutex> lock(noise_failure_mutex);
for (const auto& failed_routine : noise_failures) {
if (!noise_failures.empty()) {
for (const auto& failed_routine : noise_failures) {
diag("Noise failure detected in: %s", failed_routine.c_str());
}
return EXIT_FAILURE;
}
return EXIT_FAILURE;
}
// Add noise tools to the count of executed tests if they didn't fail

@ -821,14 +821,14 @@ int wait_post_enpoint_ready(string endpoint, string post_params, uint32_t timeou
return res;
}
int wait_get_enpoint_ready(string endpoint, uint32_t timeout, uint32_t delay) {
int wait_get_enpoint_ready(string endpoint, uint32_t timeout, uint32_t delay, const string& userpwd) {
double waited = 0;
int res = -1;
while (waited < timeout) {
string curl_resp_err {};
uint64_t curl_res_code = 0;
int curl_res = perform_simple_get(endpoint, curl_res_code, curl_resp_err);
int curl_res = perform_simple_get(endpoint, curl_res_code, curl_resp_err, userpwd);
if (curl_res != CURLE_OK || curl_res_code != 200) {
diag("'curl_res': %d, 'curl_err': '%s', waiting for '%d'ms...", curl_res, curl_resp_err.c_str(), delay);

@ -397,7 +397,9 @@ int wait_post_enpoint_ready(
std::string endpoint, std::string post_params, uint32_t timeout, uint32_t delay=100
);
int wait_get_enpoint_ready(std::string endpoint, uint32_t timeout, uint32_t delay=100);
int wait_get_enpoint_ready(
std::string endpoint, uint32_t timeout, uint32_t delay=100, const std::string& userpwd=""
);
/**
* @brief Perform a simple POST query to the specified endpoint using the supplied

@ -127,7 +127,7 @@ int main(int argc, char** argv) {
spawn_internal_noise(cl, internal_noise_rest_prometheus_poller, {{"enable_rest_api", "true"}});
spawn_internal_noise(cl, internal_noise_pgsql_traffic_v2, {{"num_connections", "100"}, {"reconnect_interval", "100"}, {"avg_delay_ms", "300"}});
plan(48);
plan(48 + (cl.use_noise ? 3 : 0));
MYSQL* mysql = mysql_init(NULL);
if (!mysql)

@ -956,7 +956,7 @@ void execute_query_cache_notice_test(PGconn* admin_conn, PGconn* conn) {
void execute_prepared_test(PGconn* admin_conn, PGconn* conn) {
// 1) Enable query-cache-for-SELECT rules (same as basic test) so the system *would* cache
// simple queries <EFBFBD> but extended query protocol should bypass cache.
// simple queries -- but extended query protocol should bypass cache.
if (!executeQueries(admin_conn, {
"DELETE FROM pgsql_query_rules",
"INSERT INTO pgsql_query_rules (rule_id,active,match_digest,cache_ttl) VALUES (2,1,'^SELECT',4000)",

Loading…
Cancel
Save