From 4df1bb59df24c8957adc63924a278d21e5641e74 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Sun, 16 Aug 2026 21:39:47 -0300 Subject: [PATCH] Add blob utility methods --- src/fb-cpp/Attachment.cpp | 80 +++++++++++++++++++++++++++++++++++++++ src/fb-cpp/Attachment.h | 25 ++++++++++++ src/test/Attachment.cpp | 59 +++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) diff --git a/src/fb-cpp/Attachment.cpp b/src/fb-cpp/Attachment.cpp index 17a3e9d..9030095 100644 --- a/src/fb-cpp/Attachment.cpp +++ b/src/fb-cpp/Attachment.cpp @@ -116,6 +116,86 @@ void Attachment::resetSession() &statusWrapper, nullptr, 0, "alter session reset", SQL_DIALECT_V6, nullptr, nullptr, nullptr, nullptr); } +std::optional Attachment::blobIdToString( + Transaction& transaction, std::optional blobId, const BlobOptions& options) +{ + if (!blobId.has_value()) + return std::nullopt; + + Blob reader{*this, transaction, blobId.value(), options}; + const auto lengthHint = reader.getLength(); + std::string value; + value.reserve(lengthHint); + std::vector buffer(std::numeric_limits::max()); + + for (;;) + { + const auto read = reader.readSegment(buffer); + + if (read == 0u) + break; + + value.append(buffer.data(), read); + } + + reader.close(); + + return value; +} + +std::optional> Attachment::blobIdToBytes( + Transaction& transaction, std::optional blobId, const BlobOptions& options) +{ + if (!blobId.has_value()) + return std::nullopt; + + Blob reader{*this, transaction, blobId.value(), options}; + const auto lengthHint = reader.getLength(); + std::vector value; + value.reserve(lengthHint); + std::vector buffer(std::numeric_limits::max()); + + for (;;) + { + const auto read = reader.readSegment(buffer); + + if (read == 0u) + break; + + value.insert(value.end(), buffer.data(), buffer.data() + read); + } + + reader.close(); + + return value; +} + +std::optional Attachment::blobIdFromString( + Transaction& transaction, std::optional value, const BlobOptions& options) +{ + if (!value.has_value()) + return std::nullopt; + + Blob writer{*this, transaction, options}; + writer.write(std::span{value->data(), value->size()}); + writer.close(); + + return writer.getId(); +} + +std::optional Attachment::blobIdFromBytes( + Transaction& transaction, std::optional> value, const BlobOptions& options) +{ + if (!value.has_value()) + return std::nullopt; + + Blob writer{*this, transaction, options}; + writer.write(value.value()); + writer.close(); + + return writer.getId(); +} + bool Attachment::execute(Transaction& transaction, std::string_view sql, const StatementOptions& options) { Statement statement{*this, transaction, sql, options}; diff --git a/src/fb-cpp/Attachment.h b/src/fb-cpp/Attachment.h index d22bdbb..957b6b5 100644 --- a/src/fb-cpp/Attachment.h +++ b/src/fb-cpp/Attachment.h @@ -27,6 +27,7 @@ #include "fb-cpp-export.h" #include "fb-api.h" +#include "Blob.h" #include "RowSet.h" #include "SmartPtrs.h" #include "StatementOptions.h" @@ -297,6 +298,30 @@ namespace fbcpp return handle; } + /// + /// Reads a blob as a string, or returns nullopt for a null identifier. + /// + std::optional blobIdToString( + Transaction& transaction, std::optional blobId, const BlobOptions& options = {}); + + /// + /// Reads a blob as bytes, or returns nullopt for a null identifier. + /// + std::optional> blobIdToBytes( + Transaction& transaction, std::optional blobId, const BlobOptions& options = {}); + + /// + /// Creates a blob from a string, or returns nullopt for a null value. + /// + std::optional blobIdFromString( + Transaction& transaction, std::optional value, const BlobOptions& options = {}); + + /// + /// Creates a blob from bytes, or returns nullopt for a null value. + /// + std::optional blobIdFromBytes( + Transaction& transaction, std::optional> value, const BlobOptions& options = {}); + /// /// Disconnects from the database. /// diff --git a/src/test/Attachment.cpp b/src/test/Attachment.cpp index 8512cc2..ca7b725 100644 --- a/src/test/Attachment.cpp +++ b/src/test/Attachment.cpp @@ -30,6 +30,8 @@ #include "fb-cpp/Transaction.h" #include #include +#include +#include #include @@ -95,6 +97,63 @@ BOOST_AUTO_TEST_CASE(createDatabaseWithForcedWritesOff) transaction.commit(); } +BOOST_AUTO_TEST_CASE(blobIdConversions) +{ + const auto database = getTempFile("Attachment-blobIdConversions.fdb"); + + Attachment attachment{getClient(), database, + AttachmentOptions().setCreateDatabase(true).setForcedWrites(false).setConnectionCharSet("UTF8")}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const auto streamOptions = BlobOptions().setType(BlobType::STREAM); + const auto multiSegmentSize = + static_cast(std::numeric_limits::max()) + std::size_t{1024}; + + std::string text(multiSegmentSize, '\0'); + for (std::size_t i = 0; i < text.size(); ++i) + text[i] = static_cast(i % 251); + + const auto textBlobId = attachment.blobIdFromString(transaction, text, streamOptions); + BOOST_REQUIRE(textBlobId.has_value()); + BOOST_CHECK(textBlobId->isEmpty() == false); + + const auto textResult = attachment.blobIdToString(transaction, textBlobId, streamOptions); + BOOST_REQUIRE(textResult.has_value()); + BOOST_CHECK_EQUAL(*textResult, text); + + std::vector bytes(multiSegmentSize); + for (std::size_t i = 0; i < bytes.size(); ++i) + bytes[i] = static_cast(i % 251); + + const auto bytesBlobId = attachment.blobIdFromBytes(transaction, bytes, streamOptions); + BOOST_REQUIRE(bytesBlobId.has_value()); + BOOST_CHECK(bytesBlobId->isEmpty() == false); + + const auto bytesResult = attachment.blobIdToBytes(transaction, bytesBlobId, streamOptions); + BOOST_REQUIRE(bytesResult.has_value()); + BOOST_CHECK(*bytesResult == bytes); + + std::string emptyText; + const auto emptyTextBlobId = attachment.blobIdFromString(transaction, emptyText, streamOptions); + BOOST_REQUIRE(emptyTextBlobId.has_value()); + const auto emptyTextResult = attachment.blobIdToString(transaction, emptyTextBlobId, streamOptions); + BOOST_REQUIRE(emptyTextResult.has_value()); + BOOST_CHECK(emptyTextResult->empty()); + + std::vector emptyBytes; + const auto emptyBytesBlobId = attachment.blobIdFromBytes(transaction, emptyBytes, streamOptions); + BOOST_REQUIRE(emptyBytesBlobId.has_value()); + const auto emptyBytesResult = attachment.blobIdToBytes(transaction, emptyBytesBlobId, streamOptions); + BOOST_REQUIRE(emptyBytesResult.has_value()); + BOOST_CHECK(emptyBytesResult->empty()); + + BOOST_CHECK(!attachment.blobIdFromString(transaction, std::nullopt, streamOptions).has_value()); + BOOST_CHECK(!attachment.blobIdFromBytes(transaction, std::nullopt, streamOptions).has_value()); + BOOST_CHECK(!attachment.blobIdToString(transaction, std::nullopt, streamOptions).has_value()); + BOOST_CHECK(!attachment.blobIdToBytes(transaction, std::nullopt, streamOptions).has_value()); +} + BOOST_AUTO_TEST_CASE(executePreparesAndExecutesStatement) { const auto database = getTempFile("Attachment-executePreparesAndExecutesStatement.fdb");