From 86266a869740c80bd90448c75f902befe48a0259 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Tue, 24 Feb 2026 16:00:53 +0000 Subject: [PATCH 1/5] Add Batch Execution support (IBatch wrapper) (#26) Add Batch, BatchOptions, BatchCompletionState, and BlobPolicy types that wrap the Firebird IBatch interface for bulk DML operations. Two creation paths: - From a prepared Statement (IStatement::createBatch) - From an Attachment + SQL text (IAttachment::createBatch) Also adds getAttachment() and getInputMessage() accessors to Statement. --- src/fb-cpp/Batch.cpp | 305 ++++++++++++++++++++++++++ src/fb-cpp/Batch.h | 454 ++++++++++++++++++++++++++++++++++++++ src/fb-cpp/Statement.h | 16 ++ src/fb-cpp/fb-cpp.h | 1 + src/test/Batch.cpp | 484 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1260 insertions(+) create mode 100644 src/fb-cpp/Batch.cpp create mode 100644 src/fb-cpp/Batch.h create mode 100644 src/test/Batch.cpp diff --git a/src/fb-cpp/Batch.cpp b/src/fb-cpp/Batch.cpp new file mode 100644 index 0000000..2bdbf99 --- /dev/null +++ b/src/fb-cpp/Batch.cpp @@ -0,0 +1,305 @@ +/* + * MIT License + * + * Copyright (c) 2026 F.D.Castel + * + * 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 "Batch.h" +#include "Attachment.h" +#include "Client.h" +#include "Statement.h" +#include "Transaction.h" +#include + +using namespace fbcpp; +using namespace fbcpp::impl; + + +// --- BatchCompletionState --- + +BatchCompletionState::BatchCompletionState(Client& client, FbUniquePtr handle) noexcept + : client{&client}, + status{client.newStatus()}, + statusWrapper{client, status.get()}, + handle{std::move(handle)} +{ +} + +BatchCompletionState::BatchCompletionState(BatchCompletionState&& o) noexcept + : client{o.client}, + status{std::move(o.status)}, + statusWrapper{std::move(o.statusWrapper)}, + handle{std::move(o.handle)} +{ +} + +unsigned BatchCompletionState::getSize() +{ + return handle->getSize(&statusWrapper); +} + +int BatchCompletionState::getState(unsigned pos) +{ + return handle->getState(&statusWrapper, pos); +} + +std::optional BatchCompletionState::findError(unsigned pos) +{ + const auto result = handle->findError(&statusWrapper, pos); + + if (result == fb::IBatchCompletionState::NO_MORE_ERRORS) + return std::nullopt; + + return result; +} + +std::vector BatchCompletionState::getStatus(unsigned pos) +{ + auto tempStatus = client->newStatus(); + + handle->getStatus(&statusWrapper, tempStatus.get(), pos); + + std::vector result; + const auto* errors = tempStatus->getErrors(); + + if (errors) + { + const auto* p = errors; + + while (*p != isc_arg_end) + { + result.push_back(*p++); + result.push_back(*p++); + } + + result.push_back(isc_arg_end); + } + + return result; +} + + +// --- Batch --- + +Batch::Batch(Statement& statement, Transaction& transaction, const BatchOptions& options) + : client{&statement.getAttachment().getClient()}, + transaction{&transaction}, + statement{&statement}, + status{client->newStatus()}, + statusWrapper{*client, status.get()} +{ + assert(statement.isValid()); + assert(transaction.isValid()); + + const auto parBlock = buildParametersBlock(*client, options); + + handle.reset(statement.getStatementHandle()->createBatch( + &statusWrapper, statement.getInputMetadata().get(), static_cast(parBlock.size()), parBlock.data())); +} + +Batch::Batch(Attachment& attachment, Transaction& transaction, std::string_view sql, unsigned dialect, + const BatchOptions& options) + : client{&attachment.getClient()}, + transaction{&transaction}, + status{client->newStatus()}, + statusWrapper{*client, status.get()} +{ + assert(attachment.isValid()); + assert(transaction.isValid()); + + const auto parBlock = buildParametersBlock(*client, options); + + handle.reset(attachment.getHandle()->createBatch(&statusWrapper, transaction.getHandle().get(), + static_cast(sql.length()), sql.data(), dialect, nullptr, static_cast(parBlock.size()), + parBlock.data())); +} + +Batch::Batch(Batch&& o) noexcept + : client{o.client}, + transaction{o.transaction}, + statement{o.statement}, + status{std::move(o.status)}, + statusWrapper{std::move(o.statusWrapper)}, + handle{std::move(o.handle)} +{ +} + + +// --- Adding messages --- + +void Batch::add(unsigned count, const void* inBuffer) +{ + assert(isValid()); + handle->add(&statusWrapper, count, inBuffer); +} + +void Batch::addMessage() +{ + assert(isValid()); + assert(statement && "addMessage() requires the Statement-based constructor"); + handle->add(&statusWrapper, 1, statement->getInputMessage().data()); +} + + +// --- Blob support --- + +BlobId Batch::addBlob(std::span data, const BlobOptions& bpb) +{ + assert(isValid()); + + const auto preparedBpb = prepareBpb(*client, bpb); + + BlobId blobId; + handle->addBlob(&statusWrapper, static_cast(data.size()), data.data(), &blobId.id, + static_cast(preparedBpb.size()), preparedBpb.data()); + + return blobId; +} + +void Batch::appendBlobData(std::span data) +{ + assert(isValid()); + handle->appendBlobData(&statusWrapper, static_cast(data.size()), data.data()); +} + +void Batch::addBlobStream(std::span data) +{ + assert(isValid()); + handle->addBlobStream(&statusWrapper, static_cast(data.size()), data.data()); +} + +BlobId Batch::registerBlob(const BlobId& existingBlob) +{ + assert(isValid()); + + BlobId batchId; + handle->registerBlob(&statusWrapper, &existingBlob.id, &batchId.id); + + return batchId; +} + +void Batch::setDefaultBpb(const BlobOptions& bpb) +{ + assert(isValid()); + + const auto preparedBpb = prepareBpb(*client, bpb); + handle->setDefaultBpb(&statusWrapper, static_cast(preparedBpb.size()), preparedBpb.data()); +} + +unsigned Batch::getBlobAlignment() +{ + assert(isValid()); + return handle->getBlobAlignment(&statusWrapper); +} + + +// --- Execution --- + +BatchCompletionState Batch::execute() +{ + assert(isValid()); + + auto completionState = fbUnique(handle->execute(&statusWrapper, transaction->getHandle().get())); + + return BatchCompletionState{*client, std::move(completionState)}; +} + +void Batch::cancel() +{ + assert(isValid()); + handle->cancel(&statusWrapper); + handle.reset(); +} + +void Batch::close() +{ + assert(isValid()); + handle->close(&statusWrapper); + handle.reset(); +} + +FbRef Batch::getMetadata() +{ + assert(isValid()); + + FbRef metadata; + metadata.reset(handle->getMetadata(&statusWrapper)); + + return metadata; +} + + +// --- Internal helpers --- + +std::vector Batch::buildParametersBlock(Client& client, const BatchOptions& options) +{ + auto builder = fbUnique(client.getUtil()->getXpbBuilder(&statusWrapper, fb::IXpbBuilder::BATCH, nullptr, 0)); + + if (options.getMultiError()) + builder->insertInt(&statusWrapper, fb::IBatch::TAG_MULTIERROR, 1); + + if (options.getRecordCounts()) + builder->insertInt(&statusWrapper, fb::IBatch::TAG_RECORD_COUNTS, 1); + + if (const auto bufferSize = options.getBufferBytesSize(); bufferSize.has_value()) + builder->insertInt(&statusWrapper, fb::IBatch::TAG_BUFFER_BYTES_SIZE, static_cast(bufferSize.value())); + + if (options.getBlobPolicy() != BlobPolicy::NONE) + { + builder->insertInt(&statusWrapper, fb::IBatch::TAG_BLOB_POLICY, static_cast(options.getBlobPolicy())); + } + + if (options.getDetailedErrors() != 64) + builder->insertInt( + &statusWrapper, fb::IBatch::TAG_DETAILED_ERRORS, static_cast(options.getDetailedErrors())); + + const auto buffer = builder->getBuffer(&statusWrapper); + const auto length = builder->getBufferLength(&statusWrapper); + + std::vector result(length); + + if (length != 0) + std::memcpy(result.data(), buffer, length); + + return result; +} + +std::vector Batch::prepareBpb(Client& client, const BlobOptions& bpb) +{ + auto builder = fbUnique(client.getUtil()->getXpbBuilder(&statusWrapper, fb::IXpbBuilder::BPB, + reinterpret_cast(bpb.getBpb().data()), static_cast(bpb.getBpb().size()))); + + if (const auto type = bpb.getType(); type.has_value()) + builder->insertInt(&statusWrapper, isc_bpb_type, static_cast(type.value())); + + if (const auto storage = bpb.getStorage(); storage.has_value()) + builder->insertInt(&statusWrapper, isc_bpb_storage, static_cast(storage.value())); + + const auto buffer = builder->getBuffer(&statusWrapper); + const auto length = builder->getBufferLength(&statusWrapper); + + std::vector result(length); + + if (length != 0) + std::memcpy(result.data(), buffer, length); + + return result; +} diff --git a/src/fb-cpp/Batch.h b/src/fb-cpp/Batch.h new file mode 100644 index 0000000..0682e27 --- /dev/null +++ b/src/fb-cpp/Batch.h @@ -0,0 +1,454 @@ +/* + * MIT License + * + * Copyright (c) 2026 F.D.Castel + * + * 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. + */ + +#ifndef FBCPP_BATCH_H +#define FBCPP_BATCH_H + +#include "fb-api.h" +#include "Blob.h" +#include "SmartPtrs.h" +#include "Exception.h" +#include +#include +#include +#include +#include +#include +#include + + +/// +/// fb-cpp namespace. +/// +namespace fbcpp +{ + class Attachment; + class Client; + class Statement; + class Transaction; + + /// + /// Selects the blob handling policy for a Batch. + /// + enum class BlobPolicy : unsigned char + { + /// + /// Blobs are not allowed in the batch. + /// + NONE = fb::IBatch::BLOB_NONE, + + /// + /// Batch-local blob IDs are generated by the Firebird engine. + /// + ID_ENGINE = fb::IBatch::BLOB_ID_ENGINE, + + /// + /// Batch-local blob IDs are generated by the caller. + /// + ID_USER = fb::IBatch::BLOB_ID_USER, + + /// + /// Blobs are sent inline as a stream. + /// + STREAM = fb::IBatch::BLOB_STREAM, + }; + + /// + /// Configuration options for creating a Batch. + /// + /// These options map to `IXpbBuilder` parameters for the `IBatch` interface. + /// All setters return `*this` for fluent configuration. + /// + class BatchOptions final + { + public: + /// + /// Returns whether multiple errors are collected per execution. + /// + bool getMultiError() const + { + return multiError; + } + + /// + /// Enables or disables collection of multiple errors per execution. + /// + BatchOptions& setMultiError(bool value) + { + multiError = value; + return *this; + } + + /// + /// Returns whether per-message affected row counts are reported. + /// + bool getRecordCounts() const + { + return recordCounts; + } + + /// + /// Enables or disables per-message affected row counts. + /// + BatchOptions& setRecordCounts(bool value) + { + recordCounts = value; + return *this; + } + + /// + /// Returns the batch buffer size in bytes, or nullopt for the server default. + /// + std::optional getBufferBytesSize() const + { + return bufferBytesSize; + } + + /// + /// Sets the batch buffer size in bytes. + /// + BatchOptions& setBufferBytesSize(unsigned value) + { + bufferBytesSize = value; + return *this; + } + + /// + /// Returns the blob handling policy. + /// + BlobPolicy getBlobPolicy() const + { + return blobPolicy; + } + + /// + /// Sets the blob handling policy. + /// + BatchOptions& setBlobPolicy(BlobPolicy value) + { + blobPolicy = value; + return *this; + } + + /// + /// Returns the maximum number of detailed error statuses to collect. + /// + unsigned getDetailedErrors() const + { + return detailedErrors; + } + + /// + /// Sets the maximum number of detailed error statuses to collect. + /// + BatchOptions& setDetailedErrors(unsigned value) + { + detailedErrors = value; + return *this; + } + + private: + bool multiError = false; + bool recordCounts = false; + std::optional bufferBytesSize; + BlobPolicy blobPolicy = BlobPolicy::NONE; + unsigned detailedErrors = 64; + }; + + /// + /// Wraps `IBatchCompletionState` to provide RAII-safe access to batch execution results. + /// + /// This is a move-only type. The underlying Firebird handle is disposed in the destructor. + /// + class BatchCompletionState final + { + public: + /// + /// Per-message state value indicating the message failed to execute. + /// + static constexpr int EXECUTE_FAILED = fb::IBatchCompletionState::EXECUTE_FAILED; + + /// + /// Per-message state value indicating success with no row-count information. + /// + static constexpr int SUCCESS_NO_INFO = fb::IBatchCompletionState::SUCCESS_NO_INFO; + + /// + /// Constructs a BatchCompletionState from a Firebird completion state handle. + /// + explicit BatchCompletionState(Client& client, FbUniquePtr handle) noexcept; + + /// + /// Transfers ownership of another completion state into this one. + /// + BatchCompletionState(BatchCompletionState&& o) noexcept; + + /// + /// Move assignment is not supported. + /// + BatchCompletionState& operator=(BatchCompletionState&&) = delete; + + /// + /// Copy construction is not supported. + /// + BatchCompletionState(const BatchCompletionState&) = delete; + + /// + /// Copy assignment is not supported. + /// + BatchCompletionState& operator=(const BatchCompletionState&) = delete; + + /// + /// Disposes the underlying completion state handle. + /// + ~BatchCompletionState() noexcept = default; + + public: + /// + /// Returns the number of messages processed. + /// + unsigned getSize(); + + /// + /// Returns the per-message result at the given position. + /// + /// The value is either the number of affected rows, `EXECUTE_FAILED`, or `SUCCESS_NO_INFO`. + /// + int getState(unsigned pos); + + /// + /// Finds the next error at or after the given position. + /// + /// Returns `std::nullopt` when there are no more errors. + /// + std::optional findError(unsigned pos); + + /// + /// Returns the detailed error status vector for the given position. + /// + /// The returned vector has the same format as `IStatus::getErrors()`. + /// + std::vector getStatus(unsigned pos); + + private: + Client* client; + FbUniquePtr status; + impl::StatusWrapper statusWrapper; + FbUniquePtr handle; + }; + + /// + /// @brief Wraps the Firebird `IBatch` interface for bulk DML operations. + /// + /// A Batch collects multiple parameter sets ("messages") and sends them to + /// the server in a single round-trip for execution. This maps directly to + /// ODBC's "array of parameter values" feature (`SQL_ATTR_PARAMSET_SIZE > 1`) + /// and is the primary performance path for ETL workloads against Firebird 4.0+. + /// + /// Two creation paths are supported: + /// - From a **prepared `Statement`** — uses `IStatement::createBatch()`. + /// The `Statement` must remain valid for the lifetime of the `Batch`. + /// The convenience method `addMessage()` copies the Statement's current + /// input-message buffer into the batch. + /// - From an **`Attachment` + SQL text** — uses `IAttachment::createBatch()`. + /// Messages must be added via the raw `add()` method. + /// + class Batch final + { + public: + /// + /// Creates a Batch from a prepared Statement. + /// + /// The Statement must remain valid for the lifetime of the Batch. + /// + explicit Batch(Statement& statement, Transaction& transaction, const BatchOptions& options = {}); + + /// + /// Creates a Batch from an Attachment and SQL text. + /// + explicit Batch(Attachment& attachment, Transaction& transaction, std::string_view sql, unsigned dialect = 3, + const BatchOptions& options = {}); + + /// + /// Transfers ownership of another Batch into this one. + /// + Batch(Batch&& o) noexcept; + + /// + /// Move assignment is not supported. + /// + Batch& operator=(Batch&&) = delete; + + /// + /// Copy construction is not supported. + /// + Batch(const Batch&) = delete; + + /// + /// Copy assignment is not supported. + /// + Batch& operator=(const Batch&) = delete; + + /// + /// Closes the batch handle if still valid. + /// + ~Batch() noexcept + { + if (isValid()) + { + try + { + close(); + } + catch (...) + { + // swallow + } + } + } + + public: + /// + /// Returns whether the batch handle is valid. + /// + bool isValid() const noexcept + { + return handle != nullptr; + } + + /// + /// @name Adding messages + /// @{ + /// + + /// + /// Adds one or more raw messages to the batch buffer. + /// + /// `inBuffer` must contain `count` aligned messages matching the input metadata. + /// + void add(unsigned count, const void* inBuffer); + + /// + /// Adds the Statement's current input-message buffer as one message. + /// + /// Requires the Statement-based constructor. + /// Typical usage: + /// ``` + /// stmt.setInt32(0, val); + /// batch.addMessage(); + /// ``` + /// + void addMessage(); + + /// + /// @} + /// + + /// + /// @name Blob support + /// @{ + /// + + /// + /// Adds an inline blob and returns its batch-local ID. + /// + /// Only valid when `BlobPolicy` is `ID_ENGINE` or `ID_USER`. + /// + BlobId addBlob(std::span data, const BlobOptions& bpb = {}); + + /// + /// Appends more data to the last blob added with `addBlob()`. + /// + void appendBlobData(std::span data); + + /// + /// Adds blob data in stream mode (`BlobPolicy::STREAM` only). + /// + void addBlobStream(std::span data); + + /// + /// Registers an existing blob (created via the normal `Blob` class) for use + /// in the batch, and returns its batch-local ID. + /// + BlobId registerBlob(const BlobId& existingBlob); + + /// + /// Sets the default BPB (Blob Parameter Block) for blobs in this batch. + /// + void setDefaultBpb(const BlobOptions& bpb); + + /// + /// Returns the blob alignment requirement for this batch. + /// + unsigned getBlobAlignment(); + + /// + /// @} + /// + + /// + /// @name Execution + /// @{ + /// + + /// + /// Executes all queued messages and returns the completion state. + /// + BatchCompletionState execute(); + + /// + /// Cancels the batch, discarding all queued messages. + /// + void cancel(); + + /// + /// Closes the batch handle and releases resources. + /// + void close(); + + /// + /// Returns the input metadata for this batch. + /// + FbRef getMetadata(); + + /// + /// @} + /// + + private: + std::vector buildParametersBlock(Client& client, const BatchOptions& options); + std::vector prepareBpb(Client& client, const BlobOptions& bpb); + + private: + Client* client; + Transaction* transaction; + Statement* statement = nullptr; + FbUniquePtr status; + impl::StatusWrapper statusWrapper; + FbRef handle; + }; +} // namespace fbcpp + + +#endif // FBCPP_BATCH_H diff --git a/src/fb-cpp/Statement.h b/src/fb-cpp/Statement.h index e6c37be..2cd7491 100644 --- a/src/fb-cpp/Statement.h +++ b/src/fb-cpp/Statement.h @@ -330,6 +330,14 @@ namespace fbcpp /// @brief Reports whether the statement currently owns a prepared handle. /// + /// + /// Returns the Attachment object reference used to create this Statement. + /// + Attachment& getAttachment() noexcept + { + return *attachment; + } + /// /// Returns whether the Statement object is valid. /// @@ -364,6 +372,14 @@ namespace fbcpp return inMetadata; } + /// + /// @brief Provides direct access to the raw input message buffer. + /// + std::vector& getInputMessage() noexcept + { + return inMessage; + } + /// /// @brief Returns the metadata describing columns produced by the statement. /// diff --git a/src/fb-cpp/fb-cpp.h b/src/fb-cpp/fb-cpp.h index 8640a6e..c612ac4 100644 --- a/src/fb-cpp/fb-cpp.h +++ b/src/fb-cpp/fb-cpp.h @@ -30,6 +30,7 @@ #include "Transaction.h" #include "Descriptor.h" #include "Statement.h" +#include "Batch.h" #include "Blob.h" #include "EventListener.h" diff --git a/src/test/Batch.cpp b/src/test/Batch.cpp new file mode 100644 index 0000000..462d771 --- /dev/null +++ b/src/test/Batch.cpp @@ -0,0 +1,484 @@ +/* + * MIT License + * + * Copyright (c) 2026 F.D.Castel + * + * 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/Attachment.h" +#include "fb-cpp/Batch.h" +#include "fb-cpp/Statement.h" +#include "fb-cpp/Transaction.h" +#include +#include +#include + + +BOOST_AUTO_TEST_SUITE(BatchSuite) + +BOOST_AUTO_TEST_CASE(constructorFromStatementAndExecute) +{ + const auto database = getTempFile("Batch-constructorFromStatementAndExecute.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + { // scope + Transaction transaction{attachment}; + Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null, name varchar(50))"}; + ddl.execute(transaction); + transaction.commit(); + } + + // Insert using batch from Statement with addMessage(). + { // scope + Transaction transaction{attachment}; + Statement insert{attachment, transaction, "insert into batch_test (id, name) values (?, ?)"}; + + Batch batch{insert, transaction, BatchOptions().setRecordCounts(true)}; + + insert.setInt32(0, 1); + insert.setString(1, "Alice"); + batch.addMessage(); + + insert.setInt32(0, 2); + insert.setString(1, "Bob"); + batch.addMessage(); + + insert.setInt32(0, 3); + insert.setString(1, "Charlie"); + batch.addMessage(); + + auto completionState = batch.execute(); + + BOOST_CHECK_EQUAL(completionState.getSize(), 3U); + BOOST_CHECK_EQUAL(completionState.getState(0), 1); + BOOST_CHECK_EQUAL(completionState.getState(1), 1); + BOOST_CHECK_EQUAL(completionState.getState(2), 1); + BOOST_CHECK(!completionState.findError(0).has_value()); + + transaction.commit(); + } + + // Verify inserted data. + { // scope + Transaction transaction{attachment}; + Statement select{attachment, transaction, "select id, name from batch_test order by id"}; + + BOOST_CHECK(select.execute(transaction)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); + BOOST_CHECK_EQUAL(select.getString(1).value(), "Alice"); + + BOOST_CHECK(select.fetchNext()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); + BOOST_CHECK_EQUAL(select.getString(1).value(), "Bob"); + + BOOST_CHECK(select.fetchNext()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); + BOOST_CHECK_EQUAL(select.getString(1).value(), "Charlie"); + + BOOST_CHECK(!select.fetchNext()); + } +} + +BOOST_AUTO_TEST_CASE(constructorFromAttachmentAndExecute) +{ + const auto database = getTempFile("Batch-constructorFromAttachmentAndExecute.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + { // scope + Transaction transaction{attachment}; + Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null, val integer)"}; + ddl.execute(transaction); + transaction.commit(); + } + + // Insert using batch from Attachment + SQL. + { // scope + Transaction transaction{attachment}; + Batch batch{attachment, transaction, "insert into batch_test (id, val) values (?, ?)", 3, + BatchOptions().setRecordCounts(true)}; + + // Get metadata to build raw messages. + auto metadata = batch.getMetadata(); + + FbUniquePtr tempStatus{CLIENT.newStatus()}; + impl::StatusWrapper tempWrapper{CLIENT, tempStatus.get()}; + const auto msgLength = metadata->getMessageLength(&tempWrapper); + const auto idOffset = metadata->getOffset(&tempWrapper, 0); + const auto idNullOffset = metadata->getNullOffset(&tempWrapper, 0); + const auto valOffset = metadata->getOffset(&tempWrapper, 1); + const auto valNullOffset = metadata->getNullOffset(&tempWrapper, 1); + + std::vector message(msgLength, std::byte{0}); + + for (int i = 1; i <= 3; ++i) + { + *reinterpret_cast(&message[idOffset]) = i; + *reinterpret_cast(&message[idNullOffset]) = FB_FALSE; + *reinterpret_cast(&message[valOffset]) = i * 100; + *reinterpret_cast(&message[valNullOffset]) = FB_FALSE; + batch.add(1, message.data()); + } + + auto completionState = batch.execute(); + + BOOST_CHECK_EQUAL(completionState.getSize(), 3U); + BOOST_CHECK_EQUAL(completionState.getState(0), 1); + BOOST_CHECK_EQUAL(completionState.getState(1), 1); + BOOST_CHECK_EQUAL(completionState.getState(2), 1); + + transaction.commit(); + } + + // Verify. + { // scope + Transaction transaction{attachment}; + Statement select{attachment, transaction, "select id, val from batch_test order by id"}; + + BOOST_CHECK(select.execute(transaction)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); + BOOST_CHECK_EQUAL(select.getInt32(1).value(), 100); + + BOOST_CHECK(select.fetchNext()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); + BOOST_CHECK_EQUAL(select.getInt32(1).value(), 200); + + BOOST_CHECK(select.fetchNext()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); + BOOST_CHECK_EQUAL(select.getInt32(1).value(), 300); + + BOOST_CHECK(!select.fetchNext()); + } +} + +BOOST_AUTO_TEST_CASE(moveConstructorTransfersOwnership) +{ + const auto database = getTempFile("Batch-moveConstructorTransfersOwnership.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + { // scope + Transaction transaction{attachment}; + Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null)"}; + ddl.execute(transaction); + transaction.commit(); + } + + { // scope + Transaction transaction{attachment}; + Statement insert{attachment, transaction, "insert into batch_test (id) values (?)"}; + + Batch original{insert, transaction}; + BOOST_CHECK(original.isValid()); + + Batch moved{std::move(original)}; + BOOST_CHECK(moved.isValid()); + BOOST_CHECK(!original.isValid()); + + insert.setInt32(0, 42); + moved.addMessage(); + + auto completionState = moved.execute(); + BOOST_CHECK_EQUAL(completionState.getSize(), 1U); + + transaction.commit(); + } +} + +BOOST_AUTO_TEST_CASE(executeReportsNoInfoWhenRecordCountsDisabled) +{ + const auto database = getTempFile("Batch-noInfo.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + { // scope + Transaction transaction{attachment}; + Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null)"}; + ddl.execute(transaction); + transaction.commit(); + } + + { // scope + Transaction transaction{attachment}; + Statement insert{attachment, transaction, "insert into batch_test (id) values (?)"}; + + Batch batch{insert, transaction}; + + insert.setInt32(0, 1); + batch.addMessage(); + + auto completionState = batch.execute(); + + BOOST_CHECK_EQUAL(completionState.getSize(), 1U); + BOOST_CHECK_EQUAL(completionState.getState(0), BatchCompletionState::SUCCESS_NO_INFO); + + transaction.commit(); + } +} + +BOOST_AUTO_TEST_CASE(executeWithBadDataReportsExecuteFailed) +{ + const auto database = getTempFile("Batch-badData.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + { // scope + Transaction transaction{attachment}; + Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null primary key)"}; + ddl.execute(transaction); + transaction.commit(); + } + + // Insert duplicate keys to cause errors. + { // scope + Transaction transaction{attachment}; + Statement insert{attachment, transaction, "insert into batch_test (id) values (?)"}; + + Batch batch{ + insert, transaction, BatchOptions().setMultiError(true).setRecordCounts(true).setDetailedErrors(10)}; + + insert.setInt32(0, 1); + batch.addMessage(); + + insert.setInt32(0, 1); // duplicate + batch.addMessage(); + + insert.setInt32(0, 2); + batch.addMessage(); + + auto completionState = batch.execute(); + + BOOST_CHECK_EQUAL(completionState.getSize(), 3U); + BOOST_CHECK_EQUAL(completionState.getState(0), 1); + BOOST_CHECK_EQUAL(completionState.getState(1), BatchCompletionState::EXECUTE_FAILED); + BOOST_CHECK_EQUAL(completionState.getState(2), 1); + + auto errorPos = completionState.findError(0); + BOOST_REQUIRE(errorPos.has_value()); + BOOST_CHECK_EQUAL(errorPos.value(), 1U); + + // No more errors after position 1. + BOOST_CHECK(!completionState.findError(errorPos.value() + 1).has_value()); + + transaction.commit(); + } + + // Verify only valid rows were inserted. + { // scope + Transaction transaction{attachment}; + Statement count{attachment, transaction, "select count(*) from batch_test"}; + + BOOST_CHECK(count.execute(transaction)); + BOOST_CHECK_EQUAL(count.getInt32(0).value(), 2); + } +} + +BOOST_AUTO_TEST_CASE(cancelDiscardsMessages) +{ + const auto database = getTempFile("Batch-cancelDiscardsMessages.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + { // scope + Transaction transaction{attachment}; + Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null)"}; + ddl.execute(transaction); + transaction.commit(); + } + + { // scope + Transaction transaction{attachment}; + Statement insert{attachment, transaction, "insert into batch_test (id) values (?)"}; + + Batch batch{insert, transaction}; + + insert.setInt32(0, 1); + batch.addMessage(); + + batch.cancel(); + BOOST_CHECK(!batch.isValid()); + + transaction.commit(); + } + + // Verify nothing was inserted. + { // scope + Transaction transaction{attachment}; + Statement count{attachment, transaction, "select count(*) from batch_test"}; + + BOOST_CHECK(count.execute(transaction)); + BOOST_CHECK_EQUAL(count.getInt32(0).value(), 0); + } +} + +BOOST_AUTO_TEST_CASE(blobWithIdEngine) +{ + const auto database = getTempFile("Batch-blobWithIdEngine.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + { // scope + Transaction transaction{attachment}; + Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null, data blob)"}; + ddl.execute(transaction); + transaction.commit(); + } + + { // scope + Transaction transaction{attachment}; + Statement insert{attachment, transaction, "insert into batch_test (id, data) values (?, ?)"}; + + Batch batch{insert, transaction, BatchOptions().setBlobPolicy(BlobPolicy::ID_ENGINE).setRecordCounts(true)}; + + const std::string blobText = "Hello from batch blob!"; + const auto blobData = std::as_bytes(std::span{blobText}); + + auto blobId = batch.addBlob(blobData); + + insert.setInt32(0, 1); + insert.setBlobId(1, blobId); + batch.addMessage(); + + auto completionState = batch.execute(); + + BOOST_CHECK_EQUAL(completionState.getSize(), 1U); + BOOST_CHECK_EQUAL(completionState.getState(0), 1); + + transaction.commit(); + } + + // Verify blob content. + { // scope + Transaction transaction{attachment}; + Statement select{attachment, transaction, "select data from batch_test where id = 1"}; + + BOOST_CHECK(select.execute(transaction)); + + const auto receivedBlobId = select.getBlobId(0); + BOOST_REQUIRE(receivedBlobId.has_value()); + + Blob reader{attachment, transaction, receivedBlobId.value(), BlobOptions().setType(BlobType::STREAM)}; + + std::vector buffer(1024); + const auto read = reader.read(buffer); + std::string result(reinterpret_cast(buffer.data()), read); + + BOOST_CHECK_EQUAL(result, "Hello from batch blob!"); + } +} + +BOOST_AUTO_TEST_CASE(registerExistingBlob) +{ + const auto database = getTempFile("Batch-registerExistingBlob.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + { // scope + Transaction transaction{attachment}; + Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null, data blob)"}; + ddl.execute(transaction); + transaction.commit(); + } + + { // scope + Transaction transaction{attachment}; + + // Create a regular blob first. + Blob writer{attachment, transaction, BlobOptions().setType(BlobType::STREAM)}; + const std::string blobText = "Registered blob data"; + writer.write(std::as_bytes(std::span{blobText})); + writer.close(); + const auto existingBlobId = writer.getId(); + + Statement insert{attachment, transaction, "insert into batch_test (id, data) values (?, ?)"}; + + Batch batch{insert, transaction, BatchOptions().setBlobPolicy(BlobPolicy::ID_ENGINE).setRecordCounts(true)}; + + auto batchBlobId = batch.registerBlob(existingBlobId); + + insert.setInt32(0, 1); + insert.setBlobId(1, batchBlobId); + batch.addMessage(); + + auto completionState = batch.execute(); + + BOOST_CHECK_EQUAL(completionState.getSize(), 1U); + BOOST_CHECK_EQUAL(completionState.getState(0), 1); + + transaction.commit(); + } + + // Verify blob content. + { // scope + Transaction transaction{attachment}; + Statement select{attachment, transaction, "select data from batch_test where id = 1"}; + + BOOST_CHECK(select.execute(transaction)); + + const auto receivedBlobId = select.getBlobId(0); + BOOST_REQUIRE(receivedBlobId.has_value()); + + Blob reader{attachment, transaction, receivedBlobId.value(), BlobOptions().setType(BlobType::STREAM)}; + + std::vector buffer(1024); + const auto read = reader.read(buffer); + std::string result(reinterpret_cast(buffer.data()), read); + + BOOST_CHECK_EQUAL(result, "Registered blob data"); + } +} + +BOOST_AUTO_TEST_CASE(closeReleasesHandle) +{ + const auto database = getTempFile("Batch-closeReleasesHandle.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + { // scope + Transaction transaction{attachment}; + Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null)"}; + ddl.execute(transaction); + transaction.commit(); + } + + { // scope + Transaction transaction{attachment}; + Statement insert{attachment, transaction, "insert into batch_test (id) values (?)"}; + + Batch batch{insert, transaction}; + BOOST_CHECK(batch.isValid()); + + batch.close(); + BOOST_CHECK(!batch.isValid()); + } +} + +BOOST_AUTO_TEST_SUITE_END() From 6c417a03e775ec7ab11916e21bfd7998e17cc9f9 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Wed, 25 Feb 2026 02:01:06 +0000 Subject: [PATCH 2/5] Update AGENTS.md with maintainer code style preferences Add rules from PR #40 review: - Use lowercase suffixes for numeric literals (1u, not 1U) - Do not use string literals in assert() expressions - Do not explicitly define destructor as = default unless necessary - Prefer vector iterator-range constructor over allocate + memcpy --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ab752f4..20191f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,12 @@ } ``` - When adding new source files, copy the header from the existing files, but update the Copyright line to reflect the current year and use `$(git config --get user.name)` as the original author. +- Use lowercase suffixes for numeric literals (e.g. `1u`, `3u`, not `1U`, `3U`). +- Do not use string literals in `assert()` expressions (e.g. `assert(ptr && "msg")` is not idiomatic C++). +- Do not explicitly define a destructor as `= default` unless it is necessary (e.g. to make it virtual or to + control the definition translation unit). +- Prefer constructing `std::vector` directly from iterator ranges (constructor 5) instead of allocating and copying + with `std::memcpy`. ### Build - If .cpp files are added, it's necessary to run `cmake --preset default` from the repo root. From 56e86e0aed0b9f5797fc42cb4c90bca10e61b3e1 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Wed, 25 Feb 2026 02:01:19 +0000 Subject: [PATCH 3/5] Address PR #40 review feedback - Remove string literal from assert() expression - Fix brace style: remove braces from single-line if, add braces to multi-line if - Use vector iterator-range constructor instead of allocate + memcpy - Remove unnecessary explicit default destructor on BatchCompletionState - Rename getMetadata() to getInputMetadata() for consistency with Statement - Add getInputDescriptors() with lazy descriptor caching - Fix numeric suffix casing: U -> u in all test assertions --- src/fb-cpp/Batch.cpp | 56 ++++++++++++++++++++++++++++++++------------ src/fb-cpp/Batch.h | 15 +++++++----- src/test/Batch.cpp | 18 +++++++------- 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/fb-cpp/Batch.cpp b/src/fb-cpp/Batch.cpp index 2bdbf99..0d10df4 100644 --- a/src/fb-cpp/Batch.cpp +++ b/src/fb-cpp/Batch.cpp @@ -27,7 +27,6 @@ #include "Client.h" #include "Statement.h" #include "Transaction.h" -#include using namespace fbcpp; using namespace fbcpp::impl; @@ -154,7 +153,7 @@ void Batch::add(unsigned count, const void* inBuffer) void Batch::addMessage() { assert(isValid()); - assert(statement && "addMessage() requires the Statement-based constructor"); + assert(statement); handle->add(&statusWrapper, 1, statement->getInputMessage().data()); } @@ -236,7 +235,7 @@ void Batch::close() handle.reset(); } -FbRef Batch::getMetadata() +FbRef Batch::getInputMetadata() { assert(isValid()); @@ -246,6 +245,16 @@ FbRef Batch::getMetadata() return metadata; } +const std::vector& Batch::getInputDescriptors() +{ + assert(isValid()); + + if (inputDescriptors.empty()) + buildInputDescriptors(); + + return inputDescriptors; +} + // --- Internal helpers --- @@ -263,23 +272,18 @@ std::vector Batch::buildParametersBlock(Client& client, const Batc builder->insertInt(&statusWrapper, fb::IBatch::TAG_BUFFER_BYTES_SIZE, static_cast(bufferSize.value())); if (options.getBlobPolicy() != BlobPolicy::NONE) - { builder->insertInt(&statusWrapper, fb::IBatch::TAG_BLOB_POLICY, static_cast(options.getBlobPolicy())); - } if (options.getDetailedErrors() != 64) + { builder->insertInt( &statusWrapper, fb::IBatch::TAG_DETAILED_ERRORS, static_cast(options.getDetailedErrors())); + } const auto buffer = builder->getBuffer(&statusWrapper); const auto length = builder->getBufferLength(&statusWrapper); - std::vector result(length); - - if (length != 0) - std::memcpy(result.data(), buffer, length); - - return result; + return {buffer, buffer + length}; } std::vector Batch::prepareBpb(Client& client, const BlobOptions& bpb) @@ -296,10 +300,32 @@ std::vector Batch::prepareBpb(Client& client, const BlobOptions& b const auto buffer = builder->getBuffer(&statusWrapper); const auto length = builder->getBufferLength(&statusWrapper); - std::vector result(length); + return {buffer, buffer + length}; +} - if (length != 0) - std::memcpy(result.data(), buffer, length); +void Batch::buildInputDescriptors() +{ + auto metadata = getInputMetadata(); + const auto count = metadata->getCount(&statusWrapper); - return result; + inputDescriptors.reserve(count); + + for (unsigned index = 0u; index < count; ++index) + { + inputDescriptors.push_back(Descriptor{ + .originalType = static_cast(metadata->getType(&statusWrapper, index)), + .adjustedType = static_cast(metadata->getType(&statusWrapper, index)), + .scale = metadata->getScale(&statusWrapper, index), + .length = metadata->getLength(&statusWrapper, index), + .offset = metadata->getOffset(&statusWrapper, index), + .nullOffset = metadata->getNullOffset(&statusWrapper, index), + .isNullable = static_cast(metadata->isNullable(&statusWrapper, index)), + .name = metadata->getField(&statusWrapper, index), + .relation = metadata->getRelation(&statusWrapper, index), + .alias = metadata->getAlias(&statusWrapper, index), + .owner = metadata->getOwner(&statusWrapper, index), + .charSetId = metadata->getCharSet(&statusWrapper, index), + .subType = metadata->getSubType(&statusWrapper, index), + }); + } } diff --git a/src/fb-cpp/Batch.h b/src/fb-cpp/Batch.h index 0682e27..bcf4e57 100644 --- a/src/fb-cpp/Batch.h +++ b/src/fb-cpp/Batch.h @@ -27,6 +27,7 @@ #include "fb-api.h" #include "Blob.h" +#include "Descriptor.h" #include "SmartPtrs.h" #include "Exception.h" #include @@ -219,11 +220,6 @@ namespace fbcpp /// BatchCompletionState& operator=(const BatchCompletionState&) = delete; - /// - /// Disposes the underlying completion state handle. - /// - ~BatchCompletionState() noexcept = default; - public: /// /// Returns the number of messages processed. @@ -430,7 +426,12 @@ namespace fbcpp /// /// Returns the input metadata for this batch. /// - FbRef getMetadata(); + FbRef getInputMetadata(); + + /// + /// Returns cached input parameter descriptors for this batch. + /// + const std::vector& getInputDescriptors(); /// /// @} @@ -439,6 +440,7 @@ namespace fbcpp private: std::vector buildParametersBlock(Client& client, const BatchOptions& options); std::vector prepareBpb(Client& client, const BlobOptions& bpb); + void buildInputDescriptors(); private: Client* client; @@ -447,6 +449,7 @@ namespace fbcpp FbUniquePtr status; impl::StatusWrapper statusWrapper; FbRef handle; + std::vector inputDescriptors; }; } // namespace fbcpp diff --git a/src/test/Batch.cpp b/src/test/Batch.cpp index 462d771..f2dbb73 100644 --- a/src/test/Batch.cpp +++ b/src/test/Batch.cpp @@ -69,7 +69,7 @@ BOOST_AUTO_TEST_CASE(constructorFromStatementAndExecute) auto completionState = batch.execute(); - BOOST_CHECK_EQUAL(completionState.getSize(), 3U); + BOOST_CHECK_EQUAL(completionState.getSize(), 3u); BOOST_CHECK_EQUAL(completionState.getState(0), 1); BOOST_CHECK_EQUAL(completionState.getState(1), 1); BOOST_CHECK_EQUAL(completionState.getState(2), 1); @@ -120,7 +120,7 @@ BOOST_AUTO_TEST_CASE(constructorFromAttachmentAndExecute) BatchOptions().setRecordCounts(true)}; // Get metadata to build raw messages. - auto metadata = batch.getMetadata(); + auto metadata = batch.getInputMetadata(); FbUniquePtr tempStatus{CLIENT.newStatus()}; impl::StatusWrapper tempWrapper{CLIENT, tempStatus.get()}; @@ -143,7 +143,7 @@ BOOST_AUTO_TEST_CASE(constructorFromAttachmentAndExecute) auto completionState = batch.execute(); - BOOST_CHECK_EQUAL(completionState.getSize(), 3U); + BOOST_CHECK_EQUAL(completionState.getSize(), 3u); BOOST_CHECK_EQUAL(completionState.getState(0), 1); BOOST_CHECK_EQUAL(completionState.getState(1), 1); BOOST_CHECK_EQUAL(completionState.getState(2), 1); @@ -201,7 +201,7 @@ BOOST_AUTO_TEST_CASE(moveConstructorTransfersOwnership) moved.addMessage(); auto completionState = moved.execute(); - BOOST_CHECK_EQUAL(completionState.getSize(), 1U); + BOOST_CHECK_EQUAL(completionState.getSize(), 1u); transaction.commit(); } @@ -232,7 +232,7 @@ BOOST_AUTO_TEST_CASE(executeReportsNoInfoWhenRecordCountsDisabled) auto completionState = batch.execute(); - BOOST_CHECK_EQUAL(completionState.getSize(), 1U); + BOOST_CHECK_EQUAL(completionState.getSize(), 1u); BOOST_CHECK_EQUAL(completionState.getState(0), BatchCompletionState::SUCCESS_NO_INFO); transaction.commit(); @@ -272,14 +272,14 @@ BOOST_AUTO_TEST_CASE(executeWithBadDataReportsExecuteFailed) auto completionState = batch.execute(); - BOOST_CHECK_EQUAL(completionState.getSize(), 3U); + BOOST_CHECK_EQUAL(completionState.getSize(), 3u); BOOST_CHECK_EQUAL(completionState.getState(0), 1); BOOST_CHECK_EQUAL(completionState.getState(1), BatchCompletionState::EXECUTE_FAILED); BOOST_CHECK_EQUAL(completionState.getState(2), 1); auto errorPos = completionState.findError(0); BOOST_REQUIRE(errorPos.has_value()); - BOOST_CHECK_EQUAL(errorPos.value(), 1U); + BOOST_CHECK_EQUAL(errorPos.value(), 1u); // No more errors after position 1. BOOST_CHECK(!completionState.findError(errorPos.value() + 1).has_value()); @@ -367,7 +367,7 @@ BOOST_AUTO_TEST_CASE(blobWithIdEngine) auto completionState = batch.execute(); - BOOST_CHECK_EQUAL(completionState.getSize(), 1U); + BOOST_CHECK_EQUAL(completionState.getSize(), 1u); BOOST_CHECK_EQUAL(completionState.getState(0), 1); transaction.commit(); @@ -429,7 +429,7 @@ BOOST_AUTO_TEST_CASE(registerExistingBlob) auto completionState = batch.execute(); - BOOST_CHECK_EQUAL(completionState.getSize(), 1U); + BOOST_CHECK_EQUAL(completionState.getSize(), 1u); BOOST_CHECK_EQUAL(completionState.getState(0), 1); transaction.commit(); From 98feb391cbb171defa04fa100454bb0ce9ef3149 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes <529415+asfernandes@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:05:58 -0300 Subject: [PATCH 4/5] Misc --- src/fb-cpp/Batch.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fb-cpp/Batch.h b/src/fb-cpp/Batch.h index bcf4e57..e4c5ffe 100644 --- a/src/fb-cpp/Batch.h +++ b/src/fb-cpp/Batch.h @@ -195,6 +195,7 @@ namespace fbcpp /// static constexpr int SUCCESS_NO_INFO = fb::IBatchCompletionState::SUCCESS_NO_INFO; + public: /// /// Constructs a BatchCompletionState from a Firebird completion state handle. /// From 0438b14752c594d366d31a2d85e3fad2caa38fb7 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes <529415+asfernandes@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:08:28 -0300 Subject: [PATCH 5/5] Change BlobPolicy underlying type --- src/fb-cpp/Batch.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fb-cpp/Batch.h b/src/fb-cpp/Batch.h index e4c5ffe..16ad119 100644 --- a/src/fb-cpp/Batch.h +++ b/src/fb-cpp/Batch.h @@ -52,7 +52,7 @@ namespace fbcpp /// /// Selects the blob handling policy for a Batch. /// - enum class BlobPolicy : unsigned char + enum class BlobPolicy : std::uint8_t { /// /// Blobs are not allowed in the batch.