From f968a45c080a1e0cc4e61a2e26265e058cbb50cd Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Fri, 14 Aug 2026 12:21:29 +0000 Subject: [PATCH 01/11] fix: support CONNECTION_ID in SQLite3 server --- src/SQLite3_Server.cpp | 10 ++++++++ .../tests/test_sqlite3_special_queries.cpp | 25 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/SQLite3_Server.cpp b/src/SQLite3_Server.cpp index d2e7d3414f..396f64d7ca 100644 --- a/src/SQLite3_Server.cpp +++ b/src/SQLite3_Server.cpp @@ -815,6 +815,16 @@ void SQLite3_Server_session_handler(MySQL_Session* sess, void *_pa, PtrSize_t *p goto __run_query; } + if (query_no_space_length==strlen("SELECT CONNECTION_ID()") && !strncasecmp("SELECT CONNECTION_ID()",query_no_space, query_no_space_length)) { + const std::string connection_id_query { + "SELECT " + std::to_string(sess->thread_session_id) + " AS 'CONNECTION_ID()'" + }; + l_free(query_length,query); + query=l_strdup(connection_id_query.c_str()); + query_length=connection_id_query.length()+1; + goto __run_query; + } + // see issue #1022 if (query_no_space_length==strlen("SELECT DATABASE() AS name") && !strncasecmp("SELECT DATABASE() AS name",query_no_space, query_no_space_length)) { l_free(query_length,query); diff --git a/test/tap/tests/test_sqlite3_special_queries.cpp b/test/tap/tests/test_sqlite3_special_queries.cpp index b35e8dbda8..4d6031943c 100644 --- a/test/tap/tests/test_sqlite3_special_queries.cpp +++ b/test/tap/tests/test_sqlite3_special_queries.cpp @@ -5,6 +5,7 @@ * response of the intercepted queries. */ +#include #include #include @@ -56,6 +57,8 @@ const vector set_queries { "SET wait_timeout=86400" }; +constexpr int sqlite3_server_port { 6030 }; + int main(int argc, char** argv) { CommandLine cl; @@ -65,7 +68,7 @@ int main(int argc, char** argv) { } const vector tests { gen_tests() }; - plan(tests.size()*(3 + set_queries.size())); + plan(tests.size()*(4 + set_queries.size())); for (const test_opts_t& opts : tests) { diag("Executing test test_opts=%s", to_string(opts).c_str()); @@ -81,7 +84,7 @@ int main(int argc, char** argv) { } #endif - if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, cl.port, NULL, cflags)) { + if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, sqlite3_server_port, NULL, cflags)) { fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); return EXIT_FAILURE; } @@ -100,6 +103,24 @@ int main(int argc, char** argv) { int initdb_rc = mysql_select_db(proxy, "information_schema"); ok(initdb_rc == 0, "COM_INIT_DB should succeed rc=%d", initdb_rc); + int connection_id_rc = mysql_query(proxy, "SELECT CONNECTION_ID()"); + MYSQL_RES* connection_id_result = connection_id_rc == 0 ? mysql_store_result(proxy) : nullptr; + MYSQL_ROW connection_id_row = connection_id_result ? mysql_fetch_row(connection_id_result) : nullptr; + char* parse_end = nullptr; + const unsigned long long connection_id = connection_id_row && connection_id_row[0] + ? std::strtoull(connection_id_row[0], &parse_end, 10) + : 0; + const bool valid_connection_id = connection_id_row && connection_id_row[0] + && parse_end && *parse_end == '\0' && connection_id > 0; + if (connection_id_result) { + mysql_free_result(connection_id_result); + } + ok( + connection_id_rc == 0 && valid_connection_id, + "SELECT CONNECTION_ID() should return a nonzero session ID rc=%d", + connection_id_rc + ); + for (const auto& q : set_queries) { diag("Executing 'special SET' query q='%s'", q.c_str()); int rc = mysql_query(proxy, q.c_str()); From a8cbd66de804e6d6cf25428254adf2d1aee4d0bd Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Fri, 14 Aug 2026 13:04:23 +0000 Subject: [PATCH 02/11] test: strengthen SQLite3 CONNECTION_ID coverage --- test/tap/tests/test_sqlite3_special_queries.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/tap/tests/test_sqlite3_special_queries.cpp b/test/tap/tests/test_sqlite3_special_queries.cpp index 4d6031943c..d4589ce5fd 100644 --- a/test/tap/tests/test_sqlite3_special_queries.cpp +++ b/test/tap/tests/test_sqlite3_special_queries.cpp @@ -57,7 +57,7 @@ const vector set_queries { "SET wait_timeout=86400" }; -constexpr int sqlite3_server_port { 6030 }; +constexpr int SQLITE3_SERVER_PORT { 6030 }; int main(int argc, char** argv) { CommandLine cl; @@ -84,7 +84,7 @@ int main(int argc, char** argv) { } #endif - if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, sqlite3_server_port, NULL, cflags)) { + if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, SQLITE3_SERVER_PORT, NULL, cflags)) { fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); return EXIT_FAILURE; } @@ -112,12 +112,14 @@ int main(int argc, char** argv) { : 0; const bool valid_connection_id = connection_id_row && connection_id_row[0] && parse_end && *parse_end == '\0' && connection_id > 0; + const unsigned long long expected_connection_id = + static_cast(mysql_thread_id(proxy)); if (connection_id_result) { mysql_free_result(connection_id_result); } ok( - connection_id_rc == 0 && valid_connection_id, - "SELECT CONNECTION_ID() should return a nonzero session ID rc=%d", + connection_id_rc == 0 && valid_connection_id && connection_id == expected_connection_id, + "SELECT CONNECTION_ID() should return this session ID rc=%d", connection_id_rc ); From bf3e526cf183f98b1ac4a5640d9660164ed3fc8e Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Fri, 14 Aug 2026 18:00:09 +0000 Subject: [PATCH 03/11] docs: specify SQLite3 CONNECTION_ID EOF handling --- ...-08-14-sqlite3-connection-id-eof-design.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-14-sqlite3-connection-id-eof-design.md diff --git a/docs/superpowers/specs/2026-08-14-sqlite3-connection-id-eof-design.md b/docs/superpowers/specs/2026-08-14-sqlite3-connection-id-eof-design.md new file mode 100644 index 0000000000..ba528c5960 --- /dev/null +++ b/docs/superpowers/specs/2026-08-14-sqlite3-connection-id-eof-design.md @@ -0,0 +1,53 @@ +# SQLite3 `CONNECTION_ID()` CLIENT_DEPRECATE_EOF compatibility + +## Goal + +Make `SELECT CONNECTION_ID()` on the SQLite3 listener return a valid, nonzero +frontend session ID for both ordinary clients and clients that negotiate +`CLIENT_DEPRECATE_EOF`. + +## Root cause + +The SQLite3-server interception currently rewrites `SELECT CONNECTION_ID()` +into a SQLite query and relies on the generic SQLite-result conversion path. +The four-mode TAP matrix shows that this response is valid without +`CLIENT_DEPRECATE_EOF`, but result retrieval fails in both configurations that +negotiate it. The failure was present before the test began comparing the +returned value with `mysql_thread_id()`, so it is a response-framing defect, +not an overly strict assertion. + +The main MySQL session already implements `SELECT CONNECTION_ID()` as a +native, one-column protocol response. It selects the EOF or OK terminator +according to the negotiated client capability and uses numeric field metadata. + +## Design + +Replace the SQLite query rewrite with the corresponding native response in +`SQLite3_Server_session_handler()`: + +1. Format `sess->thread_session_id` as the one returned value. +2. Emit a one-column `CONNECTION_ID()` result with `MYSQL_TYPE_LONGLONG` + metadata. +3. Emit an intermediate EOF only when `CLIENT_DEPRECATE_EOF` is not active. +4. Terminate rows with an OK packet when it is active, otherwise an EOF + packet. +5. End the request and release the incoming packet just as other + SQLite3-server command handlers do. + +No general resultset code, handshake logic, or connection-ID allocation will +change. + +## Regression coverage + +The existing `test_sqlite3_special_queries.cpp` already runs the relevant +matrix: `CLIENT_DEPRECATE_EOF` disabled/enabled crossed with the two +multi-statement settings. It asserts that `CONNECTION_ID()` parses as a +nonzero integer and equals `mysql_thread_id(proxy)`. The pre-fix CI run is the +red state: test cases 28 and 40 fail, while cases 4 and 16 pass. + +## Verification + +Compile the focused TAP binary and run it against a matching daemon. Then run +the full CI matrix for the PR; the two g9 jobs must pass, and Codecov must be +rechecked because the currently failing test produced a patch-coverage status +failure. From 8caf75b886d633a2be35371cf14c84880782e836 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Fri, 14 Aug 2026 18:01:27 +0000 Subject: [PATCH 04/11] docs: plan SQLite3 CONNECTION_ID EOF fix --- .../2026-08-14-sqlite3-connection-id-eof.md | 92 +++++++++++++++++++ ...-08-14-sqlite3-connection-id-eof-design.md | 4 +- 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-14-sqlite3-connection-id-eof.md diff --git a/docs/superpowers/plans/2026-08-14-sqlite3-connection-id-eof.md b/docs/superpowers/plans/2026-08-14-sqlite3-connection-id-eof.md new file mode 100644 index 0000000000..560a3e438a --- /dev/null +++ b/docs/superpowers/plans/2026-08-14-sqlite3-connection-id-eof.md @@ -0,0 +1,92 @@ +# SQLite3 CONNECTION_ID EOF Compatibility Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Return a valid SQLite3-listener `CONNECTION_ID()` result for clients +with and without `CLIENT_DEPRECATE_EOF`. + +**Architecture:** The SQLite3 session handler will generate the one-column +result directly, matching the established `MySQL_Session` special-query packet +sequence. The current TAP test remains the externally observable regression +coverage across the two EOF settings and two multi-statement settings. + +**Tech Stack:** C++17, ProxySQL MySQL wire protocol, TAP/libmariadb. + +## Global Constraints + +- Preserve the exact, case-insensitive `SELECT CONNECTION_ID()` matcher. +- Return `sess->thread_session_id` as an unsigned 64-bit MySQL result value. +- Use EOF only for clients that did not negotiate `CLIENT_DEPRECATE_EOF`. +- Do not alter SQLite query execution, connection-ID allocation, or handshake logic. + +--- + +### Task 1: Emit a capability-aware native CONNECTION_ID result + +**Files:** +- Modify: `src/SQLite3_Server.cpp:818-826` +- Test: `test/tap/tests/test_sqlite3_special_queries.cpp:102-126` + +**Interfaces:** +- Consumes: `MySQL_Session::thread_session_id`, + `MySQL_Data_Stream::myconn->options.client_flag`, and `MySQL_Protocol` packet + generators. +- Produces: a standard text-protocol resultset named `CONNECTION_ID()` with + one row containing the frontend session ID. + +- [ ] **Step 1: Establish the failing regression state** + +The existing TAP assertion is the regression test. It performs +`mysql_query("SELECT CONNECTION_ID()")`, retrieves the one-row result, and +requires a nonzero number equal to `mysql_thread_id(proxy)` for each generated +option pair. + +Run: inspect CI runs `31807431795` and `31807431885` for +`test_sqlite3_special_queries_libmariadb-t`. + +Expected: cases 28 and 40 fail when `cflags` is `CLIENT_DEPRECATE_EOF`, while +cases 4 and 16 pass without it. + +- [ ] **Step 2: Replace the SQLite-query rewrite with the minimal protocol response** + +At the exact-query matcher, generate the response using this packet sequence: + +```cpp +char connection_id[32]; +snprintf(connection_id, sizeof(connection_id), "%u", sess->thread_session_id); +const bool deprecate_eof_active = + sess->client_myds->myconn->options.client_flag & CLIENT_DEPRECATE_EOF; +``` + +Emit column count and a `MYSQL_TYPE_LONGLONG` field named `CONNECTION_ID()`. +If `deprecate_eof_active` is false, emit the intermediate EOF; emit the row; +then emit either the deprecated-EOF OK terminator or the normal EOF terminator. +Set the data-stream state to `STATE_SLEEP`, set `run_query` to false, and jump +to the existing `__run_query` cleanup path. Do not call `RequestEnd()` or free +the inbound packet: SQLite3-server direct responses follow the handler's +existing lifecycle instead. + +- [ ] **Step 3: Compile the focused TAP binary** + +Run: + +```bash +make -C test/tap/tests PROXYSQL_PATH="$PWD" test_sqlite3_special_queries_libmariadb-t +``` + +Expected: the focused binary compiles successfully. + +- [ ] **Step 4: Run the four-mode regression against a matching daemon** + +Run the compiled `test_sqlite3_special_queries_libmariadb-t` through the TAP +runner or against a daemon built from the same worktree. + +Expected: all 48 TAP assertions pass, including the two +`CLIENT_DEPRECATE_EOF` `CONNECTION_ID()` assertions. + +- [ ] **Step 5: Commit the implementation** + +```bash +git add src/SQLite3_Server.cpp +git commit -m "fix: handle SQLite3 CONNECTION_ID with deprecated EOF" +``` diff --git a/docs/superpowers/specs/2026-08-14-sqlite3-connection-id-eof-design.md b/docs/superpowers/specs/2026-08-14-sqlite3-connection-id-eof-design.md index ba528c5960..acc89ec936 100644 --- a/docs/superpowers/specs/2026-08-14-sqlite3-connection-id-eof-design.md +++ b/docs/superpowers/specs/2026-08-14-sqlite3-connection-id-eof-design.md @@ -31,8 +31,8 @@ Replace the SQLite query rewrite with the corresponding native response in 3. Emit an intermediate EOF only when `CLIENT_DEPRECATE_EOF` is not active. 4. Terminate rows with an OK packet when it is active, otherwise an EOF packet. -5. End the request and release the incoming packet just as other - SQLite3-server command handlers do. +5. Set `run_query` to false and return through the existing SQLite3-server + handler cleanup path, as its direct OK responses do. No general resultset code, handshake logic, or connection-ID allocation will change. From d20bf7fe8aa2b923b433f34f0051ba8723c75703 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Fri, 14 Aug 2026 18:09:05 +0000 Subject: [PATCH 05/11] fix: handle SQLite3 CONNECTION_ID with deprecated EOF --- src/SQLite3_Server.cpp | 45 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/SQLite3_Server.cpp b/src/SQLite3_Server.cpp index 396f64d7ca..f29a055df8 100644 --- a/src/SQLite3_Server.cpp +++ b/src/SQLite3_Server.cpp @@ -816,12 +816,45 @@ void SQLite3_Server_session_handler(MySQL_Session* sess, void *_pa, PtrSize_t *p } if (query_no_space_length==strlen("SELECT CONNECTION_ID()") && !strncasecmp("SELECT CONNECTION_ID()",query_no_space, query_no_space_length)) { - const std::string connection_id_query { - "SELECT " + std::to_string(sess->thread_session_id) + " AS 'CONNECTION_ID()'" - }; - l_free(query_length,query); - query=l_strdup(connection_id_query.c_str()); - query_length=connection_id_query.length()+1; + char connection_id[32]; + snprintf(connection_id, sizeof(connection_id), "%u", sess->thread_session_id); + + SQLite3_Session *sqlite_sess = static_cast(sess->thread->gen_args); + sqlite3 *db = sqlite_sess->sessdb->get_db(); + uint16_t set_status = 0; + if (sess->autocommit) { + set_status |= SERVER_STATUS_AUTOCOMMIT; + } + if ((*proxy_sqlite3_get_autocommit)(db) == 0) { + set_status |= SERVER_STATUS_IN_TRANS; + } + + MySQL_Data_Stream *myds = sess->client_myds; + MySQL_Protocol *myprot = &myds->myprot; + myds->DSS = STATE_QUERY_SENT_DS; + int sid = 1; + myprot->generate_pkt_column_count(true, NULL, NULL, sid, 1); sid++; + myprot->generate_pkt_field(true, NULL, NULL, sid, (char*)"", (char*)"", (char*)"", (char*)"CONNECTION_ID()", (char*)"", 63, 31, MYSQL_TYPE_LONGLONG, 161, 0, false, 0, NULL); sid++; + myds->DSS = STATE_COLUMN_DEFINITION; + + const bool deprecate_eof_active = myds->myconn->options.client_flag & CLIENT_DEPRECATE_EOF; + if (!deprecate_eof_active) { + myprot->generate_pkt_EOF(true, NULL, NULL, sid, 0, set_status); sid++; + } + + char *fields[] = { connection_id }; + unsigned long lengths[] = { strlen(connection_id) }; + myprot->generate_pkt_row(true, NULL, NULL, sid, 1, lengths, fields); sid++; + myds->DSS = STATE_ROW; + + if (deprecate_eof_active) { + myprot->generate_pkt_OK(true, NULL, NULL, sid, 0, 0, set_status, 0, NULL, true); sid++; + } else { + myprot->generate_pkt_EOF(true, NULL, NULL, sid, 0, set_status); sid++; + } + + myds->DSS = STATE_SLEEP; + run_query = false; goto __run_query; } From ea677599fe1beca45a9f6291ff53e5376fa2461d Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 15 Aug 2026 05:20:32 +0000 Subject: [PATCH 06/11] docs: define backend deprecate EOF negotiation --- ...ackend-deprecate-eof-negotiation-design.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-15-backend-deprecate-eof-negotiation-design.md diff --git a/docs/superpowers/specs/2026-08-15-backend-deprecate-eof-negotiation-design.md b/docs/superpowers/specs/2026-08-15-backend-deprecate-eof-negotiation-design.md new file mode 100644 index 0000000000..1393b6debd --- /dev/null +++ b/docs/superpowers/specs/2026-08-15-backend-deprecate-eof-negotiation-design.md @@ -0,0 +1,38 @@ +# Backend `CLIENT_DEPRECATE_EOF` Negotiation Design + +**Date:** 2026-08-15 +**Status:** Approved for implementation planning +**Related:** PR #6076; MariaDB Connector/C capability patch + +## Context + +ProxySQL currently enables `CLIENT_DEPRECATE_EOF` for an outbound backend connection by changing `MYSQL::options.client_flag` before `mysql_real_connect_start()`. The bundled MariaDB Connector/C patch subsequently uses that configuration field to decide whether to retain the bit advertised by the server greeting. + +That creates an API inconsistency: the same capability passed through the `client_flag` argument to `mysql_real_connect()` is sent during the handshake, but Connector/C discards the server-advertised bit while parsing result packets. A valid deprecated-EOF result can then be parsed as legacy EOF. + +## Decision + +ProxySQL will express its outbound preference only through the local `client_flags` argument passed to `mysql_real_connect_start()`. It will no longer mutate Connector/C's persistent `MYSQL::options.client_flag` for this capability. + +The Connector/C patch will use its already-merged effective `client_flag` value when deciding whether to retain the bit from the server greeting. It must never add the bit to `mysql->server_capabilities`; a backend which does not advertise it remains a legacy-EOF backend even when ProxySQL requests it. + +Fast-forward connections retain their stricter policy: request the bit only when the frontend both negotiated it and was offered it by ProxySQL. + +## Test design + +Use ProxySQL's SQLite3 listener (port 6030) as a backend of the same ProxySQL instance. `mysql-enable_client_deprecate_eof` controls its greeting, while `mysql-enable_server_deprecate_eof` controls ProxySQL's outbound request. + +The regression coverage must prove both rows are parsed correctly: + +| SQLite3 listener advertises the capability | ProxySQL requests it | Expected backend state | +| --- | --- | --- | +| yes | yes | `server_capabilities` retains the bit; deprecated EOF is parsed | +| no | yes | `server_capabilities` lacks the bit; legacy EOF is parsed | + +The direct SQLite3 special-query test covers Connector/C's public `mysql_real_connect(..., client_flags)` API path. The existing self-loop capability-matching test covers ProxySQL's outbound connection setup. Both tests restore the global configuration after completion. + +## Non-goals + +- Do not make `CLIENT_DEPRECATE_EOF` unconditional for backends. +- Do not infer support from a requested client flag. +- Do not introduce another test service or duplicate the existing self-loop setup. From 548e033648a71bab3c531e6713466bdd55862892 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 15 Aug 2026 05:25:07 +0000 Subject: [PATCH 07/11] docs: plan backend deprecate EOF negotiation --- ...08-15-backend-deprecate-eof-negotiation.md | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-15-backend-deprecate-eof-negotiation.md diff --git a/docs/superpowers/plans/2026-08-15-backend-deprecate-eof-negotiation.md b/docs/superpowers/plans/2026-08-15-backend-deprecate-eof-negotiation.md new file mode 100644 index 0000000000..17725344b6 --- /dev/null +++ b/docs/superpowers/plans/2026-08-15-backend-deprecate-eof-negotiation.md @@ -0,0 +1,211 @@ +# Backend `CLIENT_DEPRECATE_EOF` Negotiation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make ProxySQL request backend `CLIENT_DEPRECATE_EOF` through the Connector/C connect-call flags, while retaining the server greeting as the only source of actual support. + +**Architecture:** The connector patch evaluates its already-merged effective `client_flag`, not the persistent `MYSQL::options.client_flag`. ProxySQL sets the outgoing local flags argument instead of mutating Connector/C configuration. SQLite3 Server self-loop tests independently cover servers that advertise and omit the capability. + +**Tech Stack:** C++17, MariaDB Connector/C 3.3.8 vendor patch, ProxySQL TAP tests, SQLite3 Server on port 6030, GitHub Actions g9 test jobs. + +## Global Constraints + +- Never synthesize `CLIENT_DEPRECATE_EOF` in `mysql->server_capabilities`; preserve the server greeting. +- Preserve the fast-forward requirement that frontend negotiation and frontend advertised capability both permit the request. +- Restore all modified global MySQL variables and test configuration on every test exit path. +- Cover the exact public Connector/C `mysql_real_connect(..., client_flags)` route that caused PR #6076's g9 failure. +- Add detailed Doxygen comments to ProxySQL code and explanatory comments to the vendor patch. +- Update PR #6076's body with the handshake-state explanation and test matrix. + +--- + +### Task 1: Make the direct Connector/C regression observable + +**Files:** +- Modify: `test/tap/tests/test_sqlite3_special_queries.cpp:15-130` +- Test: `test/tap/tests/test_sqlite3_special_queries_libmariadb-t` + +**Interfaces:** +- Consumes: admin connection values supplied by `CommandLine`; SQLite3 Server listener at port `6030`; `mysql-enable_client_deprecate_eof`. +- Produces: a test matrix that connects through `mysql_real_connect(..., CLIENT_DEPRECATE_EOF)` when SQLite3 Server advertises the capability and when it does not. + +- [ ] **Step 1: Add a failing direct-connect capability matrix** + + Add helpers which set and restore `mysql-enable_client_deprecate_eof` through the admin interface. For each state, connect with `CLIENT_DEPRECATE_EOF`, assert the corresponding `MYSQL::server_capabilities` bit, then run `SELECT CONNECTION_ID()` and assert one numeric row equal to `mysql_thread_id()`. + + The test cases must be named as follows: + + ```cpp + ok(server_supports_deprecate_eof == expected_server_capability, + "SQLite3 advertised CLIENT_DEPRECATE_EOF as configured"); + ok(connection_id_rc == 0 && valid_connection_id && connection_id == expected_connection_id, + "SELECT CONNECTION_ID() parses with the negotiated backend EOF mode"); + ``` + +- [ ] **Step 2: Run the MariaDB-linked TAP binary and verify RED** + + Run: + + ```bash + TAP_QUIET_ENVLOAD=1 test/tap/tests/test_sqlite3_special_queries_libmariadb-t + ``` + + Expected: the advertised-capability case fails before the Connector/C patch because the library receives a deprecated-EOF result yet has cleared the greeting bit after `mysql_real_connect(..., client_flags)`. + +- [ ] **Step 3: Keep the existing public API call shape** + + Do not assign `CLIENT_DEPRECATE_EOF` to `MYSQL::options.client_flag` in the test. The final `mysql_real_connect()` argument is the regression surface. + +- [ ] **Step 4: Commit the test-only RED change** + + ```bash + git add test/tap/tests/test_sqlite3_special_queries.cpp + git commit -m "test: cover backend deprecate EOF negotiation" + ``` + +### Task 2: Correct the connector decision and ProxySQL outbound request + +**Files:** +- Modify: `deps/mariadb-client-library/client_deprecate_eof.patch:493-501` +- Modify: `lib/mysql_connection.cpp:903-956` +- Test: `test/tap/tests/test_sqlite3_special_queries_libmariadb-t` + +**Interfaces:** +- Consumes: Connector/C's local `client_flag` after it has been OR-ed with `mysql->options.client_flag`; ProxySQL's `client_flags` reference passed to `mysql_real_connect_start()`. +- Produces: a requested capability in the outgoing connect call and a `mysql->server_capabilities` value that reflects only the backend greeting. + +- [ ] **Step 1: Change the vendor-patch condition** + + Replace the persistent-options check with the effective connection flags: + + ```c + if ((client_flag & CLIENT_DEPRECATE_EOF) == 0) { + mysql->server_capabilities &= ~CLIENT_DEPRECATE_EOF; + } + ``` + + Document in the patch that `client_flag` includes both the public connect-call argument and persistent options, and that the condition only clears a missing request; it never adds a server capability. + +- [ ] **Step 2: Move ProxySQL's request into the local outgoing flags** + + In `MySQL_Connection::connect_start_SetClientFlag`, set or clear `CLIENT_DEPRECATE_EOF` in `client_flags`. Do not write `mysql->options.client_flag` for this capability. The normal path requests it when `mysql-enable_server_deprecate_eof` is enabled or session tracking is enforced. The fast-forward path clears the local bit, then re-adds it only when the frontend actually negotiated and was advertised the capability. + + Add a Doxygen block immediately above the decision documenting all three states: + + ```cpp + /** + * @brief Select the backend CLIENT_DEPRECATE_EOF request for this connect attempt. + * @details The local connect-call flags express a client preference only. Connector/C + * records actual support from the backend greeting in server_capabilities; + * a backend that does not advertise the bit remains a legacy-EOF backend. + */ + ``` + +- [ ] **Step 3: Run the direct regression test and verify GREEN** + + Run: + + ```bash + TAP_QUIET_ENVLOAD=1 test/tap/tests/test_sqlite3_special_queries_libmariadb-t + ``` + + Expected: both advertised and non-advertised cases return a valid `CONNECTION_ID()` row; `server_capabilities` is set only in the advertised case. + +- [ ] **Step 4: Review the actual diff and commit the functional correction** + + ```bash + git diff --check + git diff -- deps/mariadb-client-library/client_deprecate_eof.patch lib/mysql_connection.cpp + git add deps/mariadb-client-library/client_deprecate_eof.patch lib/mysql_connection.cpp + git commit -m "fix: preserve backend deprecate EOF negotiation" + ``` + +### Task 3: Exercise ProxySQL's backend connect path through SQLite3 Server + +**Files:** +- Modify: `test/tap/tests/test_match_eof_conn_cap.cpp:1-975` +- Test: `test/tap/tests/test_match_eof_conn_cap-t` + +**Interfaces:** +- Consumes: existing self-loop hostgroup pointing to `127.0.0.1:6030`, `apply_proxy_conf()`, and its cleanup that reloads global configuration from disk. +- Produces: a real row-returning query through a ProxySQL backend connection for both server-advertised capability states. + +- [ ] **Step 1: Add a failing backend-result assertion** + + Extend the existing connection-acquisition path after backend creation. Execute a row-returning query through hostgroup `SQLITE3_HG` and assert that exactly one row with the expected value is returned. Run it for the two required states: + + ```cpp + { .cli_depr_eof = true, .srv_depr_eof = true, .force_mismatch = false }, + { .cli_depr_eof = false, .srv_depr_eof = true, .force_mismatch = false }, + ``` + + The latter proves a request does not fabricate server support: the SQLite3 greeting omits the bit, and the result must still parse as legacy EOF. + +- [ ] **Step 2: Run the focused self-loop TAP test** + + Run: + + ```bash + TAP_QUIET_ENVLOAD=1 test/tap/tests/test_match_eof_conn_cap-t + ``` + + Expected: the row assertions and existing connection-count and cleanup assertions pass with the Task 2 connect-call refactor. The direct Connector/C test in Task 1 remains the RED regression proof; this test verifies the separate ProxySQL outbound-connect path. + +- [ ] **Step 3: Add test-local documentation** + + Update the test's Doxygen file description to explain the asymmetric cases: `mysql-enable_server_deprecate_eof` requests the capability on the outbound connect call, while `mysql-enable_client_deprecate_eof` controls what the SQLite3 backend greeting advertises. + +- [ ] **Step 4: Commit the end-to-end regression coverage** + + ```bash + git add test/tap/tests/test_match_eof_conn_cap.cpp + git commit -m "test: verify backend deprecate EOF negotiation" + ``` + +### Task 4: Verify and document the important compatibility correction + +**Files:** +- Modify: PR #6076 body +- Test: focused MariaDB-linked special-query test; self-loop TAP test; relevant g9 CI jobs + +**Interfaces:** +- Consumes: the two regression suites and the final commits. +- Produces: a reviewable PR description and an explicit compatibility-oriented commit description. + +- [ ] **Step 1: Run final local verification** + + Run: + + ```bash + git diff --check + TAP_QUIET_ENVLOAD=1 test/tap/tests/test_sqlite3_special_queries_libmariadb-t + TAP_QUIET_ENVLOAD=1 test/tap/tests/test_match_eof_conn_cap-t + ``` + + Expected: both TAP commands exit zero, direct connection tests cover both greeting states, and the diff has no whitespace errors. + +- [ ] **Step 2: Update the PR body** + + Document: + + 1. why `MYSQL::options.client_flag` was not a safe source of truth for the `mysql_real_connect(..., client_flags)` API; + 2. why ProxySQL now requests through the local connect-call flags; + 3. why a missing backend greeting bit still produces legacy EOF parsing; + 4. the SQLite3 self-loop test matrix and local/CI verification commands. + +- [ ] **Step 3: Make the final commit description explicit** + + Use a commit message/body equivalent to: + + ```text + fix: preserve backend deprecate EOF negotiation + + Request CLIENT_DEPRECATE_EOF through the mysql_real_connect flags rather + than mutating Connector/C's persistent options. Retain the capability only + when the backend greeting advertises it, so legacy backends continue to use + legacy EOF parsing. Add direct Connector/C and SQLite3 self-loop coverage. + ``` + +- [ ] **Step 4: Push the commits and inspect the g9 checks** + + Push the PR branch, then inspect the g9 test jobs that previously failed. Report any unrelated failures separately from this regression. From 69938d11f22c9fbfbab27a2d6c291784808f1f0a Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 15 Aug 2026 05:36:00 +0000 Subject: [PATCH 08/11] test: cover backend deprecate EOF negotiation --- .../tests/test_sqlite3_special_queries.cpp | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/test/tap/tests/test_sqlite3_special_queries.cpp b/test/tap/tests/test_sqlite3_special_queries.cpp index d4589ce5fd..3e0b05a62d 100644 --- a/test/tap/tests/test_sqlite3_special_queries.cpp +++ b/test/tap/tests/test_sqlite3_special_queries.cpp @@ -59,6 +59,113 @@ const vector set_queries { constexpr int SQLITE3_SERVER_PORT { 6030 }; +static bool set_client_deprecate_eof(MYSQL* admin, bool enabled) { + const string query { + "SET mysql-enable_client_deprecate_eof=" + std::to_string(enabled ? 1 : 0) + }; + if (mysql_query(admin, query.c_str()) != 0) { + diag("Failed to set mysql-enable_client_deprecate_eof: %s", mysql_error(admin)); + return false; + } + if (mysql_query(admin, "LOAD MYSQL VARIABLES TO RUNTIME") != 0) { + diag("Failed to load mysql variables to runtime: %s", mysql_error(admin)); + return false; + } + return true; +} + +static bool get_client_deprecate_eof(MYSQL* admin, bool& enabled) { + const char* query { + "SELECT variable_value FROM global_variables " + "WHERE variable_name='mysql-enable_client_deprecate_eof'" + }; + if (mysql_query(admin, query) != 0) { + diag("Failed to read mysql-enable_client_deprecate_eof: %s", mysql_error(admin)); + return false; + } + + MYSQL_RES* result = mysql_store_result(admin); + MYSQL_ROW row = result ? mysql_fetch_row(result) : nullptr; + const char* value = row ? row[0] : nullptr; + const bool valid_value = value && + (!strcmp(value, "0") || !strcmp(value, "1") || + !strcmp(value, "false") || !strcmp(value, "true")); + if (valid_value) { + enabled = !strcmp(value, "1") || !strcmp(value, "true"); + } else { + diag("Unexpected mysql-enable_client_deprecate_eof value: %s", value ? value : "(null)"); + } + if (result) { + mysql_free_result(result); + } + return valid_value; +} + +class restore_client_deprecate_eof { + MYSQL* admin; + bool enabled; + +public: + restore_client_deprecate_eof(MYSQL* admin, bool enabled) : admin { admin }, enabled { enabled } {} + ~restore_client_deprecate_eof() { + if (!set_client_deprecate_eof(admin, enabled)) { + diag("Failed to restore mysql-enable_client_deprecate_eof"); + } + } +}; + +static void test_direct_deprecate_eof_matrix(const CommandLine& cl, MYSQL* admin) { + bool original_enabled = false; + if (!get_client_deprecate_eof(admin, original_enabled)) { + ok(false, "SQLite3 advertised CLIENT_DEPRECATE_EOF as configured"); + ok(false, "SELECT CONNECTION_ID() parses with the negotiated backend EOF mode"); + ok(false, "SQLite3 advertised CLIENT_DEPRECATE_EOF as configured"); + ok(false, "SELECT CONNECTION_ID() parses with the negotiated backend EOF mode"); + return; + } + restore_client_deprecate_eof restore { admin, original_enabled }; + + for (const bool expected_server_capability : { false, true }) { + if (!set_client_deprecate_eof(admin, expected_server_capability)) { + ok(false, "SQLite3 advertised CLIENT_DEPRECATE_EOF as configured"); + ok(false, "SELECT CONNECTION_ID() parses with the negotiated backend EOF mode"); + continue; + } + + MYSQL* proxy = mysql_init(NULL); + const bool connected = proxy && mysql_real_connect( + proxy, cl.host, cl.username, cl.password, NULL, SQLITE3_SERVER_PORT, NULL, + CLIENT_DEPRECATE_EOF + ); + const bool server_supports_deprecate_eof = connected && + (proxy->server_capabilities & CLIENT_DEPRECATE_EOF); + ok(server_supports_deprecate_eof == expected_server_capability, + "SQLite3 advertised CLIENT_DEPRECATE_EOF as configured"); + + int connection_id_rc = connected ? mysql_query(proxy, "SELECT CONNECTION_ID()") : -1; + MYSQL_RES* connection_id_result = connection_id_rc == 0 ? mysql_store_result(proxy) : nullptr; + MYSQL_ROW connection_id_row = connection_id_result ? mysql_fetch_row(connection_id_result) : nullptr; + char* parse_end = nullptr; + const unsigned long long connection_id = connection_id_row && connection_id_row[0] + ? std::strtoull(connection_id_row[0], &parse_end, 10) + : 0; + const bool valid_connection_id = connection_id_row && connection_id_row[0] + && parse_end && *parse_end == '\0' && connection_id > 0; + const unsigned long long expected_connection_id = connected + ? static_cast(mysql_thread_id(proxy)) + : 0; + if (connection_id_result) { + mysql_free_result(connection_id_result); + } + ok(connection_id_rc == 0 && valid_connection_id && connection_id == expected_connection_id, + "SELECT CONNECTION_ID() parses with the negotiated backend EOF mode"); + + if (proxy) { + mysql_close(proxy); + } + } +} + int main(int argc, char** argv) { CommandLine cl; @@ -68,7 +175,20 @@ int main(int argc, char** argv) { } const vector tests { gen_tests() }; - plan(tests.size()*(4 + set_queries.size())); + plan(4 + tests.size()*(4 + set_queries.size())); + + MYSQL* admin = mysql_init(NULL); + if (!admin || !mysql_real_connect( + admin, cl.admin_host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0 + )) { + diag("Failed to connect to the admin interface: %s", admin ? mysql_error(admin) : "mysql_init failed"); + if (admin) { + mysql_close(admin); + } + return EXIT_FAILURE; + } + test_direct_deprecate_eof_matrix(cl, admin); + mysql_close(admin); for (const test_opts_t& opts : tests) { diag("Executing test test_opts=%s", to_string(opts).c_str()); From 1de788be25f5f178b305407da54b7fb89ddf8fc2 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 15 Aug 2026 05:38:25 +0000 Subject: [PATCH 09/11] test: require one SQLite3 connection ID row --- test/tap/tests/test_sqlite3_special_queries.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/tap/tests/test_sqlite3_special_queries.cpp b/test/tap/tests/test_sqlite3_special_queries.cpp index 3e0b05a62d..f59ca42a55 100644 --- a/test/tap/tests/test_sqlite3_special_queries.cpp +++ b/test/tap/tests/test_sqlite3_special_queries.cpp @@ -149,7 +149,10 @@ static void test_direct_deprecate_eof_matrix(const CommandLine& cl, MYSQL* admin const unsigned long long connection_id = connection_id_row && connection_id_row[0] ? std::strtoull(connection_id_row[0], &parse_end, 10) : 0; - const bool valid_connection_id = connection_id_row && connection_id_row[0] + const bool valid_connection_id = connection_id_result + && mysql_num_fields(connection_id_result) == 1 + && mysql_num_rows(connection_id_result) == 1 + && connection_id_row && connection_id_row[0] && parse_end && *parse_end == '\0' && connection_id > 0; const unsigned long long expected_connection_id = connected ? static_cast(mysql_thread_id(proxy)) From 615c476131dbf7b7c8fb490b08b1e05e62f12e04 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 15 Aug 2026 05:54:49 +0000 Subject: [PATCH 10/11] fix: preserve backend deprecate EOF negotiation Pass CLIENT_DEPRECATE_EOF through the local per-attempt flags so Connector/C receives the complete request supplied to mysql_real_connect_start(). Treat the backend greeting as the source of actual support: the connector only clears an advertised capability when the effective merged client request omitted it, and never fabricates server_capabilities. --- .../client_deprecate_eof.patch | 15 ++++++----- lib/mysql_connection.cpp | 26 ++++++++++++++----- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/deps/mariadb-client-library/client_deprecate_eof.patch b/deps/mariadb-client-library/client_deprecate_eof.patch index bf8a89822e..e9640a1b47 100644 --- a/deps/mariadb-client-library/client_deprecate_eof.patch +++ b/deps/mariadb-client-library/client_deprecate_eof.patch @@ -486,17 +486,18 @@ index 75e3c9a4..916024a8 100644 { if (mysql->net.last_errno == CR_SERVER_LOST) my_set_error(mysql, CR_SERVER_LOST, SQLSTATE_UNKNOWN, -@@ -1894,6 +2049,16 @@ restart: +@@ -1894,6 +2049,17 @@ restart: } } -+ /* If client flags doesn't have 'deprecate_eof' we forcely disable the server capability, -+ * This, in combination with the capability flag 'CLIENT_DEPRECATE_EOF' being disabled by default, -+ * completely disables the library support for CLIENT_DEPRECATE_EOF. This two changes were introduced -+ * as part of ProxySQL capability for controlling 'deprecate_eof' support in both client and -+ * backend connections. For more context see: #3280. ++ /* `client_flag` is the effective request for this connection: mysql_real_connect() ++ * has already merged its public client-flag argument with mysql->options.client_flag. ++ * If that merged request omits CLIENT_DEPRECATE_EOF, hide support advertised by the ++ * server so the remaining result parser consistently expects legacy EOF packets. ++ * This check only removes an unrequested capability from the greeting; it must never ++ * add CLIENT_DEPRECATE_EOF when the server itself did not advertise support. See #3280. + */ -+ if ((mysql->options.client_flag & CLIENT_DEPRECATE_EOF) == 0) { ++ if ((client_flag & CLIENT_DEPRECATE_EOF) == 0) { + mysql->server_capabilities &= ~CLIENT_DEPRECATE_EOF; + } + diff --git a/lib/mysql_connection.cpp b/lib/mysql_connection.cpp index 58bc17bcdb..55d4ee4ba0 100644 --- a/lib/mysql_connection.cpp +++ b/lib/mysql_connection.cpp @@ -929,16 +929,28 @@ void MySQL_Connection::connect_start_SetClientFlag(unsigned long& client_flags) } } - // 'CLIENT_DEPRECATE_EOF' capability is disabled by default in mariadb_client. - // Based on the value of 'mysql-enable_server_deprecate_eof', enable this - // capability in a new connection. + /** + * @brief Select the backend CLIENT_DEPRECATE_EOF request for this connect attempt. + * @details The local connect-call flags express a client preference only. Connector/C + * records actual support from the backend greeting in server_capabilities; + * a backend that does not advertise the bit remains a legacy-EOF backend. + * + * @par Normal backend connections + * Request CLIENT_DEPRECATE_EOF when mysql-enable_server_deprecate_eof is enabled. + * @par Enforced session tracking + * Request CLIENT_DEPRECATE_EOF regardless of that setting because session tracking + * requires the deprecate-EOF protocol. + * @par Fast-forward connections + * Replace the normal preference with the frontend's negotiated state. Forward the + * request only when the frontend both requested the capability and saw it advertised. + */ if (mysql_thread___enable_server_deprecate_eof) { - mysql->options.client_flag |= CLIENT_DEPRECATE_EOF; + client_flags |= CLIENT_DEPRECATE_EOF; } // override 'mysql-enable_server_deprecate_eof' behavior if 'session_track_variables' is set to 'ENFORCED' if (mysql_thread___session_track_variables == session_track_variables::ENFORCED) { - mysql->options.client_flag |= CLIENT_DEPRECATE_EOF; + client_flags |= CLIENT_DEPRECATE_EOF; } if (myds != NULL) { @@ -947,11 +959,11 @@ void MySQL_Connection::connect_start_SetClientFlag(unsigned long& client_flags) assert(myds->sess->client_myds != NULL); MySQL_Connection * c = myds->sess->client_myds->myconn; assert(c != NULL); - mysql->options.client_flag &= ~(CLIENT_DEPRECATE_EOF); // we disable it by default + client_flags &= ~(CLIENT_DEPRECATE_EOF); // we disable it by default // if both client_flag and server_capabilities (used for client) , set CLIENT_DEPRECATE_EOF if (c->options.client_flag & CLIENT_DEPRECATE_EOF) { if (c->options.server_capabilities & CLIENT_DEPRECATE_EOF) { - mysql->options.client_flag |= CLIENT_DEPRECATE_EOF; + client_flags |= CLIENT_DEPRECATE_EOF; } } // In case of 'fast_forward', we only enable compression if both, client and backend matches. Otherwise, From 35b33df2ab4233319cc1f7b25f04d97146df02fe Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 15 Aug 2026 06:14:12 +0000 Subject: [PATCH 11/11] test: verify backend deprecate EOF negotiation --- test/tap/tests/test_match_eof_conn_cap.cpp | 55 ++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/tap/tests/test_match_eof_conn_cap.cpp b/test/tap/tests/test_match_eof_conn_cap.cpp index 791e74fecd..5fd2893dbc 100644 --- a/test/tap/tests/test_match_eof_conn_cap.cpp +++ b/test/tap/tests/test_match_eof_conn_cap.cpp @@ -16,6 +16,10 @@ * 2. Attempts to perform a query, forcing the creation of a backend conn. * 3. Performs multiple checks over the query and ProxySQL error log metrics: * - Checks if the query was expected to fail/succeed (conn creation). + * - For the two normal backend-request cases, checks that a one-row result is parsed through the + * SQLite3 self-loop. `mysql-enable_server_deprecate_eof` requests `CLIENT_DEPRECATE_EOF` on the + * outbound connect call, while `mysql-enable_client_deprecate_eof` controls whether the SQLite3 + * backend greeting advertises support. Thus both deprecated-EOF and legacy-EOF results are tested. * - Checks error log for conn creation failures (caps mismatch). * - Checks audit log for number of conn received (SQLite3 backend ProxySQL-ProxySQL). * - Checks stats on connection creation to be increased by the expected amount. @@ -326,6 +330,26 @@ struct test_cnf_t { proxy_cnf_t proxy_conf; }; +const vector backend_result_proxy_cnfs { + { .cli_depr_eof = true, .srv_depr_eof = true, .force_mismatch = false }, + { .cli_depr_eof = false, .srv_depr_eof = true, .force_mismatch = false }, +}; + +bool should_check_backend_result(const test_cnf_t& test_cnf) { + if (test_cnf.pool_status.warmup || test_cnf.conn_conf.fast_forward) { + return false; + } + + return std::any_of( + backend_result_proxy_cnfs.begin(), backend_result_proxy_cnfs.end(), + [&test_cnf] (const proxy_cnf_t& cnf) { + return cnf.cli_depr_eof == test_cnf.proxy_conf.cli_depr_eof + && cnf.srv_depr_eof == test_cnf.proxy_conf.srv_depr_eof + && cnf.force_mismatch == test_cnf.proxy_conf.force_mismatch; + } + ); +} + vector gen_all_proxy_cnfs() { vector> all_bin_vec { get_all_bin_vec(4) }; std::sort(all_bin_vec.begin(), all_bin_vec.end()); @@ -453,6 +477,35 @@ int test_conn_acquisition(MYSQL* admin, const test_cnf_t& test_conf) { mysql_query_t(proxy, "COMMIT"); } + if (rc == 0 && should_check_backend_result(test_conf)) { + const string expected_value { "proxysql-backend-deprecate-eof" }; + const string result_query { + "/* hostgroup=" + _TO_S(SQLITE3_HG) + " */ SELECT '" + expected_value + "'" + }; + diag("Issue row-returning query through SQLite3 backend query=\"%s\"", result_query.c_str()); + + const int result_rc { mysql_query_t(proxy, result_query) }; + MYSQL_RES* result { result_rc == 0 ? mysql_store_result(proxy) : nullptr }; + MYSQL_ROW row { result ? mysql_fetch_row(result) : nullptr }; + const bool valid_result { + result + && mysql_num_fields(result) == 1 + && mysql_num_rows(result) == 1 + && row && row[0] + && expected_value == row[0] + }; + + ok( + result_rc == 0 && valid_result, + "Backend result should parse with negotiated EOF mode proxy_conf='%s'", + to_string(proxy_cnf).c_str() + ); + + if (result) { + mysql_free_result(result); + } + } + // Sanity check; query should **NEVER** fail if mismatch is allowed (no fast-forward). if (rc != 0 && !conn_cnf.fast_forward) { diag( @@ -939,6 +992,8 @@ int main(int argc, char** argv) { #else * 2 #endif + // gen_all_proxy_cnfs() projects a four-bit matrix onto three fields, so each case occurs twice. + + 2 * backend_result_proxy_cnfs.size() ); MYSQL* admin { create_mysql_conn({ cl.admin_host, cl.admin_username, cl.admin_password, cl.admin_port }) };