From 2476c566e31b2b578170c485b968a5f0712c9369 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Wed, 24 Dec 2025 16:09:10 -0300 Subject: [PATCH] Feature #11 - Multi-database transaction (2PC) support --- src/fb-cpp/SmartPtrs.h | 3 +- src/fb-cpp/Transaction.cpp | 92 ++++++++- src/fb-cpp/Transaction.h | 116 ++++++++++- src/test/Transaction2PC.cpp | 388 ++++++++++++++++++++++++++++++++++++ 4 files changed, 581 insertions(+), 18 deletions(-) create mode 100644 src/test/Transaction2PC.cpp diff --git a/src/fb-cpp/SmartPtrs.h b/src/fb-cpp/SmartPtrs.h index 5f1b32f..f88de32 100644 --- a/src/fb-cpp/SmartPtrs.h +++ b/src/fb-cpp/SmartPtrs.h @@ -40,7 +40,8 @@ namespace fbcpp { void operator()(fb::IDisposable* obj) noexcept { - obj->dispose(); + if (obj) + obj->dispose(); } }; } // namespace impl diff --git a/src/fb-cpp/Transaction.cpp b/src/fb-cpp/Transaction.cpp index ed3866a..e137f57 100644 --- a/src/fb-cpp/Transaction.cpp +++ b/src/fb-cpp/Transaction.cpp @@ -32,16 +32,9 @@ using namespace fbcpp; using namespace fbcpp::impl; -Transaction::Transaction(Attachment& attachment, const TransactionOptions& options) - : client{attachment.getClient()} +static FbUniquePtr buildTpb( + fb::IMaster* master, StatusWrapper& statusWrapper, const TransactionOptions& options) { - assert(attachment.isValid()); - - const auto master = client.getMaster(); - - const auto status = client.newStatus(); - StatusWrapper statusWrapper{client, status.get()}; - auto tpbBuilder = fbUnique(master->getUtilInterface()->getXpbBuilder(&statusWrapper, fb::IXpbBuilder::TPB, reinterpret_cast(options.getTpb().data()), static_cast(options.getTpb().size()))); @@ -135,6 +128,21 @@ Transaction::Transaction(Attachment& attachment, const TransactionOptions& optio if (options.getAutoCommit()) tpbBuilder->insertTag(&statusWrapper, isc_tpb_autocommit); + return tpbBuilder; +} + + +Transaction::Transaction(Attachment& attachment, const TransactionOptions& options) + : client{attachment.getClient()} +{ + assert(attachment.isValid()); + + const auto master = client.getMaster(); + + const auto status = client.newStatus(); + StatusWrapper statusWrapper{client, status.get()}; + + auto tpbBuilder = buildTpb(master, statusWrapper, options); const auto tpbBuffer = tpbBuilder->getBuffer(&statusWrapper); const auto tpbBufferLen = tpbBuilder->getBufferLength(&statusWrapper); @@ -154,32 +162,72 @@ Transaction::Transaction(Attachment& attachment, std::string_view setTransaction setTransactionCmd.data(), SQL_DIALECT_V6, nullptr, nullptr, nullptr, nullptr)); } +Transaction::Transaction(std::span> attachments, const TransactionOptions& options) + : client{attachments[0].get().getClient()}, + isMultiDatabase{true} +{ + assert(!attachments.empty()); + + // Validate all attachments use the same Client + for (const auto& attachment : attachments) + { + assert(attachment.get().isValid()); + + if (&attachment.get().getClient() != &client) + throw std::invalid_argument("All attachments must use the same Client for multi-database transactions"); + } + + const auto master = client.getMaster(); + + const auto status = client.newStatus(); + StatusWrapper statusWrapper{client, status.get()}; + + auto tpbBuilder = buildTpb(master, statusWrapper, options); + const auto tpbBuffer = tpbBuilder->getBuffer(&statusWrapper); + const auto tpbBufferLen = tpbBuilder->getBufferLength(&statusWrapper); + + auto dtcInterface = master->getDtc(); + auto dtcStart = fbUnique(dtcInterface->startBuilder(&statusWrapper)); + + // Add each attachment with the same TPB + for (const auto& attachment : attachments) + dtcStart->addWithTpb(&statusWrapper, attachment.get().getHandle().get(), tpbBufferLen, tpbBuffer); + + // Start the multi-database transaction, which disposes the IDtcStart instance + handle.reset(dtcStart->start(&statusWrapper)); + dtcStart.release(); +} + void Transaction::rollback() { assert(isValid()); + assert(state == TransactionState::ACTIVE || state == TransactionState::PREPARED); const auto status = client.newStatus(); StatusWrapper statusWrapper{client, status.get()}; handle->rollback(&statusWrapper); handle.reset(); + state = TransactionState::ROLLED_BACK; } - void Transaction::commit() { assert(isValid()); + assert(state == TransactionState::ACTIVE || state == TransactionState::PREPARED); const auto status = client.newStatus(); StatusWrapper statusWrapper{client, status.get()}; handle->commit(&statusWrapper); handle.reset(); + state = TransactionState::COMMITTED; } void Transaction::commitRetaining() { assert(isValid()); + assert(state == TransactionState::ACTIVE); const auto status = client.newStatus(); StatusWrapper statusWrapper{client, status.get()}; @@ -190,9 +238,33 @@ void Transaction::commitRetaining() void Transaction::rollbackRetaining() { assert(isValid()); + assert(state == TransactionState::ACTIVE); const auto status = client.newStatus(); StatusWrapper statusWrapper{client, status.get()}; handle->rollbackRetaining(&statusWrapper); } + +void Transaction::prepare() +{ + prepare(std::span{}); +} + +void Transaction::prepare(std::span message) +{ + assert(isValid()); + assert(state == TransactionState::ACTIVE); + + const auto status = client.newStatus(); + StatusWrapper statusWrapper{client, status.get()}; + + handle->prepare(&statusWrapper, static_cast(message.size()), message.data()); + state = TransactionState::PREPARED; +} + +void Transaction::prepare(std::string_view message) +{ + const auto messageBytes = reinterpret_cast(message.data()); + prepare(std::span{messageBytes, message.size()}); +} diff --git a/src/fb-cpp/Transaction.h b/src/fb-cpp/Transaction.h index 60db1d5..66d4594 100644 --- a/src/fb-cpp/Transaction.h +++ b/src/fb-cpp/Transaction.h @@ -29,6 +29,7 @@ #include "SmartPtrs.h" #include #include +#include #include #include #include @@ -300,16 +301,55 @@ namespace fbcpp class Client; /// - /// Represents a transaction in a Firebird database. + /// Transaction state for tracking two-phase commit lifecycle. + /// + enum class TransactionState + { + /// + /// Transaction is active and can execute statements. + /// + ACTIVE, + + /// + /// Transaction has been prepared (2PC phase 1). + /// + PREPARED, + + /// + /// Transaction has been committed. + /// + COMMITTED, + + /// + /// Transaction has been rolled back. + /// + ROLLED_BACK + }; + + /// + /// Represents a transaction in one or more Firebird databases. + /// + /// Single-database transactions are created using the Attachment constructor. + /// Multi-database transactions are created using the vector of Attachments constructor + /// and support two-phase commit (2PC) protocol via the prepare() method. + /// + /// For 2PC: + /// 1. Create multi-database transaction with multiple Attachments + /// 2. Execute statements across databases + /// 3. Call prepare() to enter prepared state + /// 4. Call commit() or rollback() to complete + /// + /// Important: Prepared transactions MUST be explicitly committed or rolled back. + /// The destructor will NOT automatically rollback prepared transactions. + /// /// The Transaction must exist and remain valid while there are other objects /// using it, such as Statement. If a Transaction object is destroyed before - /// being committed or rolled back, it will be automatically rolled back. + /// being committed or rolled back (and not prepared), it will be automatically + /// rolled back. /// class Transaction final { public: - //// TODO: 2PC transactions. - /// /// Constructs a Transaction object that starts a transaction in the specified /// Attachment using the specified options. @@ -322,14 +362,29 @@ namespace fbcpp /// explicit Transaction(Attachment& attachment, std::string_view setTransactionCmd); + /// + /// Constructs a Transaction object that starts a multi-database transaction + /// across the specified Attachments using the specified options. + /// + /// All attachments must use the same Client. The same TransactionOptions + /// (TPB) will be applied to all databases. + /// + /// This constructor enables two-phase commit (2PC) support via the prepare() method. + /// + explicit Transaction( + std::span> attachments, const TransactionOptions& options = {}); + /// /// Move constructor. /// A moved Transaction object becomes invalid. /// Transaction(Transaction&& o) noexcept : client{o.client}, - handle{std::move(o.handle)} + handle{std::move(o.handle)}, + state{o.state}, + isMultiDatabase{o.isMultiDatabase} { + o.state = TransactionState::ROLLED_BACK; } Transaction& operator=(Transaction&&) = delete; @@ -340,13 +395,20 @@ namespace fbcpp /// /// Rolls back the transaction if it is still active. /// + /// Prepared transactions are NOT automatically rolled back and must be + /// explicitly committed or rolled back before destruction. + /// ~Transaction() noexcept { if (isValid()) { + assert(state != TransactionState::PREPARED && + "Prepared transaction must be explicitly committed or rolled back"); + try { - rollback(); + if (state == TransactionState::ACTIVE) + rollback(); } catch (...) { @@ -372,29 +434,69 @@ namespace fbcpp return handle; } + /// + /// Returns the current transaction state. + /// + TransactionState getState() const noexcept + { + return state; + } + + /// + /// Prepares the transaction for two-phase commit (2PC phase 1). + /// + /// After prepare() is called, the transaction must be explicitly committed or rolled back. + /// The destructor will NOT automatically rollback prepared transactions. + /// + void prepare(); + + /// + /// Prepares the transaction for two-phase commit with a text message identifier. + /// + /// The message can be used to identify the transaction during recovery operations. + /// + void prepare(std::string_view message); + + /// + /// Prepares the transaction for two-phase commit with a binary message identifier. + /// + /// The message can be used to identify the transaction during recovery operations. + /// + void prepare(std::span message); + /// /// Commits the transaction. /// + /// Can be called from ACTIVE or PREPARED state. + /// void commit(); /// /// Commits the transaction while maintains it active. /// + /// Cannot be called on a prepared transaction. + /// void commitRetaining(); /// /// Rolls back the transaction. /// + /// Can be called from ACTIVE or PREPARED state. + /// void rollback(); /// /// Rolls back the transaction while maintains it active. - // + /// + /// Cannot be called on a prepared transaction. + /// void rollbackRetaining(); private: Client& client; FbRef handle; + TransactionState state = TransactionState::ACTIVE; + const bool isMultiDatabase = false; }; } // namespace fbcpp diff --git a/src/test/Transaction2PC.cpp b/src/test/Transaction2PC.cpp new file mode 100644 index 0000000..aa32b72 --- /dev/null +++ b/src/test/Transaction2PC.cpp @@ -0,0 +1,388 @@ +/* + * MIT License + * + * Copyright (c) 2025 Adriano dos Santos Fernandes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "TestUtil.h" +#include "fb-cpp/Transaction.h" +#include "fb-cpp/Statement.h" +#include "fb-cpp/Exception.h" +#include +#include + + +BOOST_AUTO_TEST_SUITE(Transaction2PCSuite) + +static int countLimbo(Attachment& attachment) +{ + TransactionOptions options; + options.setIsolationLevel(TransactionIsolationLevel::READ_COMMITTED); + + Transaction transaction{attachment, options}; + Statement statement{attachment, transaction, "select count(*) from rdb$transactions"}; + statement.execute(transaction); + int count = statement.getInt32(0).value(); + transaction.commit(); + return count; +} + +static int countRows(Attachment& attachment, const char* table) +{ + TransactionOptions options; + options.setIsolationLevel(TransactionIsolationLevel::READ_COMMITTED); + + Transaction transaction{attachment, options}; + std::string sql = "select count(*) from "; + sql += table; + Statement statement{attachment, transaction, sql}; + statement.execute(transaction); + int count = statement.getInt32(0).value(); + transaction.commit(); + return count; +} + +BOOST_AUTO_TEST_CASE(singleDatabasePrepareBasic) +{ + // Test prepare() on a regular single-database transaction + // This bypasses the multi-database constructor + Attachment attachment{ + CLIENT, getTempFile("Transaction2PC-singlePrepareBasic.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_CHECK(transaction.getState() == TransactionState::ACTIVE); + + transaction.prepare(); + BOOST_CHECK(transaction.getState() == TransactionState::PREPARED); + + transaction.commit(); + BOOST_CHECK(transaction.getState() == TransactionState::COMMITTED); +} + +BOOST_AUTO_TEST_CASE(singleDatabasePrepareWithMessage) +{ + // Test prepare() with message on a regular single-database transaction + Attachment attachment{ + CLIENT, getTempFile("Transaction2PC-singlePrepareMsg.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + transaction.prepare("test-transaction-id-123"); + BOOST_CHECK(transaction.getState() == TransactionState::PREPARED); + + transaction.rollback(); + BOOST_CHECK(transaction.getState() == TransactionState::ROLLED_BACK); +} + +BOOST_AUTO_TEST_CASE(multiDatabase2Attachments) +{ + Attachment attachment1{CLIENT, getTempFile("Transaction2PC-multiDatabase2Attachments-db1.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment1Drop{attachment1}; + + Attachment attachment2{CLIENT, getTempFile("Transaction2PC-multiDatabase2Attachments-db2.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment2Drop{attachment2}; + + std::vector> attachments{attachment1, attachment2}; + Transaction transaction{attachments}; + + BOOST_CHECK_EQUAL(transaction.isValid(), true); + BOOST_CHECK(transaction.getState() == TransactionState::ACTIVE); + + transaction.commit(); + BOOST_CHECK_EQUAL(transaction.isValid(), false); + BOOST_CHECK(transaction.getState() == TransactionState::COMMITTED); +} + +BOOST_AUTO_TEST_CASE(multiDatabase3Attachments) +{ + Attachment attachment1{CLIENT, getTempFile("Transaction2PC-multiDatabase3Attachments-db1.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment1Drop{attachment1}; + + Attachment attachment2{CLIENT, getTempFile("Transaction2PC-multiDatabase3Attachments-db2.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment2Drop{attachment2}; + + Attachment attachment3{CLIENT, getTempFile("Transaction2PC-multiDatabase3Attachments-db3.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment3Drop{attachment3}; + + std::vector> attachments{attachment1, attachment2, attachment3}; + Transaction transaction{attachments, TransactionOptions().setIsolationLevel(TransactionIsolationLevel::SNAPSHOT)}; + + BOOST_CHECK_EQUAL(transaction.isValid(), true); + BOOST_CHECK(transaction.getState() == TransactionState::ACTIVE); + + transaction.commit(); + BOOST_CHECK_EQUAL(transaction.isValid(), false); + BOOST_CHECK(transaction.getState() == TransactionState::COMMITTED); +} + +BOOST_AUTO_TEST_CASE(prepareCommit) +{ + Attachment attachment1{ + CLIENT, getTempFile("Transaction2PC-prepareCommit-db1.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment1Drop{attachment1}; + + Attachment attachment2{ + CLIENT, getTempFile("Transaction2PC-prepareCommit-db2.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment2Drop{attachment2}; + + std::vector> attachments{attachment1, attachment2}; + Transaction transaction{attachments}; + + BOOST_CHECK(transaction.getState() == TransactionState::ACTIVE); + + transaction.prepare(); + BOOST_CHECK_EQUAL(transaction.isValid(), true); + BOOST_CHECK(transaction.getState() == TransactionState::PREPARED); + + transaction.commit(); + BOOST_CHECK_EQUAL(transaction.isValid(), false); + BOOST_CHECK(transaction.getState() == TransactionState::COMMITTED); +} + +BOOST_AUTO_TEST_CASE(prepareRollback) +{ + Attachment attachment1{ + CLIENT, getTempFile("Transaction2PC-prepareRollback-db1.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment1Drop{attachment1}; + + Attachment attachment2{ + CLIENT, getTempFile("Transaction2PC-prepareRollback-db2.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment2Drop{attachment2}; + + std::vector> attachments{attachment1, attachment2}; + Transaction transaction{attachments}; + + BOOST_CHECK(transaction.getState() == TransactionState::ACTIVE); + + transaction.prepare(); + BOOST_CHECK_EQUAL(transaction.isValid(), true); + BOOST_CHECK(transaction.getState() == TransactionState::PREPARED); + + transaction.rollback(); + BOOST_CHECK_EQUAL(transaction.isValid(), false); + BOOST_CHECK(transaction.getState() == TransactionState::ROLLED_BACK); +} + +BOOST_AUTO_TEST_CASE(prepareWithMessage) +{ + Attachment attachment1{ + CLIENT, getTempFile("Transaction2PC-prepareWithMessage-db1.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment1Drop{attachment1}; + + Attachment attachment2{ + CLIENT, getTempFile("Transaction2PC-prepareWithMessage-db2.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment2Drop{attachment2}; + + std::vector> attachments{attachment1, attachment2}; + Transaction transaction{attachments}; + + transaction.prepare("test-transaction-123"); + BOOST_CHECK(transaction.getState() == TransactionState::PREPARED); + + transaction.commit(); + BOOST_CHECK(transaction.getState() == TransactionState::COMMITTED); +} + +BOOST_AUTO_TEST_CASE(prepareWithBinaryMessage) +{ + Attachment attachment1{CLIENT, getTempFile("Transaction2PC-prepareWithBinaryMessage-db1.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment1Drop{attachment1}; + + Attachment attachment2{CLIENT, getTempFile("Transaction2PC-prepareWithBinaryMessage-db2.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment2Drop{attachment2}; + + std::vector> attachments{attachment1, attachment2}; + Transaction transaction{attachments}; + + std::vector message{0x01, 0x02, 0x03, 0x04}; + transaction.prepare(message); + BOOST_CHECK(transaction.getState() == TransactionState::PREPARED); + + transaction.commit(); + BOOST_CHECK(transaction.getState() == TransactionState::COMMITTED); +} + +BOOST_AUTO_TEST_CASE(commitWithoutPrepare) +{ + Attachment attachment1{CLIENT, getTempFile("Transaction2PC-commitWithoutPrepare-db1.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment1Drop{attachment1}; + + Attachment attachment2{CLIENT, getTempFile("Transaction2PC-commitWithoutPrepare-db2.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment2Drop{attachment2}; + + std::vector> attachments{attachment1, attachment2}; + Transaction transaction{attachments}; + + BOOST_CHECK(transaction.getState() == TransactionState::ACTIVE); + + // Commit without prepare should work + transaction.commit(); + BOOST_CHECK_EQUAL(transaction.isValid(), false); + BOOST_CHECK(transaction.getState() == TransactionState::COMMITTED); +} + +BOOST_AUTO_TEST_CASE(statementAcrossMultipleDatabases) +{ + Attachment attachment1{CLIENT, getTempFile("Transaction2PC-statementAcrossMultipleDatabases-db1.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment1Drop{attachment1}; + + Attachment attachment2{CLIENT, getTempFile("Transaction2PC-statementAcrossMultipleDatabases-db2.fdb"), + AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment2Drop{attachment2}; + + std::vector> attachments{attachment1, attachment2}; + + // Create table in first database + { // scope + Transaction setupTx{attachment1}; + Statement stmt1{attachment1, setupTx, "create table test_table (id integer)"}; + stmt1.execute(setupTx); + setupTx.commit(); + } + + // Create table in second database + { // scope + Transaction setupTx{attachment2}; + Statement stmt2{attachment2, setupTx, "create table test_table (id integer)"}; + stmt2.execute(setupTx); + setupTx.commit(); + } + + Transaction transaction{attachments}; + + // Insert data in first database + { // scope + Statement stmt1{attachment1, transaction, "insert into test_table (id) values (1)"}; + stmt1.execute(transaction); + } + + // Insert data in second database + { // scope + Statement stmt2{attachment2, transaction, "insert into test_table (id) values (2)"}; + stmt2.execute(transaction); + } + + // Prepare and commit + BOOST_CHECK_EQUAL(countLimbo(attachment1), 0); + BOOST_CHECK_EQUAL(countLimbo(attachment2), 0); + + transaction.prepare(); + BOOST_CHECK(transaction.getState() == TransactionState::PREPARED); + + BOOST_CHECK_EQUAL(countLimbo(attachment1), 1); + BOOST_CHECK_EQUAL(countLimbo(attachment2), 1); + + transaction.commit(); + BOOST_CHECK(transaction.getState() == TransactionState::COMMITTED); + + BOOST_CHECK_EQUAL(countLimbo(attachment1), 0); + BOOST_CHECK_EQUAL(countLimbo(attachment2), 0); + + BOOST_CHECK_EQUAL(countRows(attachment1, "test_table"), 1); + BOOST_CHECK_EQUAL(countRows(attachment2, "test_table"), 1); +} + +BOOST_AUTO_TEST_CASE(singleDatabasePrepare) +{ + // Prepare() should also work on single-database transactions + Attachment attachment{ + CLIENT, getTempFile("Transaction2PC-singleDatabasePrepare.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + std::vector> attachments{attachment}; + Transaction transaction{attachments}; + + transaction.prepare(); + BOOST_CHECK(transaction.getState() == TransactionState::PREPARED); + + transaction.commit(); + BOOST_CHECK(transaction.getState() == TransactionState::COMMITTED); +} + +BOOST_AUTO_TEST_CASE(prepareRollbackData) +{ + // Verify that rollback after prepare actually rolls back the data + Attachment attachment1{ + CLIENT, getTempFile("Transaction2PC-prepareRollbackData-db1.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment1Drop{attachment1}; + + Attachment attachment2{ + CLIENT, getTempFile("Transaction2PC-prepareRollbackData-db2.fdb"), AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachment2Drop{attachment2}; + + // Create tables + { // scope + Transaction setupTx1{attachment1}; + Statement stmt1{attachment1, setupTx1, "create table test_table (id integer)"}; + stmt1.execute(setupTx1); + setupTx1.commit(); + } + + { // scope + Transaction setupTx2{attachment2}; + Statement stmt2{attachment2, setupTx2, "create table test_table (id integer)"}; + stmt2.execute(setupTx2); + setupTx2.commit(); + } + + // Insert data and rollback after prepare + { // scope + std::vector> attachments{attachment1, attachment2}; + Transaction transaction{attachments}; + + { // scope + Statement stmt1{attachment1, transaction, "insert into test_table (id) values (1)"}; + stmt1.execute(transaction); + } + + { // scope + Statement stmt2{attachment2, transaction, "insert into test_table (id) values (2)"}; + stmt2.execute(transaction); + } + + BOOST_CHECK_EQUAL(countLimbo(attachment1), 0); + BOOST_CHECK_EQUAL(countLimbo(attachment2), 0); + + transaction.prepare(); + + BOOST_CHECK_EQUAL(countLimbo(attachment1), 1); + BOOST_CHECK_EQUAL(countLimbo(attachment2), 1); + + transaction.rollback(); + } + + BOOST_CHECK_EQUAL(countRows(attachment1, "test_table"), 0); + BOOST_CHECK_EQUAL(countRows(attachment2, "test_table"), 0); +} + +BOOST_AUTO_TEST_SUITE_END()