/** * @file pgsql_txn_state_unit-t.cpp * @brief Unit tests for PgSQL_TxnCmdParser and PgSQL_ExplicitTxnStateMgr. * * Tests the pure parsing logic of PgSQL_TxnCmdParser::parse() which tokenizes * SQL transaction commands and returns a TxnCmd struct with .type and .savepoint. * * Also tests the PgSQL_ExplicitTxnStateMgr state machine, including * handle_transaction(), savepoint management, reset_state(), accessors, * and fill_internal_session() JSON serialization. These tests require * a minimal PgSQL_Session with a wired-up client_myds->myconn chain. */ #include "tap.h" #include "test_globals.h" #include "test_init.h" #include "proxysql.h" #include "PgSQL_ExplicitTxnStateMgr.h" #include "PgSQL_Session.h" #include "PgSQL_Data_Stream.h" #include "PgSQL_Connection.h" #include "PgSQL_Backend.h" #include "../deps/json/json.hpp" #include #include // ============================================================ // BEGIN variants // ============================================================ static void test_parser_begin() { PgSQL_TxnCmdParser parser; auto result = parser.parse("BEGIN"); ok(result.type == TxnCmd::Type::BEGIN, "parser: BEGIN recognized"); } static void test_parser_begin_variants() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse("BEGIN TRANSACTION"); ok(r1.type == TxnCmd::Type::BEGIN, "parser: BEGIN TRANSACTION recognized"); auto r2 = parser.parse("START TRANSACTION"); ok(r2.type == TxnCmd::Type::BEGIN, "parser: START TRANSACTION recognized"); auto r3 = parser.parse("begin"); ok(r3.type == TxnCmd::Type::BEGIN, "parser: case-insensitive begin"); auto r4 = parser.parse("Begin"); ok(r4.type == TxnCmd::Type::BEGIN, "parser: mixed-case Begin"); // START without TRANSACTION should be UNKNOWN auto r5 = parser.parse("START"); ok(r5.type == TxnCmd::Type::UNKNOWN, "parser: bare START is UNKNOWN (needs TRANSACTION)"); // BEGIN WORK is not standard PgSQL but test parser behavior auto r6 = parser.parse("BEGIN WORK"); ok(r6.type == TxnCmd::Type::BEGIN, "parser: BEGIN WORK recognized"); } // ============================================================ // COMMIT variants // ============================================================ static void test_parser_commit() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse("COMMIT"); ok(r1.type == TxnCmd::Type::COMMIT, "parser: COMMIT recognized"); auto r2 = parser.parse("commit"); ok(r2.type == TxnCmd::Type::COMMIT, "parser: case-insensitive commit"); // END is a synonym for COMMIT in PostgreSQL auto r3 = parser.parse("END"); ok(r3.type == TxnCmd::Type::COMMIT, "parser: END recognized as COMMIT synonym"); auto r4 = parser.parse("end"); ok(r4.type == TxnCmd::Type::COMMIT, "parser: case-insensitive end"); } // ============================================================ // ROLLBACK variants // ============================================================ static void test_parser_rollback() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse("ROLLBACK"); ok(r1.type == TxnCmd::Type::ROLLBACK, "parser: ROLLBACK recognized"); auto r2 = parser.parse("rollback"); ok(r2.type == TxnCmd::Type::ROLLBACK, "parser: case-insensitive rollback"); // ABORT is a synonym for ROLLBACK in PostgreSQL auto r3 = parser.parse("ABORT"); ok(r3.type == TxnCmd::Type::ROLLBACK, "parser: ABORT recognized as ROLLBACK synonym"); auto r4 = parser.parse("abort"); ok(r4.type == TxnCmd::Type::ROLLBACK, "parser: case-insensitive abort"); // ROLLBACK WORK / ROLLBACK TRANSACTION auto r5 = parser.parse("ROLLBACK WORK"); ok(r5.type == TxnCmd::Type::ROLLBACK, "parser: ROLLBACK WORK recognized"); auto r6 = parser.parse("ROLLBACK TRANSACTION"); ok(r6.type == TxnCmd::Type::ROLLBACK, "parser: ROLLBACK TRANSACTION recognized"); } // ============================================================ // ROLLBACK AND CHAIN // ============================================================ static void test_parser_rollback_and_chain() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse("ROLLBACK AND CHAIN"); ok(r1.type == TxnCmd::Type::ROLLBACK_AND_CHAIN, "parser: ROLLBACK AND CHAIN recognized"); auto r2 = parser.parse("rollback and chain"); ok(r2.type == TxnCmd::Type::ROLLBACK_AND_CHAIN, "parser: case-insensitive rollback and chain"); // ABORT AND CHAIN should also work (ABORT is synonym) auto r3 = parser.parse("ABORT AND CHAIN"); ok(r3.type == TxnCmd::Type::ROLLBACK_AND_CHAIN, "parser: ABORT AND CHAIN recognized"); } // ============================================================ // SAVEPOINT // ============================================================ static void test_parser_savepoint() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse("SAVEPOINT sp1"); ok(r1.type == TxnCmd::Type::SAVEPOINT, "parser: SAVEPOINT recognized"); ok(r1.savepoint == "sp1", "parser: savepoint name 'sp1' extracted"); auto r2 = parser.parse("SAVEPOINT my_save_point"); ok(r2.type == TxnCmd::Type::SAVEPOINT, "parser: SAVEPOINT with underscore name"); ok(r2.savepoint == "my_save_point", "parser: savepoint name 'my_save_point' extracted"); auto r3 = parser.parse("savepoint SP1"); ok(r3.type == TxnCmd::Type::SAVEPOINT, "parser: case-insensitive savepoint keyword"); ok(r3.savepoint == "SP1", "parser: savepoint name preserves original case"); } // ============================================================ // ROLLBACK TO SAVEPOINT // ============================================================ static void test_parser_rollback_to_savepoint() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse("ROLLBACK TO SAVEPOINT sp1"); ok(r1.type == TxnCmd::Type::ROLLBACK_TO, "parser: ROLLBACK TO SAVEPOINT recognized"); ok(r1.savepoint == "sp1", "parser: ROLLBACK TO SAVEPOINT extracts name"); // Without SAVEPOINT keyword auto r2 = parser.parse("ROLLBACK TO sp1"); ok(r2.type == TxnCmd::Type::ROLLBACK_TO, "parser: ROLLBACK TO sp1 (no SAVEPOINT keyword)"); ok(r2.savepoint == "sp1", "parser: ROLLBACK TO extracts name without SAVEPOINT keyword"); // Case insensitive auto r3 = parser.parse("rollback to savepoint SP2"); ok(r3.type == TxnCmd::Type::ROLLBACK_TO, "parser: case-insensitive rollback to savepoint"); ok(r3.savepoint == "SP2", "parser: savepoint name preserved in rollback to"); } // ============================================================ // RELEASE SAVEPOINT // ============================================================ static void test_parser_release_savepoint() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse("RELEASE SAVEPOINT sp1"); ok(r1.type == TxnCmd::Type::RELEASE, "parser: RELEASE SAVEPOINT recognized"); ok(r1.savepoint == "sp1", "parser: RELEASE SAVEPOINT extracts name"); // Without SAVEPOINT keyword auto r2 = parser.parse("RELEASE sp1"); ok(r2.type == TxnCmd::Type::RELEASE, "parser: RELEASE sp1 (no SAVEPOINT keyword)"); ok(r2.savepoint == "sp1", "parser: RELEASE extracts name without SAVEPOINT keyword"); // Case insensitive auto r3 = parser.parse("release savepoint SP2"); ok(r3.type == TxnCmd::Type::RELEASE, "parser: case-insensitive release savepoint"); ok(r3.savepoint == "SP2", "parser: savepoint name preserved in release"); } // ============================================================ // Non-transaction commands and edge cases // ============================================================ static void test_parser_non_txn_commands() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse("SELECT 1"); ok(r1.type == TxnCmd::Type::UNKNOWN, "parser: SELECT is UNKNOWN"); auto r2 = parser.parse("INSERT INTO t VALUES (1)"); ok(r2.type == TxnCmd::Type::UNKNOWN, "parser: INSERT is UNKNOWN"); auto r3 = parser.parse("SET search_path TO public"); ok(r3.type == TxnCmd::Type::UNKNOWN, "parser: SET is UNKNOWN"); } static void test_parser_empty_and_whitespace() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse(""); ok(r1.type == TxnCmd::Type::UNKNOWN, "parser: empty input = UNKNOWN"); auto r2 = parser.parse(" "); ok(r2.type == TxnCmd::Type::UNKNOWN, "parser: whitespace-only = UNKNOWN"); } static void test_parser_semicolons() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse("BEGIN;"); ok(r1.type == TxnCmd::Type::BEGIN, "parser: BEGIN with trailing semicolon"); auto r2 = parser.parse("COMMIT;"); ok(r2.type == TxnCmd::Type::COMMIT, "parser: COMMIT with trailing semicolon"); auto r3 = parser.parse("ROLLBACK;"); ok(r3.type == TxnCmd::Type::ROLLBACK, "parser: ROLLBACK with trailing semicolon"); } static void test_parser_leading_whitespace() { PgSQL_TxnCmdParser parser; auto r1 = parser.parse(" BEGIN"); ok(r1.type == TxnCmd::Type::BEGIN, "parser: leading whitespace before BEGIN"); auto r2 = parser.parse("\tCOMMIT"); ok(r2.type == TxnCmd::Type::COMMIT, "parser: leading tab before COMMIT"); } static void test_parser_reuse() { // Verify parser can be reused for multiple parses PgSQL_TxnCmdParser parser; auto r1 = parser.parse("BEGIN"); ok(r1.type == TxnCmd::Type::BEGIN, "parser reuse: first parse BEGIN"); auto r2 = parser.parse("COMMIT"); ok(r2.type == TxnCmd::Type::COMMIT, "parser reuse: second parse COMMIT"); auto r3 = parser.parse("SAVEPOINT sp1"); ok(r3.type == TxnCmd::Type::SAVEPOINT, "parser reuse: third parse SAVEPOINT"); ok(r3.savepoint == "sp1", "parser reuse: savepoint name correct after reuse"); auto r4 = parser.parse("ROLLBACK TO sp1"); ok(r4.type == TxnCmd::Type::ROLLBACK_TO, "parser reuse: fourth parse ROLLBACK TO"); ok(r4.savepoint == "sp1", "parser reuse: rollback to name correct after reuse"); } // ============================================================ // PgSQL_ExplicitTxnStateMgr — State Machine Tests // ============================================================ /** * @brief Helper: Create a minimal PgSQL_Session suitable for state manager testing. * * Allocates a PgSQL_Session, sets connections_handler=true to avoid * PgHGM dereference in destructor, creates a client data stream and * connection, and wires them together. * * @return Pointer to the newly created session. Caller must use * destroy_test_session() to clean up. */ static PgSQL_Session* create_test_session() { PgSQL_Session* sess = new PgSQL_Session(); // Prevent destructor from dereferencing PgHGM (which is nullptr in tests) sess->connections_handler = true; // Create client data stream and connection PgSQL_Data_Stream* ds = new PgSQL_Data_Stream(); ds->myds_type = MYDS_FRONTEND; PgSQL_Connection* conn = new PgSQL_Connection(true); ds->myconn = conn; // Populate critical variables (indices 0..PGSQL_NAME_LAST_LOW_WM-1). // The state manager's start_transaction() asserts that these have // non-zero hashes. Use the default values from pgsql_tracked_variables[]. for (int idx = 0; idx < PGSQL_NAME_LAST_LOW_WM; idx++) { const char* def_val = pgsql_tracked_variables[idx].default_value; conn->variables[idx].value = strdup(def_val); conn->var_hash[idx] = SpookyHash::Hash32(def_val, strlen(def_val), 10); } sess->client_myds = ds; // Create a backend with server_myds->myconn for verify_server_variables() // and rollback(). In DEBUG builds, commit() and rollback() call // verify_server_variables() which dereferences session->mybe->server_myds->myconn. PgSQL_Backend* be = new PgSQL_Backend(); PgSQL_Data_Stream* server_ds = new PgSQL_Data_Stream(); PgSQL_Connection* server_conn = new PgSQL_Connection(false); // Populate server connection with same critical variables for (int idx = 0; idx < PGSQL_NAME_LAST_LOW_WM; idx++) { const char* def_val = pgsql_tracked_variables[idx].default_value; server_conn->variables[idx].value = strdup(def_val); server_conn->var_hash[idx] = SpookyHash::Hash32(def_val, strlen(def_val), 10); } server_ds->myconn = server_conn; be->server_myds = server_ds; sess->mybe = be; return sess; } /** * @brief Helper: Safely destroy a test session created by create_test_session(). * * Sets client_myds to nullptr before destruction to avoid the session * destructor calling myconn->reset() (which asserts on startup_parameters). * Manually cleans up the data stream and connection. */ static void destroy_test_session(PgSQL_Session* sess) { // Clean up backend before session destruction if (sess->mybe) { PgSQL_Backend* be = sess->mybe; if (be->server_myds) { PgSQL_Data_Stream* sds = be->server_myds; if (sds->myconn) { delete sds->myconn; sds->myconn = nullptr; } be->server_myds = nullptr; delete sds; } sess->mybe = nullptr; delete be; } if (sess->client_myds) { PgSQL_Data_Stream* ds = sess->client_myds; // Detach connection before data stream destruction to avoid // the data stream destructor also deleting it PgSQL_Connection* conn = ds->myconn; ds->myconn = nullptr; delete conn; sess->client_myds = nullptr; delete ds; } delete sess; } // ============================================================ // Accessors on empty state (nullptr session is safe) // ============================================================ static void test_statemgr_initial_state() { PgSQL_ExplicitTxnStateMgr mgr(nullptr); ok(mgr.is_in_transaction() == false, "statemgr: initially not in transaction"); ok(mgr.get_savepoint_count() == 0, "statemgr: initially zero savepoints"); } static void test_statemgr_reset_empty() { PgSQL_ExplicitTxnStateMgr mgr(nullptr); // reset_state() on empty state should be a no-op (no crash) mgr.reset_state(); ok(mgr.is_in_transaction() == false, "statemgr: still not in transaction after reset on empty"); ok(mgr.get_savepoint_count() == 0, "statemgr: still zero savepoints after reset on empty"); } static void test_statemgr_fill_internal_session_empty() { PgSQL_ExplicitTxnStateMgr mgr(nullptr); nlohmann::json j; mgr.fill_internal_session(j); // When not in a transaction, fill_internal_session should not add anything ok(j.empty(), "statemgr: fill_internal_session produces empty JSON when no transaction"); } // ============================================================ // handle_transaction() — unknown command // ============================================================ static void test_statemgr_handle_unknown() { PgSQL_ExplicitTxnStateMgr mgr(nullptr); bool result = mgr.handle_transaction("SELECT 1"); ok(result == false, "statemgr: handle_transaction returns false for unknown command"); ok(mgr.is_in_transaction() == false, "statemgr: not in transaction after unknown command"); } // ============================================================ // BEGIN / COMMIT lifecycle // ============================================================ static void test_statemgr_begin_commit() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; bool r1 = mgr->handle_transaction("BEGIN"); ok(r1 == true, "statemgr: BEGIN returns true"); ok(mgr->is_in_transaction() == true, "statemgr: in transaction after BEGIN"); ok(mgr->get_savepoint_count() == 0, "statemgr: zero savepoints after BEGIN"); bool r2 = mgr->handle_transaction("COMMIT"); ok(r2 == true, "statemgr: COMMIT returns true"); ok(mgr->is_in_transaction() == false, "statemgr: not in transaction after COMMIT"); ok(mgr->get_savepoint_count() == 0, "statemgr: zero savepoints after COMMIT"); destroy_test_session(sess); } // ============================================================ // BEGIN / ROLLBACK lifecycle // ============================================================ static void test_statemgr_begin_rollback() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); ok(mgr->is_in_transaction() == true, "statemgr: in transaction after BEGIN (rollback test)"); bool r1 = mgr->handle_transaction("ROLLBACK"); ok(r1 == true, "statemgr: ROLLBACK returns true"); ok(mgr->is_in_transaction() == false, "statemgr: not in transaction after ROLLBACK"); destroy_test_session(sess); } // ============================================================ // BEGIN / ROLLBACK AND CHAIN lifecycle // ============================================================ static void test_statemgr_rollback_and_chain() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); ok(mgr->is_in_transaction() == true, "statemgr: in transaction before ROLLBACK AND CHAIN"); bool r1 = mgr->handle_transaction("ROLLBACK AND CHAIN"); ok(r1 == true, "statemgr: ROLLBACK AND CHAIN returns true"); // ROLLBACK AND CHAIN keeps the initial snapshot (stays in transaction) ok(mgr->is_in_transaction() == true, "statemgr: still in transaction after ROLLBACK AND CHAIN"); ok(mgr->get_savepoint_count() == 0, "statemgr: savepoints cleared after ROLLBACK AND CHAIN"); // Clean up by committing mgr->handle_transaction("COMMIT"); ok(mgr->is_in_transaction() == false, "statemgr: not in transaction after final COMMIT"); destroy_test_session(sess); } // ============================================================ // SAVEPOINT management // ============================================================ static void test_statemgr_savepoint_add() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); bool r1 = mgr->handle_transaction("SAVEPOINT sp1"); ok(r1 == true, "statemgr: SAVEPOINT sp1 returns true"); ok(mgr->get_savepoint_count() == 1, "statemgr: one savepoint after SAVEPOINT sp1"); bool r2 = mgr->handle_transaction("SAVEPOINT sp2"); ok(r2 == true, "statemgr: SAVEPOINT sp2 returns true"); ok(mgr->get_savepoint_count() == 2, "statemgr: two savepoints after SAVEPOINT sp2"); // Duplicate savepoint name should fail bool r3 = mgr->handle_transaction("SAVEPOINT sp1"); ok(r3 == false, "statemgr: duplicate SAVEPOINT sp1 returns false"); ok(mgr->get_savepoint_count() == 2, "statemgr: still two savepoints after duplicate"); mgr->handle_transaction("COMMIT"); ok(mgr->get_savepoint_count() == 0, "statemgr: zero savepoints after COMMIT"); destroy_test_session(sess); } // ============================================================ // RELEASE SAVEPOINT // ============================================================ static void test_statemgr_release_savepoint() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); mgr->handle_transaction("SAVEPOINT sp1"); mgr->handle_transaction("SAVEPOINT sp2"); ok(mgr->get_savepoint_count() == 2, "statemgr: two savepoints before RELEASE"); // Release sp1 — this should also remove sp2 (everything after sp1) bool r1 = mgr->handle_transaction("RELEASE SAVEPOINT sp1"); ok(r1 == true, "statemgr: RELEASE SAVEPOINT sp1 returns true"); ok(mgr->get_savepoint_count() == 0, "statemgr: zero savepoints after RELEASE sp1 (removes sp1 and later)"); ok(mgr->is_in_transaction() == true, "statemgr: still in transaction after RELEASE"); // Release non-existent savepoint should fail bool r2 = mgr->handle_transaction("RELEASE SAVEPOINT nonexistent"); ok(r2 == false, "statemgr: RELEASE non-existent savepoint returns false"); mgr->handle_transaction("COMMIT"); destroy_test_session(sess); } // ============================================================ // ROLLBACK TO SAVEPOINT // ============================================================ static void test_statemgr_rollback_to_savepoint() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); mgr->handle_transaction("SAVEPOINT sp1"); mgr->handle_transaction("SAVEPOINT sp2"); mgr->handle_transaction("SAVEPOINT sp3"); ok(mgr->get_savepoint_count() == 3, "statemgr: three savepoints before ROLLBACK TO"); // Rollback to sp2 — this should remove sp2 and sp3 state, keeping sp1 bool r1 = mgr->handle_transaction("ROLLBACK TO SAVEPOINT sp2"); ok(r1 == true, "statemgr: ROLLBACK TO sp2 returns true"); ok(mgr->get_savepoint_count() == 1, "statemgr: one savepoint after ROLLBACK TO sp2"); ok(mgr->is_in_transaction() == true, "statemgr: still in transaction after ROLLBACK TO"); // Rollback to non-existent savepoint should fail bool r2 = mgr->handle_transaction("ROLLBACK TO SAVEPOINT nonexistent"); ok(r2 == false, "statemgr: ROLLBACK TO non-existent savepoint returns false"); mgr->handle_transaction("COMMIT"); destroy_test_session(sess); } // ============================================================ // reset_state() with active transaction // ============================================================ static void test_statemgr_reset_active_transaction() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); mgr->handle_transaction("SAVEPOINT sp1"); ok(mgr->is_in_transaction() == true, "statemgr: in transaction before reset"); ok(mgr->get_savepoint_count() == 1, "statemgr: one savepoint before reset"); mgr->reset_state(); ok(mgr->is_in_transaction() == false, "statemgr: not in transaction after reset_state"); ok(mgr->get_savepoint_count() == 0, "statemgr: zero savepoints after reset_state"); destroy_test_session(sess); } // ============================================================ // fill_internal_session() with active transaction // ============================================================ static void test_statemgr_fill_internal_session_active() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); mgr->handle_transaction("SAVEPOINT sp1"); nlohmann::json j; mgr->fill_internal_session(j); // Should have "initial_state" key ok(j.contains("initial_state"), "statemgr: JSON contains 'initial_state' after BEGIN"); // Should have "savepoints" with "sp1" key ok(j.contains("savepoints"), "statemgr: JSON contains 'savepoints' after SAVEPOINT"); ok(j["savepoints"].contains("sp1"), "statemgr: JSON savepoints contains 'sp1'"); mgr->reset_state(); destroy_test_session(sess); } // ============================================================ // fill_internal_session() — transaction without savepoints // ============================================================ static void test_statemgr_fill_internal_session_no_savepoints() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); nlohmann::json j; mgr->fill_internal_session(j); ok(j.contains("initial_state"), "statemgr: JSON has 'initial_state' with no savepoints"); ok(!j.contains("savepoints"), "statemgr: JSON has no 'savepoints' key when none exist"); mgr->reset_state(); destroy_test_session(sess); } // NOTE: Duplicate BEGIN (BEGIN while already in transaction) is not tested here // because it requires NumActiveTransactions() > 0, which needs a fully wired // backend connection with an active transaction — too complex for unit testing. // ============================================================ // SAVEPOINT case-insensitive lookup // ============================================================ static void test_statemgr_savepoint_case_insensitive() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); mgr->handle_transaction("SAVEPOINT MyPoint"); ok(mgr->get_savepoint_count() == 1, "statemgr: one savepoint after SAVEPOINT MyPoint"); // Duplicate with different case should fail (case-insensitive match) bool r = mgr->handle_transaction("SAVEPOINT mypoint"); ok(r == false, "statemgr: duplicate savepoint with different case returns false"); ok(mgr->get_savepoint_count() == 1, "statemgr: still one savepoint after case-insensitive dup"); // Release with different case should succeed bool r2 = mgr->handle_transaction("RELEASE SAVEPOINT MYPOINT"); ok(r2 == true, "statemgr: RELEASE with different case succeeds"); ok(mgr->get_savepoint_count() == 0, "statemgr: zero savepoints after case-insensitive RELEASE"); mgr->handle_transaction("COMMIT"); destroy_test_session(sess); } // ============================================================ // END as COMMIT synonym through state manager // ============================================================ static void test_statemgr_end_as_commit() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); ok(mgr->is_in_transaction() == true, "statemgr: in transaction before END"); bool r = mgr->handle_transaction("END"); ok(r == true, "statemgr: END returns true"); ok(mgr->is_in_transaction() == false, "statemgr: not in transaction after END (COMMIT synonym)"); destroy_test_session(sess); } // ============================================================ // ABORT as ROLLBACK synonym through state manager // ============================================================ static void test_statemgr_abort_as_rollback() { PgSQL_Session* sess = create_test_session(); PgSQL_ExplicitTxnStateMgr* mgr = sess->transaction_state_manager; mgr->handle_transaction("BEGIN"); ok(mgr->is_in_transaction() == true, "statemgr: in transaction before ABORT"); bool r = mgr->handle_transaction("ABORT"); ok(r == true, "statemgr: ABORT returns true"); ok(mgr->is_in_transaction() == false, "statemgr: not in transaction after ABORT (ROLLBACK synonym)"); destroy_test_session(sess); } int main() { plan(112); test_init_minimal(); // --- Parser tests (54 tests) --- test_parser_begin(); // 1 test_parser_begin_variants(); // 7 test_parser_commit(); // 4 test_parser_rollback(); // 6 test_parser_rollback_and_chain(); // 3 test_parser_savepoint(); // 6 test_parser_rollback_to_savepoint(); // 6 test_parser_release_savepoint(); // 6 test_parser_non_txn_commands(); // 3 test_parser_empty_and_whitespace(); // 2 test_parser_semicolons(); // 3 test_parser_leading_whitespace(); // 2 test_parser_reuse(); // 6 // Parser subtotal: 54 // --- State Manager tests (58 tests) --- test_statemgr_initial_state(); // 2 test_statemgr_reset_empty(); // 2 test_statemgr_fill_internal_session_empty(); // 1 test_statemgr_handle_unknown(); // 2 test_statemgr_begin_commit(); // 6 test_statemgr_begin_rollback(); // 3 test_statemgr_rollback_and_chain(); // 5 test_statemgr_savepoint_add(); // 7 test_statemgr_release_savepoint(); // 5 test_statemgr_rollback_to_savepoint(); // 5 test_statemgr_reset_active_transaction(); // 4 test_statemgr_fill_internal_session_active(); // 3 test_statemgr_fill_internal_session_no_savepoints(); // 2 test_statemgr_savepoint_case_insensitive(); // 5 test_statemgr_end_as_commit(); // 3 test_statemgr_abort_as_rollback(); // 3 // State manager subtotal: 58 // Grand total: 54 + 58 = 112 test_cleanup_minimal(); return exit_status(); }