From 2bc17a5bfa8f1cb5d6274a0a9244dceae879d1bd Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Tue, 10 Nov 2026 22:48:08 -0300 Subject: [PATCH] Add TransactionOptions::setAtSnapshotNumber() and Transaction::getSnapshotNumber() Allows starting transactions (in the same or other attachments) that share the same database snapshot, using isc_tpb_at_snapshot_number and fb_info_tra_snapshot_number (Firebird 4.0+). --- src/fb-cpp/Transaction.cpp | 60 ++++++++++++++++++++++++++++++++++++++ src/fb-cpp/Transaction.h | 30 +++++++++++++++++++ src/test/Transaction.cpp | 56 +++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) diff --git a/src/fb-cpp/Transaction.cpp b/src/fb-cpp/Transaction.cpp index fb99f3c..001a373 100644 --- a/src/fb-cpp/Transaction.cpp +++ b/src/fb-cpp/Transaction.cpp @@ -26,7 +26,9 @@ #include "Attachment.h" #include "Client.h" #include "Exception.h" +#include "firebird/impl/inf_pub.h" #include +#include using namespace fbcpp; using namespace fbcpp::impl; @@ -116,6 +118,12 @@ static FbUniquePtr buildTpb( } } + if (const auto atSnapshotNumber = options.getAtSnapshotNumber()) + { + tpbBuilder->insertBigInt( + &statusWrapper, isc_tpb_at_snapshot_number, static_cast(atSnapshotNumber.value())); + } + if (options.getNoAutoUndo()) tpbBuilder->insertTag(&statusWrapper, isc_tpb_no_auto_undo); @@ -239,6 +247,58 @@ void Transaction::rollbackRetaining() handle->rollbackRetaining(&statusWrapper); } +std::uint64_t Transaction::getSnapshotNumber() +{ + assert(isValid()); + + StatusWrapper statusWrapper{client}; + + const std::uint8_t items[] = {fb_info_tra_snapshot_number, isc_info_end}; + std::uint8_t buffer[32]{}; + + handle->getInfo(&statusWrapper, sizeof(items), items, sizeof(buffer), buffer); + + const auto* ptr = buffer; + const auto* end = buffer + sizeof(buffer); + + while (ptr < end) + { + const auto item = *ptr++; + + if (item == isc_info_end) + break; + + if (item == isc_info_truncated) + throw FbCppException("Transaction::getSnapshotNumber truncated response"); + + if (item == isc_info_error) + throw FbCppException("Transaction::getSnapshotNumber error response"); + + if (ptr + 2 > end) + throw FbCppException("Transaction::getSnapshotNumber malformed response"); + + const auto itemLength = static_cast((ptr[0]) | (ptr[1] << 8)); + ptr += 2; + + if (ptr + itemLength > end || itemLength > sizeof(std::uint64_t)) + throw FbCppException("Transaction::getSnapshotNumber invalid length"); + + if (item == fb_info_tra_snapshot_number) + { + std::uint64_t result = 0; + + for (std::uint16_t i = 0; i < itemLength; ++i) + result |= static_cast(ptr[i]) << (8u * i); + + return result; + } + + ptr += itemLength; + } + + throw FbCppException("Transaction::getSnapshotNumber not supported by the server (Firebird 4.0 or later required)"); +} + void Transaction::prepare() { prepare(std::span{}); diff --git a/src/fb-cpp/Transaction.h b/src/fb-cpp/Transaction.h index 6e9b2f3..c67b7c8 100644 --- a/src/fb-cpp/Transaction.h +++ b/src/fb-cpp/Transaction.h @@ -218,6 +218,27 @@ namespace fbcpp return *this; } + /// + /// Returns the snapshot number the transaction will be started at. + /// + const std::optional& getAtSnapshotNumber() const + { + return atSnapshotNumber; + } + + /// + /// Sets the snapshot number the transaction will be started at + /// (`isc_tpb_at_snapshot_number`). + /// The transaction sees the same database state as the transaction which + /// returned this number from Transaction::getSnapshotNumber(), which must still + /// be active. Requires the SNAPSHOT isolation level and Firebird 4.0 or later. + /// + TransactionOptions& setAtSnapshotNumber(std::uint64_t value) + { + atSnapshotNumber = value; + return *this; + } + /// /// Returns whether the transaction will not automatically undo changes in /// case of a deadlock or update conflict. @@ -294,6 +315,7 @@ namespace fbcpp std::optional readCommittedMode; std::optional accessMode; std::optional waitMode; + std::optional atSnapshotNumber; bool noAutoUndo = false; bool ignoreLimbo = false; bool restartRequests = false; @@ -444,6 +466,14 @@ namespace fbcpp return state; } + /// + /// Returns the snapshot number of the transaction (`fb_info_tra_snapshot_number`). + /// It can be passed to TransactionOptions::setAtSnapshotNumber() to start other + /// transactions, in this or other attachments to the same database, that see + /// the same database state. Requires Firebird 4.0 or later. + /// + std::uint64_t getSnapshotNumber(); + /// /// Prepares the transaction for two-phase commit (2PC phase 1). /// diff --git a/src/test/Transaction.cpp b/src/test/Transaction.cpp index 3054ee4..ad45eaf 100644 --- a/src/test/Transaction.cpp +++ b/src/test/Transaction.cpp @@ -25,6 +25,7 @@ #include "TestUtil.h" #include "fb-cpp/Transaction.h" #include "fb-cpp/Exception.h" +#include "fb-cpp/Statement.h" #include @@ -142,4 +143,59 @@ BOOST_AUTO_TEST_CASE(isNotValidAfterMove) BOOST_CHECK_EQUAL(transaction2.isValid(), false); } +BOOST_AUTO_TEST_CASE(atSnapshotNumber) +{ + const auto database = getTempFile("Transaction-atSnapshotNumber.fdb"); + + Attachment attachment1{getClient(), database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment1}; + + { // scope + Transaction transaction{attachment1}; + attachment1.execute(transaction, "create table t (id integer not null primary key)"); + transaction.commit(); + } + + { // scope + Transaction transaction{attachment1}; + attachment1.execute(transaction, "insert into t (id) values (1)"); + transaction.commit(); + } + + const auto snapshotOptions = TransactionOptions().setIsolationLevel(TransactionIsolationLevel::SNAPSHOT); + + Transaction transaction1{attachment1, snapshotOptions}; + const auto snapshotNumber = transaction1.getSnapshotNumber(); + BOOST_CHECK_GT(snapshotNumber, 0u); + + { // scope + Transaction transaction{attachment1}; + attachment1.execute(transaction, "insert into t (id) values (2)"); + transaction.commit(); + } + + Attachment attachment2{getClient(), database}; + + const auto count = [](Attachment& attachment, Transaction& transaction) + { + Statement statement{attachment, transaction, "select count(*) from t"}; + BOOST_REQUIRE(statement.execute(transaction)); + return statement.getInt64(0).value(); + }; + + Transaction transaction2{attachment2, TransactionOptions(snapshotOptions).setAtSnapshotNumber(snapshotNumber)}; + BOOST_CHECK_EQUAL(transaction2.getSnapshotNumber(), snapshotNumber); + BOOST_CHECK_EQUAL(count(attachment2, transaction2), 1); + BOOST_CHECK_EQUAL(count(attachment1, transaction1), 1); + transaction2.commit(); + + Transaction transaction3{attachment2, snapshotOptions}; + BOOST_CHECK_GT(transaction3.getSnapshotNumber(), snapshotNumber); + BOOST_CHECK_EQUAL(count(attachment2, transaction3), 2); + transaction3.commit(); + + transaction1.commit(); + attachment2.disconnect(); +} + BOOST_AUTO_TEST_SUITE_END()