Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/fb-cpp/SmartPtrs.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ namespace fbcpp
{
void operator()(fb::IDisposable* obj) noexcept
{
obj->dispose();
if (obj)
obj->dispose();
}
};
} // namespace impl
Expand Down
92 changes: 82 additions & 10 deletions src/fb-cpp/Transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,9 @@ using namespace fbcpp;
using namespace fbcpp::impl;


Transaction::Transaction(Attachment& attachment, const TransactionOptions& options)
: client{attachment.getClient()}
static FbUniquePtr<fb::IXpbBuilder> 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<const std::uint8_t*>(options.getTpb().data()),
static_cast<unsigned>(options.getTpb().size())));
Expand Down Expand Up @@ -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);

Expand All @@ -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<std::reference_wrapper<Attachment>> 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()};
Expand All @@ -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<const std::uint8_t>{});
}

void Transaction::prepare(std::span<const std::uint8_t> message)
{
assert(isValid());
assert(state == TransactionState::ACTIVE);

const auto status = client.newStatus();
StatusWrapper statusWrapper{client, status.get()};

handle->prepare(&statusWrapper, static_cast<unsigned>(message.size()), message.data());
state = TransactionState::PREPARED;
}

void Transaction::prepare(std::string_view message)
{
const auto messageBytes = reinterpret_cast<const std::uint8_t*>(message.data());
prepare(std::span<const std::uint8_t>{messageBytes, message.size()});
}
116 changes: 109 additions & 7 deletions src/fb-cpp/Transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "SmartPtrs.h"
#include <memory>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
Expand Down Expand Up @@ -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.
Expand All @@ -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<std::reference_wrapper<Attachment>> 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;
Expand All @@ -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 (...)
{
Expand All @@ -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<const std::uint8_t> 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<fb::ITransaction> handle;
TransactionState state = TransactionState::ACTIVE;
const bool isMultiDatabase = false;
};
} // namespace fbcpp

Expand Down
Loading
Loading